# Workflow Orchestration Patterns in Microsoft Agent Framework

My previous [article](https://www.azureguru.net/workflows-in-microsoft-agent-framework-executors-edges-and-events) focused on introduction of workflows in MAF along with a basic overview of Executors, Edges and Events.

This article will explore different workflow orchestration patterns available in MAF. Orchestration patterns help manage and coordinate workflow execution in specific ways depending on the use case.

The available orchestration types available in MAF are:

*   **Sequential :** They execute one after another in a defined order.
    
*   **Concurrent** : The agents in this pattern execute in parallel .
    
*   **Handoff** : Here the agents transfer control to each other based on context.
    
*   **Group Chat** : The agents in group chat collaborate in a shared conversation
    
*   **Magnetic :** A manager agent dynamically coordinates specialized agents
    

Of the above patterns, **Magnetic** pattern is not supported in C# , so this article will skip that and focus on the remaining four.

In MAF when an **AIAgent** is passed to **WorkflowBuilder**, MAF automatically wraps it in an **AIAgentBinding** which creates the underlying **AIAgentHostExecutor**.

The workflow output can be streamed dynamically through.

```csharp
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
```

### **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 we have all the underlying artifacts in place, add the following code to read the settings from `appsettings.json`

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

Create a DI service registry.

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

Now that we have the basic set up ready, lets create specific agents for different workflow orchestration patterns.

### Sequential Orchestration Pattern

Lets assume, we have a translation agent that translates given text into different languages. In our example the languages are French, German and Spanish.

Below we set up a translation agent that translates a given text from English into desired language/s.

```csharp
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()));

   servicecollection.AddSingleton < AIAgent > (sp =>
     {
       Func < ChatClientAgentOptions > func = () => {
         return new ChatClientAgentOptions {
           ChatOptions = new ChatOptions {

               Instructions = "You are a translation agent.You translate text from English to another language.Translate the value in 'Text' into the language specified in 'Target Language'.Requires parameter:Text (string),  Target Language (string).Return only the translated text. ",
             },
             Name = "TranslationAgent",
             Id = "1"
         };
       };

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

     });
```

We create a Dependency Injection (DI) container `serviceProvider` from the registered services in `serviceCollection`.

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

We specify a list of languages along with the text that requires to be translated.

> The agent executor in C# supports multiple input types, including `string`, `ChatMessage`, and `IEnumerable<ChatMessage>`. When a `string` is provided as input, it is automatically converted into a `ChatMessage` with the `User` role.

So the input query can either be

```csharp
string  messages_input = $"Target Language: \"French\", \"German\", \"Spanish\" and Text: Hello ";
```

Or

```csharp
 List<ChatMessage> messages_input = [
new(ChatRole.System, $"Target Language: \"French\", \"German\", \"Spanish\" and Text: Hello")];
```

**Note:** Above we have specified `Target Language` and `Text` variables in the Agent instructions.

We resolve the registered agent from the DI container and set the collection of type `List<AIAgent>` .

```csharp
List<AIAgent> agent = new(serviceProvider.GetServices<AIAgent>());
```

Next, we declare list of type `List<Messages>` to capture and store the workflow output.

```csharp
 List<ChatMessage> messages_output = [];
```

We then initiate asynchronous streaming execution of the workflow and also set the workflow execution pattern type . In this example it is `Sequential`.

```csharp
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(AgentWorkflowBuilder.BuildSequential(agent), messages_input);
```

We can use the Events listed [here](https://www.azureguru.net/workflows-in-microsoft-agent-framework-executors-edges-and-events#events) to trace the output across each stage.

```csharp
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));

await foreach(WorkflowEvent evt in run.WatchStreamAsync()) 
{
  if (evt is AgentResponseUpdateEvent update) {    
    AgentResponse response = update.AsResponse();
    foreach(ChatMessage message in response.Messages.Where(a => a.Role.Value == "assistant" && a.Text != "")) {
      messages_output.Add(new(ChatRole.Assistant, message.Text));
    }
  } 
else if (evt is WorkflowErrorEvent workflowError) {   
    Console.Error.WriteLine(workflowError.Exception?.ToString() ?? "Unknown workflow error occurred.");   
  } 
else if (evt is ExecutorFailedEvent executorFailed) {   
    Console.Error.WriteLine($"Executor '{executorFailed.ExecutorId}' failed with {(executorFailed.Data == null ? "
      unknown error " : $"
      exception {executorFailed.Data}")}.");   
  }
}
Console.WriteLine(string.Join("", messages_output.Where(a => a.Role.Value == "assistant").Select(a => a.Text))
```

In the above code, the agent processes its cached messages only after receiving a `TurnToken`.

```csharp
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
```

If `TurnToken` is not sent the `AIAgentHostExecutor` will not process the workflow.

**Execution >>**

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/4059aa9d-68d7-433e-bb11-2efae61357a3.gif align="center")

### Concurrent Orchestration Pattern

For **Concurrent pattern** , the complete code stays the same as the **Sequential pattern**. The only change required is to change **AgentWorkflowBuilder** type from **BuildSequential** to **BuildConcurrent**.

```csharp
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(AgentWorkflowBuilder.BuildConcurrent(agent), messages_input);
```

### HandOff Orchestration Pattern

HandOff orchestration at first glance might look similar to **agents-as-tools** but there is a subtle difference between the two.

In **agent-as-tools** you basically use an agent as tool for another agent through **Agent.AsAIFunction()** which performs the execution on behalf of the calling agent.

For example , below the **AnotherAgent** performs call to **StockExchangeAgent** as agent-as-tool through **Agent.AsAIFunction()**.

```csharp
StockExchangeAgent =......
     .AsAIAgent(
        model: "your model",
        instructions: "You answer questions about stocks",
        name: "StockExchangeAgent",
        description: "An agent that answers questions about the stocks.",
        tools: [AIFunctionFactory.Create(GetStockvalue)]);

AnotherAgent =......     
       .AsAIAgent(
        model: "your model",
        instructions: "You are a helpful assistant.",
        tools: [StockExchangeAgent.AsAIFunction()]);
```

But in `Hand-Off` pattern, the agent receiving the hand-off takes complete responsibility of the entire execution.

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/48758ad7-8df2-4aea-bf9c-7351c6e371a9.png align="center")

