One agent with good tools gets you far. But some problems genuinely want a team of agents — a planner that breaks work down, specialists that each handle a slice, a reviewer that checks the output before it ships. The naive way to build that is to stuff the whole plan into one giant prompt and hope the model coordinates itself. It won’t, reliably. This article is about the alternative the Microsoft Agent Framework offers: graph-based workflows that make multi-agent coordination explicit code you can reason about, test, and observe.
Why not just prompt it?
“Prompt orchestration” — telling one model to act as several personas and manage the handoffs — fails in ways that are hard to debug:
- You can’t see why it took a path; the routing lives inside a black-box generation.
- A failure anywhere collapses the whole turn, with no clean place to retry.
- You can’t unit-test “does the reviewer reject bad output?” because there’s no reviewer, just a prompt.
- Token cost balloons as the single context carries every role’s instructions at once.
The moment coordination matters, you want it to look like software, not like a wish written in English.
That last point deserves a number, because it’s the one that surprises people. In a single-context, multi-persona prompt, every role’s instructions sit in the context for every generation, and every intermediate output accumulates on top. A five-role prompt doesn’t cost five times a one-role prompt — it costs more, because the context grows as the “conversation” between personas proceeds, and you’re paying for all of it on every turn. Splitting the roles into separate agents with separate contexts is often a cost reduction, not just an architecture improvement.
The graph model
The Agent Framework models a multi-agent system as a graph: nodes are executors (often an agent), and edges define how messages flow between them. Instead of hoping the model routes correctly, you declare the routing. That turns “the model decides everything” into “the model does the reasoning, the graph enforces the process.”
A common shape looks like this:
┌─────────┐ ┌──────────────┐
│ Planner │ ──▶ │ Researcher │ ─┐
└─────────┘ └──────────────┘ │
│ ┌──────────────┐ ▼
└────────▶ │ Writer │ ─▶ ┌──────────┐ ─▶ output
└──────────────┘ │ Reviewer │
└──────────┘
Each node is a focused agent with a narrow instruction set — which also keeps each prompt small and cheap, instead of one mega-prompt trying to be everything.
Building a workflow
You define your specialist agents, then wire them into a workflow with WorkflowBuilder. The builder takes the starting executor in its constructor and you add edges from there:
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
// Focused, single-responsibility agents
AIAgent planner = chatClient.AsAIAgent(
name: "Planner",
instructions: "Break the user's request into concrete research and writing tasks.");
AIAgent researcher = chatClient.AsAIAgent(
name: "Researcher",
instructions: "Gather accurate facts for the assigned task. Cite sources.",
tools: [/* web search / MCP tools */]);
AIAgent writer = chatClient.AsAIAgent(
name: "Writer",
instructions: "Turn the research into a clear, well-structured draft.");
AIAgent reviewer = chatClient.AsAIAgent(
name: "Reviewer",
instructions: "Check the draft for accuracy and clarity. Approve or return with specific fixes.");
// Wire them into an explicit graph
var workflow = new WorkflowBuilder(planner)
.AddEdge(planner, researcher)
.AddEdge(researcher, writer)
.AddEdge(writer, reviewer)
.Build();
Running it gives you a stream of events rather than a single return value, which is the point — you want to see each node complete, not just the final answer:
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(
workflow, new ChatMessage(ChatRole.User, "Brief me on why teams adopt service meshes."));
// Agents wrapped as executors buffer their input and only run when they
// receive a TurnToken. Forget this and the workflow does nothing at all.
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
if (evt is ExecutorCompletedEvent done)
Console.WriteLine($"{done.ExecutorId} finished");
if (evt is WorkflowOutputEvent output)
Console.WriteLine($"Result: {output.Data}");
}
That TurnToken line is the first thing that will waste an hour of your life. When an AIAgent is used as a workflow executor it caches incoming messages and doesn’t start generating until a turn token arrives. Without it, the workflow starts, no agent runs, no event fires, and nothing errors — you get a clean, silent no-op. There is also a non-streaming form, InProcessExecution.RunAsync, which collects events into NewEvents for you to iterate after completion.
Routing, not just sequencing
A straight line of agents is the least interesting thing a graph can do. The value shows up when routing is conditional.
AddEdge takes an optional condition, so an edge only fires when the message satisfies a predicate. That’s how you express “if the classifier says this is spam, send it to the spam handler and skip the rest of the pipeline entirely” — a real branch, evaluated in your code, on a typed message. When you have more than two branches, AddSwitch groups ordered cases with a default, which scales better than a pile of mutually exclusive conditions and, importantly, gives unexpected values somewhere to go instead of silently dropping them.
For parallel work there’s AddFanOutEdge to send one message to several executors at once and AddFanInBarrierEdge to collect their results back into an aggregator. WithOutputFrom marks which executors’ results count as workflow output, which matters as soon as you have branches — a spam-handling path and a send-email path can both be terminal, and both should be able to produce the final result.
Two structural details are worth knowing. Workflows can be nested: a workflow can be bound as an executor inside another workflow, so a “research” sub-graph becomes one node in a larger pipeline. And validation happens at Build(), not at runtime. The framework checks message-type compatibility between connected executors, that every executor is reachable from the start node, that executors are properly bound, and that you haven’t declared duplicate edges. An orphaned node you wired up wrong fails when you construct the workflow, which is a considerably better place to find out than in front of a customer.
The superstep model, and the performance trap in it
This is the part of the design most likely to surprise you, and it’s worth understanding before you build anything wide.
Execution follows a Bulk Synchronous Parallel model — a variant of Pregel. Work proceeds in discrete supersteps. Each superstep collects the messages emitted by the previous one, routes them along the edges, runs all triggered executors concurrently, and then waits at a synchronisation barrier for every one of them to finish before starting the next superstep.
The upside is real: execution is deterministic for a given input, there are no races between steps, and state can be checkpointed cleanly at superstep boundaries, which is what makes durable and resumable workflows possible.
The trap is that barrier. If you fan out to two branches — one a chain of three quick executors, the other a single slow one — the quick chain cannot advance past its first step until the slow executor completes. You wrote what looks like independent parallelism, and you got lock-step. Latency ends up governed by the slowest executor in each superstep, multiplied by the length of your longest chain.
The fix, when you actually need independent parallel paths, is to consolidate: collapse step1 → step2 → step3 into a single executor that does all three internally. Both branches then complete within one superstep and the barrier costs you nothing. This runs against the instinct to decompose everything into small nodes, and it’s the right call — the graph should express decisions, not every sequential step.
What the graph buys you
Making orchestration explicit gives you the things production systems need:
- Determinism where you want it. The process is fixed even though each agent’s output is generative. You always know research happens before writing.
- Testability. You can test each node in isolation (“given this draft, does the reviewer catch the error?”) and test the graph’s routing separately from the agents’ reasoning — the routing tests don’t need a model at all, which makes them fast and free.
- Observability. Each node boundary emits an event and a trace span, so you can see exactly which agent did what, how long it took, and where a run went wrong. The
invoke_agentspans nest under the workflow naturally; our OpenTelemetry guide covers the wiring. - Targeted retries and fallbacks. A flaky research step can retry without re-running planning and writing.
Budget the cost before you build
The arithmetic is unforgiving and easy to skip. Every node is at least one model call, and a sequential graph’s latency is the sum of its nodes, not the max. A four-agent pipeline where each agent takes three seconds is a twelve-second response before you’ve added a single tool call — and the researcher will add several.
Review loops multiply that. A writer-reviewer cycle that runs up to three times is potentially six extra model calls, and the worst case is the common case for exactly the inputs that are hardest, which is to say the ones users complain about. Always cap the loop explicitly, and decide what happens when the cap is hit: ship the last draft with a flag, escalate to a human, or fail. An uncapped review loop against a fussy reviewer prompt is the most expensive bug in this entire design space, because it fails by spending money rather than by throwing.
Two things reduce the bill without changing the architecture. Use a smaller, cheaper model for the mechanical nodes — routing, classification, formatting — and reserve the expensive one for the nodes that need real reasoning. This is one of the strongest arguments for a graph in the first place: a single mega-prompt has to run entirely on your best model, while a graph lets you spend differently per node. And run genuinely independent work in a fan-out so you pay for it in parallel latency rather than in series, keeping the superstep barrier in mind when you decide what “independent” means.
When you don’t need this
Multi-agent orchestration is a real cost — more moving parts, more model calls, more latency, and a new class of failure where the process is right and the handoff is wrong. If a single tool-using agent solves your problem, use that.
Reach for a workflow when the task has genuinely distinct phases, when you need a review or approval gate, or when different steps need different tools, different models, or different permissions. That last one is underrated: a graph is a natural place to enforce that the agent reading untrusted web content is not the same agent holding write credentials, which is a structural defence rather than a prompt-shaped one. See prompt injection defence for why that boundary matters.
A good rule: start with one agent, and only split into a graph when you can name the specific step that a single agent keeps getting wrong. “It would be cleaner” is not that reason. “The agent writes the draft before it has finished researching, about one time in five” is.
Note: the workflows layer ships in
Microsoft.Agents.AI.Workflows, which at the time of writing is still installed as a prerelease package, and signatures in this area move between releases. Verify against the current Agent Framework workflow docs before copying wholesale. The concepts — executors, edges, conditional routing, fan-out and fan-in, the superstep barrier — are the durable part.
Takeaway
Graph workflows turn multi-agent coordination from a fragile prompt into inspectable software. You keep the model’s reasoning where it’s valuable — inside each focused agent — while the graph enforces a process you can test, trace, and trust, with validation at build time rather than at 3 a.m.
Three things to remember when you build one. Send the turn token, or nothing happens. Understand the superstep barrier before you design a wide graph, and consolidate sequential steps rather than decomposing them. And cap every review loop, because the failure mode of an uncapped one is a bill rather than an exception.
Have a correction or a topic you want covered? Email mani.bc72@gmail.com.
