# Deep Dive into Workflow Execution in Microsoft Agent Framework

**Disclaimer** : This article is highly technical and assumes that you have strong understanding on the concepts of Edges and Executors in MAF. If you are not comfortable with Executors and Edges and how they control the flow of execution in MAF Workflows then I would highly recommend reviewing this [article](https://learn.microsoft.com/en-us/agent-framework/workflows/) and also my [article](https://www.azureguru.net/workflows-in-microsoft-agent-framework-executors-edges-and-events) on the topic.

### Use Case

Lets assume the following use case.

![Microsoft Agent Framework](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/94395484-9843-4826-a886-cc3836d02086.png align="center")

We have an input number `N` that requires to be identified as a Prime or a non Prime number.

If its a Prime number then compute its square root and send the value through **Route A** .

If its not a Prime number send it through **Route B** without its square root.

If the number is unidentifiable or unknown route the number to an Email dispatcher.

I know some of you might argue that there is no real need to use an Agentic approach to solve a problem like this. In fact, this workflow could easily be implemented using traditional programmable logic or a simple procedural flow.

The purpose of this article is not to demonstrate the most optimal implementation of a real-life use case. Instead, the goal of this article is to demonstrate how Agentic Workflows can be designed and orchestrated within MAF using multiple execution paths and conditional routing.

### **SetUp**

Create a new C# console application and add the following packages

```csharp
dotnet add package Azure;
dotnet add package Azure.AI.OpenAI;
dotnet add package Microsoft.Agents.AI;
dotnet add package Microsoft.Extensions.AI;
dotnet add package Microsoft.Extensions.Configuration;
dotnet add package Microsoft.Extensions.DependencyInjection;
dotnet add package Microsoft.Extensions.Logging;
dotnet add package Microsoft.Agents.AI.Workflows;
```

Add `appsetting.json` to the project

```csharp
"AppSettings": 
{ 
    "Chat_DeploymentName": "Deployment Name",
    "EndPoint": "Azure OpenAI endpoint",
    "ApiKey": "Azure OpenAI API key"
}
```

### **Code**

Now that all the underlying artifacts in place, add the following code to read the settings from `appsettings.json` and register `ChatClient`.

```csharp
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.Build();

 var credential = new AzureKeyCredential(configuration["AppSettings:ApiKey"]);

 servicecollection.AddKeyedChatClient("ChatClient", (sp => new AzureOpenAIClient(
          new Uri(configuration["AppSettings:EndPoint"]), credential)
              .GetChatClient(configuration["AppSettings:Chat_DeploymentName"]).AsIChatClient()));
```

**Create a DI service registry.**

```csharp
 ServiceCollection servicecollection = new ServiceCollection();
```

Now that the basic set up is ready, create two agents.

First agent is to identify if the input number is a prime number and the second agent to calculate the square root of prime number identified by the first agent.

**PrimeNumberIdentifierAgent >>**

```csharp
servicecollection.AddSingleton<AIAgent>(sp =>

{
    Func<ChatClientAgentOptions> func = () =>
   {
       return new ChatClientAgentOptions
       {
           ChatOptions = new ChatOptions
           {
               Instructions = "You are a prime number detection assistant that identifies if the number is prime or not a prime",
               ResponseFormat = ChatResponseFormat.ForJsonSchema<DetectionResult>()
           },
           Name = "TypeDetectionAgent",
           Id = "1"
       };
   };

    return new ChatClientAgent(sp.GetKeyedService<IChatClient>("ChatClient"), options: func());

});
```

**SquareRootGeneratorAgent >>**

```csharp
  servicecollection.AddSingleton<AIAgent>(sp =>

  {
      Func<ChatClientAgentOptions> func = () =>
     {
         return new ChatClientAgentOptions
         {
             ChatOptions = new ChatOptions
             {
                 Instructions = "You are an square root generator assistant. You will return the square root of the given prime number.",
                 ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema<_Response>()
             },
             Name = "SquareRootAgent",
             Id = "2"
         };
     };
      return new ChatClientAgent(sp.GetKeyedService<IChatClient>("ChatClient"), options: func());
  });
```

Create a Dependency Injection (DI) container **serviceProvider** from the registered services in **serviceCollection** and fetch the list of registered agents.

```csharp
ServiceProvider serviceprovider = servicecollection.BuildServiceProvider();

var AIAgents = serviceprovider.GetServices<AIAgent>();
List<AIAgent> lst = new(AIAgents);

AIAgent TypeDetectionAgent = lst.FirstOrDefault(args => args.Id == "1");

AIAgent SquareRootAgent = lst.FirstOrDefault(args => args.Id == "2");
```

**NumberType**

Declare an Enum type and set its options to **PrimeNumber, NotPrimeNumber** and **UnSure**.

```csharp
 public enum NumberType
 {
     PrimeNumber,
     NotPrimeNumber,   
     UnSure
 }
```

**InputNumber**

Define an object for **InputNumber** with the following properties.

```csharp
 internal sealed class InputNumber
 {
     [JsonPropertyName("id")]
     public string Id { get; set; } = String.Empty;

     [JsonPropertyName("inputnumber")]
     public string Value { get; set; } = String.Empty;
 }
```

**DetectionResult**

Define a class to represent the result of detection operation for the Input number along with the underlying reasons.

```csharp
 public sealed class DetectionResult
 {
     [JsonPropertyName("numberType")]
     [JsonConverter(typeof(JsonStringEnumConverter))]
     public NumberType numberType { get; set; }

     [JsonPropertyName("reason")]
     public string Reason { get; set; } = string.Empty;

     [JsonPropertyName("id")]
     public string Id { get; set; } = string.Empty;
 }
```

**\_Response**

Class to represent the output of the executor

```csharp
public sealed class _Response
{
    [JsonPropertyName("decision")]
    public string Decision { get; set; } = string.Empty;

    [JsonPropertyName("reason")]
    public string Reason { get; set; } = string.Empty;

    [JsonPropertyName("id")]
    public string Id { get; set; } = string.Empty;

    [JsonPropertyName("squareroot")]
     public double squareroot { get; set; } = 0.00;
}
```

**ScopeName**

**ScopeName** is required to identify shared states across agents. It ensures that Executors can access the same data in a State during the workflow execution.

```csharp
internal static class NumbervalueConstants
{
    public const string NumbervalueScope = "Numbervalue";
}
```

**Note** : **NumbervalueScope** can also be declared as a variable instead of an object.

### Executors

For our use case define two Executors.

The first executor will write to the State and the second one will read from it.

This is done by overriding the **HandleAsync** method of the State. You can find more details of Executor structure [here](https://www.azureguru.net/workflows-in-microsoft-agent-framework-executors-edges-and-events#executors).

**TypeDetectionExecutor**

The first Executor will detect if the input number is a prime number or not through the **PrimeNumberIdentifierAgent** declared earlier.

As I explained in my previous article, an Executor should inherit from the Executor base class and override **HandleAsync** method that exposes **IWorkflowContext** interface.

Create an Executor class that accepts input of type **ChatMessage** and the output is **typeof(DetectionResult)** object (defined earlier).

**PrimeNumberIdentifierAgent** (declared earlier) is injected into the Executor class through constructor injection as **typeDetectionAgent**.

```csharp
internal sealed class TypeDetectionExecutor : Executor<ChatMessage, DetectionResult>
{
    private readonly AIAgent _typeDetectionAgent;
    private List<ChatMessage> messages = new();

    public TypeDetectionExecutor(AIAgent typeDetectionAgent) : base("TypeDetectionExecutor")
    {
        this._typeDetectionAgent = typeDetectionAgent;
    }

    [MessageHandler]
    public override async ValueTask<DetectionResult> HandleAsync(
        ChatMessage message,
        IWorkflowContext context,
        CancellationToken cancellationToken = default
    )
    {
        messages.Add(message);
        var newNumber = new InputNumber { Id = Guid.NewGuid().ToString(), Value = (message.Text) };
        await context.QueueStateUpdateAsync(
            newNumber.Id,
            newNumber,
            scopeName: NumbervalueConstants.NumbervalueScope,
            cancellationToken
        );
        var response = await this._typeDetectionAgent.RunAsync(
            message,
            cancellationToken: cancellationToken
        );
        var detectionResult = JsonSerializer.Deserialize<DetectionResult>(response.Text);
        detectionResult.Id = newNumber.Id;
        return detectionResult;
    }
}
```

**Note**: There are two properties, **Id** and **Value** in the object **InputNumber**.

The values of these two properties are set in the above code through the **HandleAync** overridden method. The above code also updates the State with these values for the given scope **NumbervalueScope** defined earlier.

The code below shows how it is implemented.

```csharp
 messages.Add(message);
 var newNumber = new InputNumber
 {
     Id = Guid.NewGuid().ToString(),
     Value = (message.Text)
 };
 await context.QueueStateUpdateAsync(newNumber.Id, newNumber, scopeName: NumbervalueConstants.NumbervalueScope, cancellationToken);
```

In the next step, invoke the **typedetectionagent** which in turn sets the values for its properties defined in the **DetectionResult** object.

```csharp
var response = await this._typeDetectionAgent.RunAsync(message, cancellationToken: cancellationToken);
detectionResult.Id = newNumber.Id;
```

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/f0efffd3-40d1-44a3-89ae-039fd2aaddbc.png align="center")

> The id value marked in the above screenshot has no bearing to the value of id stored in the State. The id format updated in the state is of the form `Id = Guid.NewGuid().ToString()`

Hence we have to explicitly set the value of **Id** property of the **DetectionResult** object in **TypeDetectionExecutor**.

```csharp
detectionResult.Id = newNumber.Id;
```

And finally the response text is Deserialized with return type of the form **tyepof(DetectionResult)**.

```csharp
var detectionResult = JsonSerializer.Deserialize<DetectionResult>(response.Text);
return detectionResult;
```

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/5d06b45b-b4b1-4040-82ff-7542a81dbdb4.png align="center")