An agent dynamically transfers control to another agent after completing its task.

To put in perspective, a handoff occurs when one agent passes responsibility, conversational context and execution flow to another agent to maintain continuity of the execution flow.

Lets look at an sample scenario. Assume we have three agents

*   **Math Agent** : That answers ONLY Math questions.
    
*   **Chemistry Agent** : That answers ONLY Chemistry questions.
    
*   **Moderator Agent** : Has access to the above two specialized agents and acts as a coordinator and is aware of its role in the workflow and agents that it has at its disposal.
    

We have to ensure that all the above agents are aware of their roles and responsibilities during the execution. This can be implemented by setting relevant instructions for every agent when setting them up.

**Moderator Agent >>**

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

 {
     Func<ChatClientAgentOptions> func = () =>
    {
        return new ChatClientAgentOptions
        {
            ChatOptions = new ChatOptions
            {
                Instructions = "You determine which agent to use based on the user questions. ALWAYS handoff to another agent and do not try to respond by yourself. If you don't find any relevant agent for a specific user question inform the user that the question is irrelevant."
            },
            Name = "ModeratorAgent",
            Id = "1"
        };
    };

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

**Math Agent >>**

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

{
    Func<ChatClientAgentOptions> func = () =>
   {
       return new ChatClientAgentOptions
       {
           ChatOptions = new ChatOptions
           {
               Instructions = "You provide help with math problems. Explain your reasoning at each step and include examples. Only respond about math queries."
           },
           Name = "MathAgent",
           Id = "2"
       };
   };

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

**Chemistry Agent >>**

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

{
    Func<ChatClientAgentOptions> func = () =>
   {
       return new ChatClientAgentOptions
       {
           ChatOptions = new ChatOptions
           {
               Instructions = "You provide help with chemistry problems. Explain your reasoning at each step and include examples. Only respond about chemistry queries."
           },
           Name = "ChemistryAgent",
           Id = "3"
       };
   };

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

Once the agents are defined, we have to set up the workflow but before that we fetch the list of agents from the DI container

```csharp
var agents = serviceProvider.GetServices<AIAgent>();
List<AIAgent> AgentList = new(agents);
```

We then define the workflow

```csharp
#pragma warning disable MAAIW001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
 var workflow = AgentWorkflowBuilder.CreateHandoffBuilderWith(AgentList[0])
.WithHandoffs(AgentList[0], [AgentList[1], AgentList[2]])
.WithHandoffs([AgentList[1], AgentList[2]], AgentList[0])
.Build();
#pragma warning restore MAAIW001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
```

> Since handoff workflow feature is in preview, ensure that you have set the #pragma warning MAAIW001

The `AgentList[index]` above is derived from the `agents` collection.

In the next step , we ask a series of questions. One related to Math, one related to Chemistry and the last one related to History.

We declare a variable called `lastExecutorId` to differentiate responses between individual executors.

The user queries are passed through `List<ChatMessage> messages = [];` while the chat history is stored in `List<ChatMessage> messages_output = [];`.This is optional incase the chat history is required to be stored for further analysis. You can refer to my this [article](https://www.azureguru.net/chat-history-in-microsoft-agent-framework) on storing chat history in MAF.

We pass a series of questions to the agents.

```csharp
List<ChatMessage> messages = [];
List<ChatMessage> messages_output = [];
messages.Add(new(ChatRole.User, "What is a derivative?"));
messages.Add(new(ChatRole.User, "What is the Chemical formula for water?"));
messages.Add(new(ChatRole.User, "Tell me more about Middle ages"));
string? lastExecutorId = null;
```

and set up the workflow builder.

```csharp
  foreach (var msg in messages)
  {
      await using StreamingRun run_ = await InProcessExecution.RunStreamingAsync(workflow, msg);

      messages_output.Add(new(ChatRole.User, msg.Text));
      await run_.TrySendMessageAsync(new TurnToken(emitEvents: true));
      await foreach (WorkflowEvent evt in run_.WatchStreamAsync())
      {         

          if (evt is AgentResponseUpdateEvent e)
          {
              if (e.ExecutorId != lastExecutorId)
              {
                  Console.WriteLine();
                  lastExecutorId = e.ExecutorId;
                  Console.WriteLine();

              }

              Console.Write(e.Update.Text);
              messages_output.Add(new(ChatRole.Assistant, e.Update.Text));
              if (e.Update.Contents.OfType<FunctionCallContent>().FirstOrDefault() is FunctionCallContent call)
              {
                  Console.ForegroundColor = ConsoleColor.Green;
                  Console.WriteLine("---------------------------------------------------------------");
                  Console.WriteLine($"  [Calling function '{call.Name}' with arguments: {JsonSerializer.Serialize(call.Arguments)}]");
                  

                  Console.WriteLine("The user query is : " + msg);
                  Console.WriteLine("---------------------------------------------------------------");
                  Console.ResetColor();
              }

          }

      }

  }
```

**Execution >>**

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/a22e8a5e-0c0e-4240-b00a-e19d94c7a9fe.gif align="center")

### Group Chat Orchestration Pattern

This is a collaborative pattern where an orchestrator decides the conversation flow. Unlike the handoff pattern where the moderator was an agent, the orchestrator in group chat pattern is not an agent.

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/bea5a1b3-3f87-4a38-8f6e-c50168e2236a.png align="center")

The agents are selected by the orchestrator to coordinate who executes next. The agents here have an advantage where they can review other agents responses in multiple iterations.

The orchestrator can use different strategies like round-robin, prompt-based, custom logic to select speakers.

Lets assume a use case where we define an agent that creates slogan for a football club and the second agent which is a reviewer, reviews the created content and provides feedback and suggests improvements.

**ContentCreatorAgent >>**

```csharp
   servicecollection.AddSingleton<AIAgent>(sp =>
   {
       Func<ChatClientAgentOptions> func = () =>
       {
           return new ChatClientAgentOptions()
           {
               ChatOptions = new ChatOptions
               {
                   Instructions = "You are a content creator.You create content for football teams.Be concise and please stick to the topic."
               },
               Name = "ContentCreator",
               Id = "1"
           };

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

   );
```

**ContentReviewerAgent >>**

```csharp
   servicecollection.AddSingleton<AIAgent>(sp =>
        {
            Func<ChatClientAgentOptions> func = () =>
            {
                return new ChatClientAgentOptions()
                {
                    ChatOptions = new ChatOptions
                    {
                        Instructions = "You are a content reviewer.You review content for football teams.Be concise and please stick to the topic."
                    },
                    Name = "ContentReviewer",
                    Id = "2"
                };
            };

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

 }

);
```

Once the agents are defined, we have to set up the workflow but before that ,we fetch the list of agents from the DI container.

```csharp
 var agents = serviceProvider.GetServices<AIAgent>();
 List<AIAgent> AgentList = new(agents);
```

We then define the workflow

```csharp
var workflowcontent = AgentWorkflowBuilder
.CreateGroupChatBuilderWith(agents =>
 new RoundRobinGroupChatManager(agents)
  {
   MaximumIterationCount = 5  // Maximum number of turns
  })
  .AddParticipants(AgentList[0], AgentList[1])
  .Build();
```

We then set up the workflow and also add the entire conversation to messages that is of type`List<ChatMessages>`required only incase we need to store the entire conversation.

```csharp
var messages = new List < ChatMessage > 
{
new(ChatRole.User, "Create a slogan for football club FC Barcelona")
 };

 string ? lastExecutorId = null;

await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflowcontent, messages);
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
await foreach(WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false)) 
{

   if (evt is AgentResponseUpdateEvent e) 
{
     if (e.ExecutorId != lastExecutorId) 
     {
        Console.WriteLine();
        lastExecutorId = e.ExecutorId;
        Console.WriteLine(e.ExecutorId);
        Console.WriteLine();
     }

     Console.Write(e.Update.Text);
     messages.Add(new(ChatRole.Assistant, e.Update.Text));
   } 
 }
```

**Execution >>**

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/0227cb4d-cd97-4187-9725-1375b6efb6d8.gif align="center")

### Wrapping It Up

Selection of workflow orchestration pattern in MAF is very crucial mechanism to make multi agent system to work effectively and efficiently. In this article I tried covering the most important workflow orchestration pattern provided in MAF.

Irrespective of the underlying framework used, the idea remains the same: clear, precise and consistent selection of workflow orchestration pattern enable agents to work together effectively.

Thanks for reading !!!
