P26-05-30">

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.

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.

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:

  1. 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.
  2. 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.
  3. Handoff is currently blocked on Ollama by an upstream bug in agent-framework-ollama 1.0.0b260521 that forwards allow_multiple_tool_calls= to a kwarg the ollama package 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_from is a kwarg on WorkflowBuilder.__init__, not on .build(). The error if you put it on .build() is TypeError: 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 with ctx.send_message(T_Out). A WorkflowContext[T_Out, T_W_Out] also yields workflow outputs with ctx.yield_output(T_W_Out). Use Never for 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.