In the next step, define an Executor that returns the square root of the given prime number through the **SquareRootAgent**.

The Executor should accept **tyepof(DetectionResult)** as its input and read the State based on the values of **Id** , **NumberType** and **Scopename**.

**Id** and **NumberType** are two properties defined in the **DetectionResult** object .

But before that we need to define a method that checks the value of the property **NumberType** in the **Detectionresult** object.

**CheckCondition**

The **CheckCondition** method creates and returns a condition function that checks if the object is of type **DetectionResult** and if it is, then check if it contains a specific **NumberType** based on the input parameter of type **NumberType** passed to it and return either true or false.

```csharp
 private static Func<object?, bool> CheckCondition(NumberType Type) =>
 detectionResult =>
     detectionResult is DetectionResult result &&
     result.numberType == Type;
```

**Note** : Recall that there is a **NumberType** property in the **DetectionResult** object.

```csharp
 [JsonPropertyName("numberType")]
 [JsonConverter(typeof(JsonStringEnumConverter))]
 public NumberType numberType { get; set; }
```

Later in the article we will use the function **CheckCondition** while setting up the workflow.

Lets move ahead and define an Executor that returns the square root value of the prime number.

**SquareRootExecutor**

This Executor calculates the square root of the given number only if that number has been identified as a prime number by the **TypeDetectionExecutor**.

