Agents Workflows and Orchestration

In my last article (The .NET AI Stack, May/June 2026 https://www.codemag.com/Article/266071/The-.NET-AI-Stack), I introduced Microsoft Agent Framework (AF) as the “spiritual successor” of Semantic Kernel and AutoGen. At that time, AF was a release candidate. As I write this, a few months later, I'm using a very mature v1.15. In that article I talked about Microsoft.Extensions.AI, Semantic Kernel, AutoGen and other parts of the Microsoft AI stack and discussed the basics of Agent Framework for creating agents. In this article, I'll build on those basics and introduce some more advanced concepts including how to orchestrate multiple agents into a workflow.

Single Agent Recap

As a reminder, I left off with creating an AIAgent using any number of providers. In the example below, I based the agent on Azure OpenAI, gave it some instructions, and provided it with a tool I created in C#. Then asked it a question.

AIAgent agent = new AzureOpenAIClient(
    new Uri(_azureOpenAiEndpoint),
    new ApiKeyCredential(_azureOpenAiApiKey))
    .GetChatClient("gpt-4o")
    .AsAIAgent(
        instructions: "You're a friendly assistant. Keep answers brief.",
        tools: [AIFunctionFactory.Create(WeatherTool.GetWeather)],
        name: "MyAgentWithTools");

Console.WriteLine(await agent.RunAsync(
    "I'm in Taos, NM. Should I take an umbrella today?"));

The sample code for that article included examples for creating agents with Ollama models running locally, using sessions to preserve chat history for a two-way conversation, forcing structured output from the model, and creating long running and durable calls that run in the background. While not an exhaustive list of AIAgent features, these are some of the major components available in a single agent.

But single agents are so last month! Really complex workflows may require multiple agents that compound the power of individual agents. With agent workflows and orchestrations, each agent represents a specialist which contributes to an overall solution. The trick is to create a workflow that represents the underlying process, and that part is up to you. It sounds easy at first, but there is no one-size-fits-all workflow for every process and models are not yet advanced enough to figure all that out for you. Instead of forcing you to manually build each workflow, Agent Framework packages some of the most common workflows as something they call Orchestrations. Currently, there are 4 static Orchestrations: Sequential, Concurrent, Handoff and Group Chat. A fifth Orchestration, Magentic, is a work in progress.

Static Orchestrations

Sequential, Concurrent, Handoff and Group Chat are all created with the static AgentWorkFlowBuilder class. They're called static orchestrations or static workflows because the handoffs paths between agents are pre-determined.

Sequential Workflow

The most basic Orchestration represents a sequential workflow with a fixed set of linear, sequential steps. For example, if I wanted to use the above sample to retrieve my local weather, but . One route I could take would be to add instructions to a single agent to reply in Spanish and rely on the agent to do both tasks. In basic scenarios, if the model is powerful enough, this will work. But stepping back and looking at the bigger picture, it would be more powerful to build two agents, one to retrieve local weather and another to translate text to Spanish. I could then tweak each agent individually to get better at the one task it's designed to do (like the single responsibility principle in SOLID development), and I can compose new workflows using each agent individually. For instance, I could convert things besides weather forecasts into Spanish. As I build up a set of specialized agents, I can assemble them into various teams of agents to accomplish a variety of tasks.

AIAgent agent1 = new AzureOpenAIClient(
    new Uri(_azureOpenAiEndpoint),
    new ApiKeyCredential(_azureOpenAiApiKey))
    .GetChatClient("gpt-4o")
    .AsAIAgent(
        instructions: "You're a friendly assistant. Keep answers brief.",
        tools: [AIFunctionFactory.Create(WeatherTool.GetWeather)],
        name: "MyAgentWithTools");

AIAgent agent2 = new OllamaSharp.OllamaApiClient(
    "http://localhost:11434", "llama3.1:8b")
    .AsAIAgent(
        instructions:
            "You are a translation assistant who only
             responds in Spanish.",
        name: "SpanishTranslator");

// Next, set up the sequential workflow with the agents.
var workflow = AgentWorkflowBuilder.BuildSequential(agent1, agent2);

// Run the workflow
var messages = new List<ChatMessage>
{
    new(ChatRole.User, "I'm in Taos, NM."),
    new(ChatRole.User, "Should I take an umberlla?")
};

await using StreamingRun run = await InProcessExecution.RunStreamingAsync(
    workflow,
    messages);

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

I've started with the same weather agent above and created a new agent (agent2) based on Ollama running locally on my machine serving a llama3.1:8b model. This might not be the best model to use for this particular task, but it runs locally and costs nothing. The real point is that I can compose agents based on any provider, any model, any tool set into a workflow. I simply use the pre-built orchestration by calling AgentWorkflowBuilder.BuildSequential and passing a list (or any IEnumerable) of agents. The agents will be called in the order they're listed with each agent handing off its output as input to the next agent. Notice that the workflow provides a session for conversation history, so I can have multi-turn conversations. That session is also used to facilitate the handoff between agents.

Most of the code involves the creation of agent1 and agent2. The creation of the Orchestration (sequential workflow) happens in one line. Kicking off the Orchestration is two lines, one to create the streaming workflow and the other to start the workflow.

//Show data from events as they stream in from the 
// workflow execution.
string? lastExecutorId = null;
List<ChatMessage> result = [];

await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
    if (evt is AgentResponseUpdateEvent e)
    {
        if (e.ExecutorId != lastExecutorId)
        {
            lastExecutorId = e.ExecutorId;
            Console.WriteLine();
            Console.Write($"{e.ExecutorId}: ");
        }

        Console.Write(e.Update.Text);
    }
    else if (evt is WorkflowOutputEvent outputEvt)
    {
        result = outputEvt.As<List<ChatMessage>>()!;
    }
}

