Four orchestration patterns in MAF — and when to pick each
Sequential, Concurrent, Handoff, and Custom WorkflowBuilder. Four shapes the Microsoft Agent Framework ships out of the box. Each one is the right answer to a different question.
The Microsoft Agent Framework ships four orchestration topologies as first-class builders. The names are easy. The harder thing is knowing which one your problem actually wants. This post is the side-by-side I wish I had when I started. (For the full chapter-to-code map, see the MAF reference architecture overview.)
from agent_framework.orchestrations import (
SequentialBuilder,
ConcurrentBuilder,
HandoffBuilder,
)
from agent_framework import WorkflowBuilder
Four shapes. Same Agent type going in. Four very different runtime sequences coming out. (For the Go errgroup take on the concurrent pattern, see parallel agent dispatch with errgroup.)
1. Sequential — refinement pipeline
Use when: each step's output is the input the next step needs. Refining a draft, summarising a researched brief, classifying then enriching.
from agent_framework.orchestrations import SequentialBuilder
workflow = SequentialBuilder(
participants=[planner, researcher, writer, critic],
output_from="all",
).build()
result = await workflow.run(prompt)
sequenceDiagram
autonumber
User->>Sequential: prompt
Sequential->>Planner: full conversation
Planner-->>Sequential: + planner msg
Sequential->>Researcher: full conversation
Researcher-->>Sequential: + researcher msg
Sequential->>Writer: full conversation
Writer-->>Sequential: + writer msg
Sequential->>Critic: full conversation
Critic-->>Sequential: + critic msg
Sequential-->>User: full conversation
Each agent sees the entire growing conversation. Live run against granite4.1:3b on "Define eventual consistency in one short sentence":
01 [user] Define eventual consistency in one short sentence.
02 [planner] Eventual consistency refers to a data model updated periodically...
03 [researcher] **Sources:** [1] Definition from AI literature on Eventual Consistency
04 [writer] Eventual consistency is a consistency model that ensures all nodes...
05 [critic] STRENGTHS: ... ISSUES: ... LGTM — no revisions needed.
Four agents, four LLM calls, sequential.
2. Concurrent — fan-out / fan-in
Use when: the same prompt benefits from N independent perspectives. Brainstorming, multi-judge review, risk analysis across roles.
from agent_framework.orchestrations import ConcurrentBuilder
workflow = ConcurrentBuilder(
participants=[researcher, marketer, legal]
).build()
sequenceDiagram
autonumber
User->>Concurrent: prompt
par fan-out
Concurrent->>Researcher: prompt
and
Concurrent->>Marketer: prompt
and
Concurrent->>Legal: prompt
end
par fan-in
Researcher-->>Concurrent: result
Marketer-->>Concurrent: result
Legal-->>Concurrent: result
end
Concurrent-->>User: merged
The gotcha I hit: ConcurrentBuilder yields either AgentResponse (per participant) or a pre-merged list[Message] depending on the aggregator. Don't assume one shape:
merged: list[Message] = []
for output in events.get_outputs() or []:
if isinstance(output, AgentResponse):
merged.extend(output.messages)
elif isinstance(output, Message):
merged.append(output)
elif isinstance(output, list):
merged.extend(output)
That defensive code in workflows/concurrent.py only got written after a TypeError: 'AgentResponse' object is not iterable crashed a live run.
3. Handoff — triage + specialist mesh + HITL
Use when: the user's intent decides which specialist responds, and the conversation may continue across turns. Customer support, on-call triage, a one-to-many router.
from agent_framework.orchestrations import HandoffBuilder
workflow = (
HandoffBuilder(
name="customer_support_handoff",
participants=[triage, refund_agent, order_agent, return_agent],
termination_condition=lambda conv: (
len(conv) > 0 and "welcome" in conv[-1].text.lower()
),
)
.with_start_agent(triage)
.build()
)
sequenceDiagram
autonumber
User->>Handoff: prompt
Handoff->>Triage: route
Triage-->>Handoff: handoff: refund_agent
Handoff->>RefundAgent: subtask
RefundAgent-->>Handoff: result
Handoff->>User: request_info (HITL)
User-->>Handoff: response
Handoff->>RefundAgent: continued
RefundAgent-->>Handoff: result
alt termination
Handoff-->>User: closing
else
Handoff->>User: request_info again
end
Three production gotchas worth knowing:
- Every participant needs
require_per_service_call_history_persistence=True. Not just the triage. The error is helpfully explicit if you forget:ValueError: Handoff workflows require all participant agents to have 'require_per_service_call_history_persistence=True'. The following agents are missing this setting: researcher, writer, critic. - The termination condition is your job. Without it the workflow keeps prompting forever. I use
"welcome" in conversation[-1].text.lower()to recognise the assistant's farewell. - Handoff is currently blocked on Ollama by an upstream bug in
agent-framework-ollama 1.0.0b260521that forwardsallow_multiple_tool_calls=to a kwarg theollamapackage never accepted. Handoff works fine on OpenAI and Foundry.
4. Custom — WorkflowBuilder + Executor
Use when: none of the three above fit. Conditional branching, loops, sub-workflows, anything with non-linear edges.
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, executor, handler
from typing_extensions import Never
class Classify(Executor):
@handler
async def classify(self, text: str, ctx: WorkflowContext[str]) -> None:
marker = "SHORT::" if len(text) <= 80 else "LONG::"
await ctx.send_message(marker + text)
@executor(id="finalize")
async def finalize(text: str, ctx: WorkflowContext[Never, str]) -> None:
if text:
await ctx.yield_output(text)
workflow = (
WorkflowBuilder(start_executor=normalize, output_from=[finalize])
.add_edge(normalize, classify)
.add_edge(classify, short_path)
.add_edge(classify, long_path)
.add_edge(short_path, finalize)
.add_edge(long_path, finalize)
.build()
)
flowchart TB
Start([prompt]) --> Normalize
Normalize --> Classify{Classify by length}
Classify -->|≤ 80| ShortPath
Classify -->|> 80| LongPath
ShortPath --> Finalize
LongPath --> Finalize
Finalize --> Out([yield_output])
Two API details that took me a beat:
output_fromis a kwarg onWorkflowBuilder.__init__, not on.build(). The error if you put it on.build()isTypeError: WorkflowBuilder.build() got an unexpected keyword argument 'output_from'. Forward-compatible code passes it on the constructor.- A
WorkflowContext[T_Out]sends messages to downstream nodes withctx.send_message(T_Out). AWorkflowContext[T_Out, T_W_Out]also yields workflow outputs withctx.yield_output(T_W_Out). UseNeverfor either type-parameter when you mean "this node doesn't do that."
A cheat-sheet
| Pattern | Latency | Cost | Best for |
|---|---|---|---|
| Sequential | n × LLM, serial | n × LLM | Refinement pipelines, drafts |
| Concurrent | 1 × LLM (parallel) | n × LLM | Independent perspectives |
| Handoff | varies (HITL) | varies | Routing + multi-turn user interaction |
| Custom graph | depends on graph | depends | Loops, conditional fan-out, sub-workflows |
The default I reach for first is sequential, because most pipelines need refinement, not parallelism. Concurrent is what I reach for second when I want explicit diversity (three judges, three risk perspectives). Handoff is the right shape for support-desk flows and almost nothing else. The custom builder is what you use when one of the previous three "almost works but" — you don't fight the high-level builder, you drop a level.
Running every shape against Ollama
The repo ships a Makefile target per workflow so you can poke at all four:
make sequential PROMPT="In one sentence: what is eventual consistency?"
make concurrent PROMPT="Three perspectives on feature flags."
make custom PROMPT="hello there" # no LLM needed
# make handoff is OpenAI/Foundry only until the Ollama bug is patched
The next post in this series is on the Agent Registry — why MAF doesn't ship one and what to build instead. For the two additional patterns MAF documents (Group Chat and Magentic), see five MAF orchestration shapes.