**SquareRootAgent** (declared earlier) is injected into the Executor class through constructor injection as **squarerootnumberexecutionAssistantAgent**.

```csharp
internal sealed class SquareRootExecutor : Executor<DetectionResult, _Response>
{
    private readonly AIAgent _squarerootnumberexecutionAssistantAgent;

    public SquareRootExecutor(AIAgent squarerootnumberexecutionAssistantAgent)
        : base("SquareRootExecutor")
    {
        this._squarerootnumberexecutionAssistantAgent = squarerootnumberexecutionAssistantAgent;
    }

    [MessageHandler]
    public override async ValueTask<_Response> HandleAsync(
        DetectionResult result,
        IWorkflowContext context,
        CancellationToken cancellationToken = default
    )
    {
        if (result.numberType != NumberType.PrimeNumber)
        {
            throw new InvalidOperationException("This executor should only handle prime numbers.");
        }

        var number =
            await context.ReadStateAsync<InputNumber>(
               result.Id,
               scopeName:NumbervalueConstants.NumbervalueScope,
               cancellationToken
            ) ?? throw new InvalidOperationException("Number not found.");

        var response_ = JsonSerializer.Deserialize<_Response>(
            (
                await this._squarerootnumberexecutionAssistantAgent.RunAsync(
                    number.Value,
                    cancellationToken: cancellationToken
                )
            ).Text
        );
        response_.Id = result.Id;
        return response_!;
    }
}
```