Orchestrations and workflows emit a collection of events you can hook into including: WorklowStartedEvent, WorkflowOutputEvent, WorkflowErrorEvent, WorkflowWarningEvent, ExecutorInvokedEvent, ExecutorCompletedEvent, ExecutorFailedEvent, AgentResponseEvent, AgentResponseUpdateEvent, SuperStepStartedEvent, SuperStepCompletedEvent, and RequestInfoEvent. Plus, you can create your own custom events and emit them from your own executors (an agent is one type of executor that can exist in a workflow). In this example, I'm only looking for AgentResponseUpdateEvent (to watch as agents generate output for the next step) and WorkflowOutputEvent (to copy the output when the workflow is finished). Since we copied the output into a List<ChatMessage> named result, we can display the results like this:

foreach (var message in result)
{
    Console.WriteLine($"{message.Role}: {message.Text}");
}

Concurrent Workflow

The Concurrent workflow is similar to the Sequential workflow except that instead of flowing in a linear fashion, multiple agents run in parallel. This is useful when there are several steps to complete and the steps don't depend on one another or on the order they're run in. For example, if after I receive a question for a user and I have to both detect the language it's written in and whether it contains information I expect (such as the location I want to retrieve weather for), I can create an agent for each task and run them in parallel to speed up processing.

Aside from different instruction for agent2 to detect language instead of converting to Spanish, the code is the same except that I call BuildConcurrent instead of BuildSequential.

var workflow = AgentWorkflowBuilder.BuildConcurrent(agent1, agent2);

Handoff Workflow

The Handoff workflow is a little more complex in that you create an agent whose responsibility is to control the workflow and hand off tasks to other agents as necessary.

//Create the handoff agent which will manage the
//handoff between the agents.
//This is a special type of agent that has the ability
//to hand off messages to other agents in the workflow,
//and decide when to do so based on specified rules
AIAgent handoffAgent = new AzureOpenAIClient(
    new Uri(_azureOpenAiEndpoint),
    new ApiKeyCredential(_azureOpenAiApiKey))
    .GetChatClient("gpt-4o")
    .AsAIAgent(
        instructions: "The user will ask you a   
    question. If it's about the weather, handoff to 
    MyAgentWithTools. If it's about translation or in 
    Spanish, handoff to SpanishTranslator. ALWAYS 
    handoff to another agent, don't answer the user's  
    question directly.",
        name: "HandoffAgent");

var workflow = AgentWorkflowBuilder.CreateHandoffBuilderWith(
        handoffAgent)
    .WithHandoffs(handoffAgent, [agent1, agent2])
    .WithHandoffs([agent1, agent2], handoffAgent)
    .Build();

As the developer, you statically determine the rules for when the handoff agent hands off to another agent. The LLM doesn't reason about which agent to call next, it follows your rules. The sample code shows that the specialty agents can hand the process back to the handoff agent.

Group Chat Workflow

Group Chat workflow is similar to Handoff workflow, but instead of having a dedicated agent in charge of controlling the handoffs, the handoffs are based on a strategy. The only strategy that currently comes out of the box is the RoundRobinGroupChatManager which calls agents sequentially in the order received, but unlike the Sequential workflow, the sequence can be looped through multiple times. Because agents could potentially keep talking amongst themselves for a very long time, delaying responses, eating up tokens and running up costs, this class has a MaximumIterationCount property which caps how many times each individual agent can be invoked. This pattern can work well for things like self-refinement where agents in the group judge the output of previous agents (e.g. "Is this statement correct?", or "Is this valid T-SQL?"). In this scenario, multiple runs of the workflow often improve the final output. But the real power of the Group Chat Workflow is that you can implement your own handoff strategy by subclassing the abstract GroupChatManager class.

Dynamic Workflows

