concurrent · Fan-out / Fan-in Workflow
How to broadcast one input to several executors in parallel and join their answers with a barrier — the core workflow graph primitives, with no LLM in the way.
What this lesson demonstrates
This lesson teaches the graph primitives deliberately without an agent. A start executor broadcasts a question; a fan-out edge hands it to two domain experts (a physicist and a chemist) that run concurrently; a fan-in barrier edge waits for both to answer, then delivers their outputs together to an aggregation executor that joins them into the workflow’s result.
Because every executor is a plain string → string Go function, the whole thing runs in-process — no model, no credential, no network — so you can run it (and its test) with nothing configured.
The graph
Construction is factored into buildWorkflow(), and the topology is expressed in four builder calls:
return workflow.NewBuilder(start).
AddFanOutEdge(start, []workflow.ExecutorBinding{physics, chemistry}).
AddFanInBarrierEdge([]workflow.ExecutorBinding{physics, chemistry}, aggregate).
WithOutputFrom(aggregate).
Build()
The start executor broadcasts with ctx.SendMessage("", question) — the empty target ID means “send onto all my outgoing edges”, which the fan-out edge picks up.
What to notice
- The barrier is what makes it a join, not a race.
AddFanInBarrierEdgeholds delivery until every source executor has produced a message, so the aggregator always sees both answers. Drop the barrier and you’d get whichever finished first. - Stateful executor ⇒
BindNewExecutorFunc. The aggregator accumulates messages in a slice, so it’s created per-run viaBindNewExecutorFunc(a fresh instance each run) rather than shared.OnMessageDeliveryFinishedFuncis where it joins the collected strings and callsctx.YieldOutput(...)once the turn’s delivery is done. - Protocol declarations via
Extend+ConfigureProtocol. The start executor declares itSendsMessageType(string)and the aggregator declares itYieldsOutputType(string); the builder type-checks the graph before it runs. - Three graph pieces.
workflow.NewExecutor(id, fn)wraps a function as a node; the fan-out / fan-in edges wire the topology;WithOutputFrom(aggregate)names which executor’sYieldOutputis the workflow’s result.
How it maps to the Agent Framework Go SDK
AddFanOutEdge and AddFanInBarrierEdge are the SDK’s structural concurrency edges — the same primitives that back NewConcurrentWorkflowBuilder (the bilingual “workflow as an agent” lesson used exactly this fan-out/fan-in shape under the hood). Learning them at the raw level here is what lets you later mix agents and plain executors in one graph and reason about when a node actually fires.
Run it
go run ./tutorial/03-workflows/concurrent/concurrent
go test ./tutorial/03-workflows/concurrent/concurrent
Both lines of output may appear in either order — the experts run concurrently; the barrier only guarantees both are present before the aggregator fires. There’s no live model path here; the offline test actually runs the workflow and asserts both experts’ answers reach the aggregated output.
Next: concurrent · Map-Reduce Workflow
Key takeaways
- The lesson teaches the graph primitives with no agent at all — every executor is a plain
string → stringGo function, so the workflow and its test run in-process with no model, credential, or network. - The start executor broadcasts with
ctx.SendMessage("", question): the empty target ID means “send onto all my outgoing edges,” which the fan-out edge picks up. AddFanInBarrierEdgeis what makes the merge a join rather than a race — it holds delivery until every source has produced a message, so the aggregator always sees both answers instead of whichever finished first.- A stateful aggregator is created per run with
BindNewExecutorFunc(a fresh instance each time) and joins its collected messages inOnMessageDeliveryFinishedFunc, callingctx.YieldOutputonce delivery is done. - Executors declare their protocol via
Extend+ConfigureProtocol(SendsMessageType,YieldsOutputType) so the builder type-checks the graph before it runs; these fan-out/fan-in edges are the same primitives that backNewConcurrentWorkflowBuilder.
Further reading
- concurrent · Map-Reduce Workflow — the next lesson, built on these edges
- Workflow Mechanics — Microsoft Agent Framework Go
- Agents in Workflows — Microsoft Agent Framework Go
- Microsoft Agent Framework Go source