The above approach is similar to that of **TypeDetectionExecutor**.

The only difference here is that the **TypeDetectionExecutor** has **ChatMessage** as its input and **typeof(DetectionResult)** as the output .

In the case of **SquareRootExecutor** the **DetectionResult** acts as input and the Deserialized value of **typeof(\_Reponse)** is the output read from the State.

```csharp
internal sealed class SquareRootExecutor : Executor<DetectionResult, _Response>
```

In the following line of code, retrieve the value from the State through **result.Id** and if found, use it as an input to the **SquareRootAgent** in form of **number.Value**.

```csharp
var number = await context.ReadStateAsync<InputNumber>(result.Id, scopeName: NumbervalueConstants.NumbervalueScope, cancellationToken)
?? throw new InvalidOperationException("Number not found.");

var response_ = JsonSerializer.Deserialize<_Response>(
            (
                await this._squarerootnumberexecutionAssistantAgent.RunAsync(
                    number.Value,
                    cancellationToken: cancellationToken
                )
            ).Text
        );
```

As was the case with **TypeDetectionExecutor** , here also we explicitly set the value for the **Id** property in the **\_Response** object.

```csharp
 response_.Id = result.Id;
```

In the next step, we require an Executor that can action the input classified as a non prime number by the **TypeDetectionExecutor** . But in this case there is no need for an Agent as **TypeDetectionExecutor** has already classified the input.

So all we have to do is create Executor that reads the **NumberType** Enum value from the State where **DetectionResult** is the input.

**PrimeNo\_SendDetails\_RouteA\_Executor**

This Executor **YieldsOutput** when the **Inputnumber** is a prime number.

```csharp
internal sealed class PrimeNo_SendDetails_RouteA_Executor() : Executor<_Response>("PrimeNo_SendDetails_RouteA_Executor")
{
    [YieldsOutput(typeof(string))]
     public override async ValueTask HandleAsync(DetectionResult result, IWorkflowContext context, CancellationToken cancellationToken = default)
     {
         if (result.numberType == NumberType.PrimeNumber)
         {
             var number = await context.ReadStateAsync<InputNumber>(result.Id, scopeName: NumbervalueConstants.NumbervalueScope);
             await context.YieldOutputAsync($"Given number {number.Value} marked as prime: {result.Reason}", cancellationToken);
         }
         else
         {
             throw new InvalidOperationException("This executor should only handle prime numbers.");
         }
     }
}
```

**Non\_PrimeNo\_SendDetails\_RouteB\_Executor**

This Executor **YieldsOutput** when the **Inputnumber** is a non prime number.