Magentic Workflow

Magentic workflow is a new, improved version of the Magentic-One workflow pattern developed by the AutoGen team. It's an advanced pattern that dynamically selects agents, decomposes tasks to create plan for solving complex problems, monitor its own progress and issues, can include human-in-the-loop interactions, and more. It's basically the Cadillac of orchestrations. It differs from the static Handoff workflow where handoffs are determined by rules, not by reasoning, and no planning or monitoring takes place. Unfortunately, as of this writing, it's not available in C# yet (though it is available in ython).

Custom Workflows

Agents, Executors, Edges, and Contracts

Orchestrations are just pre-packaged workflows, but you can also create your own workflows from scratch. Workflows are built from agents, executors, edges, and contracts. Executors are coded classes that look like agents to Agent Framework. Like agents, executors handle strongly typed messages. Edges define the handoffs between agents, and other executors, a.k.a. the workflow. And contracts are strongly typed messages that executors and agents ‘handle'.

internal sealed partial class OutputExecutor() : Executor("OutputExecutor")
{
    protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)
    {
        return protocolBuilder
            .SendsMessage<List<ChatMessage>>()
            .SendsMessage<TurnToken>()
            .ConfigureRoutes(routes =>
            {
                routes.AddHandler<List<ChatMessage>>(HandleAsync);
            });
    }

    [MessageHandler]
    private async ValueTask HandleAsync(List<ChatMessage> messages, IWorkflowContext context, CancellationToken cancellationToken = default)
    {
        //Send the chat message to the next agent executor
        await context.SendMessageAsync(messages, cancellationToken: cancellationToken);

        //Send a turn token to signal the agent to process
        //the accumulated messages
        await context.SendMessageAsync(new TurnToken(emitEvents: true), cancellationToken: cancellationToken);
    }
}

This example contains an extremely simple custom executor that does nothing but pass a message through. In a world situation, you could add code to inspect, process, log, etc. The OutputExecutor code shows that the contract (strongly typed message) being passed around is a simple List<ChatMessage>, which is pretty standard input and output for agents. The OutputExecutor class inherits from the Executor base class and provides an implementation of the ConfigureProtocol method which defines that the executor will output a List<ChatMessage>, and it can also output TurnTokens which is a signal that a handoff should take place. Next, it sets up a route with a new handler. The handler definition is a class decorated with a [MessageHandler] attribute. It handles calls made with a List<ChatMessge>. Again, this custom executor is for demonstration purposes, and all it does is pass the List<ChatMessage> through, then it calls SendMessageAsync again with a TurnToken to let AF know that it needs to process the next step. If you need to handle additional contract types as input, create additional handler methods that accept the contract type being passed.

Now that we have a custom executor defined, the next step is to define the workflow. Edges describe the workflow. The most basic type of edge is the direct edge. In this example, the workflow is sequential: agent1 customExecutor agent2. Your workflow may have zero or more agents and zero or more custom executors. There are several types of edges in addition to direct, including conditional edges, switch-case edges, multi-selection (fan out 1-many) edges, and fan-in (many-1) edges.

AIAgent agent1 = new AzureOpenAIClient(
    new Uri(_azureOpenAiEndpoint),
    new ApiKeyCredential(_azureOpenAiApiKey))
    .GetChatClient(_chatDeployment)
    .AsAIAgent(
        instructions: "You are a friendly assistant. Keep your answers brief.",
        tools: [AIFunctionFactory.Create(WeatherTool.GetWeather)],
        name: "MyAgentWithToolsAndMemory");

//custom logic component
var customExecutor = new OutputExecutor();

AIAgent agent2 = new OllamaSharp.OllamaApiClient("http://localhost:11434", "llama3.1:8b")
    .AsAIAgent(
        instructions: $"You are a translation assistant who only responds in Spanish. Be brief",
        name: "SpanishTranslator");

//Simple, sequential workflow using Direct edges
var workflow = new WorkflowBuilder(agent1)
    .AddEdge(agent1, customExecutor)
    .AddEdge(customExecutor, agent2)
    .WithOutputFrom(customExecutor)
    .Build();

You can also add features to your workflow such as human in the loop, state management, checkpoint and resume, and observability. Agent Framework provides the building blocks to build any workflow with any features. And since entire workflows (not just agents) can be treated as a step in another workflow, you can compose larger workflows from smaller ones.

Summary

While agents are extremely powerful, at some point, you may need to build a complex system that a single agent can't handle. In these cases, you may need to create workflows that route steps in complex tasks from one agent to another. You may need to model the workflow among agents to represent a specific process. Agent Framework provides workflow building blocks, consisting of executors, edges and contracts to allow you do just that. It even includes some pre-packaged workflows called orchestrations, so you don't have to build common workflows from scratch.