```csharp
 internal sealed class Non_PrimeNo_SendDetails_RouteB_Executor() : Executor<DetectionResult>("Non_PrimeNo_SendDetails_RouteB_Executor")
 {
     [YieldsOutput(typeof(string))]
     public override async ValueTask HandleAsync(DetectionResult result, IWorkflowContext context, CancellationToken cancellationToken = default)
     {
         if (result.numberType == NumberType.NotPrimeNumber)
         {
             var number = await context.ReadStateAsync<InputNumber>(result.Id, scopeName: NumbervalueConstants.NumbervalueScope);
             await context.YieldOutputAsync($"Given number {number.Value} marked as non prime: {result.Reason}", cancellationToken);
         }
         else
         {
             throw new InvalidOperationException("This executor should only handle non prime numbers.");
         }
     }
 }
```

**UnsureExecutor**

This Executor **YieldsOutput** when the **Inputnumber** is unknown.

```csharp
internal sealed partial class UnsureExecutor() : Executor<DetectionResult>("UnsureExecutor")
  {
      [YieldsOutput(typeof(string))]
      public override async ValueTask HandleAsync(DetectionResult result, IWorkflowContext context, CancellationToken cancellationToken = default)
      {
          if (result.numberType == NumberType.UnSure)
          {
              var number = await context.ReadStateAsync<InputNumber>(result.Id, scopeName: NumbervalueConstants.NumbervalueScope);
              await context.YieldOutputAsync($"Input marked as unsure: {result.Reason}.Email sent for the input value : {number?.Value}");
          }
          else
          {
              throw new ArgumentException("This executor should only handle uncertain inputs");
          }
      }
  }
```

If you understand working of **TypeDetectionExecutor** & **SquareRootExecutor**, understanding **NonPrimeNumberExecutor** and **UnsureExecutor** isn't that difficult.

The input to above two Executors is **typeof(DetectionResult)** and the Executors add output to the workflow output queue through **YieldOutputAsync**.

> Now that we have all the required executors, objects, and agents, it’s time to add them to the workflow and define their execution flow.

### WorkFlow

```plaintext
                          typeDetectionExecutor
                                   |
           -----------------------------------------------
           |                       |                     |
           v                       v                     v
     PrimeNumber               NotPrimeNumber          Default
           |                       |                     |
           v                       v                     v
PrimeNo_SquareRootExecutor   Executor_RouteB    Unsure_Executor 
           |
           |
           v
   Executor_RouteA
```

Our entry point to the workflow will be the **TypeDetectionExecutor**.

```csharp
var typeDetectionExecutor = new TypeDetectionExecutor(TypeDetectionAgent);
WorkflowBuilder builder = new(typeDetectionExecutor);
```

Next ,create instances of all the Executor that will be used inside a workflow to define **Edges** and **Output**.

```csharp
var PrimeNo_SquareRootExecutor = new SquareRootExecutor(SquareRootAgent);
var SendDetails_Executor_RouteA = new PrimeNo_SendDetails_RouteA_Executor();
var SendDetails_Executor_RouteB = new Non_PrimeNo_SendDetails_RouteB_Executor();
var SendEmail_Executor_Unsure = new UnsureExecutor();
```

Define **Switches** and **Conditions**.

```csharp
builder.AddSwitch(typeDetectionExecutor, switchBuilder =>
  switchBuilder
  .AddCase(
      CheckCondition(NumberType.PrimeNumber),
      PrimeNo_SquareRootExecutor
      )
  .AddCase(
      CheckCondition(NumberType.NotPrimeNumber),
      SendDetails_Executor_RouteB
  )
  .WithDefault(
      SendEmail_Executor_Unsure
         )
  )
.AddEdge(PrimeNo_SquareRootExecutor,SendDetails_Executor_RouteA)
.WithOutputFrom(SendDetails_Executor_RouteA, SendDetails_Executor_RouteB, SendEmail_Executor_Unsure);
```

The **typeDetectionExecutor** acts as the Executor binding source to the **WorkFlow**.

Earlier in the article I mentioned that we will use the **CheckCondition** method which returns ***true/false*** based on the value of **NumberType**.

The **CheckCondition** method is used to validate the conditions associated with each **AddCase(...)** statement within the switch statement.

![Microsoft Agent Framework](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/f56d6304-5e2e-4b46-ad35-dd030289a0e0.png align="center")

The **AddCase(...)** function above expects two parameters :

The first parameter is of type **Func<T, bool>**, where the **Func** accepts an input object of type ***T*** which in our case is **type(DetectionResult)**.

The **Func** used is the **CheckCondition** function that returns a Boolean value.

The second parameter is an **Executor** or **IEnumberable**.

In the next step define an **Edge** with the following condition.

```csharp
.AddEdge(PrimeNo_SquareRootExecutor, SendDetails_Executor_RouteA)
```

The workflow executes the **PrimeNo\_SquareRootExecutor** only when the following case is true

```csharp
.AddCase(
    CheckCondition(NumberType.PrimeNumber),
    PrimeNo_SquareRootExecutor
)
```

and if not it proceeds to **WithOutPutFrom(...)** .

```csharp
 .WithOutputFrom(SendDetails_Executor_RouteA,SendDetails_Executor_RouteB, SendEmail_Executor_Unsure)
```

**WithOutPutFrom(..)** registers Executors that **YieldsOutput**.

![Microsoft Agent Framework](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/a708d89e-dc36-46d2-b27b-7d5fb9df6ee4.png align="center")

In our case it will be SendDetails\_Executor\_RouteA, SendDetails\_Executor\_RouteB and SendEmail\_Executor\_Unsure.

Now you might question as to why do we have SendDetails\_Executor\_RouteA as part of **AddEdge(...)** and also part of **WithOutputFrom(...)**.

Let me try to explain :

**AddEdge(...)** defines the execution path .

When the given number is not a prime number we have to find the square root of that number . That is done through the PrimeNo\_SquareRootExecutor. The execution then needs to flow to the SendDetails\_Executor\_RouteA Executor as its next step.

Since the switch only routes execution to **PrimeNo\_SquareRootExecutor** , we explicitly define an edge using **AddEdge(...)** to instruct the workflow engine about the next Executor that should be executed in the workflow path.

**Note** : **AddEdge(...)** only defines connections between executors. To produce an output we have to use **WithOutputFrom(...)**.

But then why haven't we defined **AddEdge(...)** for SendEmail\_Executor\_Unsure or SendDetails\_Executor\_RouteB ?

This is because **AddCase(...)** and **WithDefault(...)** already creates the execution routes to those Executors. Both of these flows directly reach their final Executors and do not require any additional execution step which is required in the case of PrimeNo\_SquareRootExecutor.

Once the execution of PrimeNo\_SquareRootExecutor completes, the workflow still needs to continue to SendDetails\_Executor\_RouteA.

Therefore, we explicitly define this continuation path using \*\*AddEdge(...)\*\*and the output from SendDetails\_Executor\_RouteA is captured from **WithOutputFrom(...)**.

### Execution

```csharp
var workflow = builder.Build();

string input = "Your Input Number/Letter";

await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, new ChatMessage(ChatRole.User, input));

  await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
  await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
  {
    if (evt is WorkflowOutputEvent outputEvent)
    {
     Console.WriteLine($"{outputEvent}");
    }
  }
```

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/2cd5d3de-ca52-4965-8203-2f5bcfc98413.gif align="center")

> In the screengrab above at line 140, the first executor is named "primenumberExecutor" but in code its "PrimeNo\_SquareRootExecutor".
> 
> They are simply different names but are the same executor.

### Conclusion

First of all thank you for making all the way till here :) . I understand the complexity involved in setting up of workflows in Microsoft Agent Framework .

Through this article , I have tried my best within the best of my abilities to explain the overall execution flow of workflows in MAF. At the beginning , the implementation looks very complicated and overwhelming but once you understand and get into the essence of the overall steps involved the entire flow looks very easy.

Another crucial aspect of Workflow execution in MAF is Checkpointing which I will deep dive into in my next article.

Till then Ciao !!!
