The Sequential Pipeline Pattern, Implemented via the Reactive Reducer
A little background
I’m working through the 13 multi-agent orchestration patterns that keep showing up in real systems, and implementing each one as a concrete Aegis FSM on the reactive reducer design. If you haven’t read the last two articles, the 30-second version: an Aegis FSM is a reactive reducer - context is the single source of truth, agents advance the machine by writing context (never by requesting a transition), and a microstep loop fires the first eligible, guard-passing edge until the machine parks. There’s no requestTransition tool and no sign-off call. The machine moves itself; actors only ever contribute values.
This is pattern #1, and the one everyone starts with: the sequential pipeline.
The pattern
Sequential Pipeline. Each agent owns a distinct stage of the workflow and passes its output forward as the input to the next stage. Control moves in a predictable, usually linear direction, making the pattern easy to reason about and observe. Agents can have different roles, tools, models, or context windows, but each is expected to complete its responsibility before the workflow advances.
Good for:
- Work with a natural ordering
- Research → design → implementation → review
- ETL-like processing
- Document generation
- Multi-stage transformation pipelines
Common failure points:
- Early mistakes compound downstream
- Context degrades during handoffs
- Slowest agent determines latency
- Recovery may require replaying earlier stages
The pattern itself is boring. That’s the point - it’s the shape of most real work. The interesting question is what the implementation does to those four failure points, because in a naive agent pipeline, the handoffs are where things rot.
The handoff problem
In most multi-agent frameworks, a handoff is a message: agent A writes a summary, agent B reads it. That summary is a lossy compression of A’s actual work, and the only thing standing between you and silent context degradation is how good A is at summarizing. Nobody verifies that B got what A meant.
The reactive reducer changes the substrate. There’s no handoff message at all. Each stage’s output is a field in a shared, typed context store, and the next stage is a state whose objective reads from that store. The handoff is a context write, and the machine is the only thing that moves.
Two things to hold onto before we look at the machine:
- A stage is a state with a
requires. The owning agent’s job is to produce the context field that requirement names. Until that field is set, the machine parks - it doesn’t time out, spin, or guess. - The next stage’s objective is templated on the previous stage’s output.
{{ context.<field> }}interpolates the full value into the next agent’s instruction, so the handoff carries the artifact itself, not a summary of it.
The machine
Here’s a four-stage pipeline - research → design → implement → review - with a rework edge out of review. It’s the same shape as our real dev-loop FSM, trimmed down to the pattern.
fsm:
name: doc-pipeline
version: 1
initialState: research
context:
research: { type: string }
design: { type: string }
implemented: { type: string } # what was built - NOT a bool; see the note below
review: { type: enum, values: [approved, changes] }
review_notes: { type: string }
agents:
researcher: { canTransition: [research] }
designer: { canTransition: [design] }
engineer: { canTransition: [implement, rework] } # every state it acts in
reviewer: { canTransition: [review] }
states:
research:
description: "Research the topic, then stage the findings: updateContext(field=research, value=<findings>)."
requires:
- { name: research, from: { agent: researcher } }
transitions:
- { toState: design }
design:
description: >-
Design the approach from the research: "{{ context.research }}".
Stage it: updateContext(field=design, value=<design>).
requires:
- { name: design, from: { agent: designer } }
transitions:
- { toState: implement }
implement:
description: "Implement per the design. When it's done and tested, stage what you built: updateContext(field=implemented, value=<what changed>)."
requires:
- { name: implemented, from: { agent: engineer } }
transitions:
- { toState: review }
review:
description: "Adversarially review the implementation. approved → done; changes → rework with notes."
requires:
- { name: review, from: { agent: reviewer } }
- { name: review_notes, from: { agent: reviewer }, required: false, label: "Notes (if changes)" }
transitions:
- { toState: done, guards: [ { backend: cel, expression: "context.review == 'approved'" } ] }
- toState: rework
guards: [ { backend: cel, expression: "context.review == 'changes'" } ]
onTransition:
- { assign: { implemented: "", review: "" } } # consume the trigger AND the artifact, KEEP the notes
rework:
description: >-
The reviewer sent it back: "{{ context.review_notes }}".
Fix it, then re-stage what changed: updateContext(field=implemented, value=<what changed>).
requires:
- { name: implemented, from: { agent: engineer } }
transitions:
- toState: review
onTransition:
- { assign: { review_notes: "" } } # clear so the next review re-collects
done:
description: "All four stages green."
terminal: true
policy:
maxExchangesPerStage: 30
maxExchangesPerAgentPerStage: 15
checkpointEvery: 5
Sequential Pipeline work flow:
Walk one handoff and you’ll see the whole pattern in miniature. The machine rests in design, parked - design isn’t set yet, so the state’s single edge isn’t eligible. The runtime summons the designer, and the designer’s injected objective already contains the researcher’s full findings, interpolated by {{ context.research }}. The designer does the work, writes updateContext(field=design, value=...), and that write is the entire handoff. The reducer re-runs: design is present, the edge is eligible, it has no guards, so it fires. Nobody asked the machine to move.
Notice what the rework edge does, because it’s the load-bearing detail. When the reviewer writes review = changes, the review → rework edge fires and its onTransition consumes the trigger: review is cleared, so the edge can’t immediately re-fire when the machine re-runs to stable, and implemented is blanked, so rework waits for a fresh implementation instead of being instantly satisfied by the one that just got rejected. But review_notes is deliberately kept - the rework state’s objective needs it, and {{ context.review_notes }} hands the actual feedback to the engineer. Then the rework → review edge clears review_notes so the next review collects fresh notes instead of reusing stale ones. That consume-the-trigger / keep-the-feedback discipline is what makes the loop reentrant instead of oscillating.
One thing that bites here, and is worth stating once for the whole series: blank is absent, but false is present. A requirement is satisfied when its field is present, and the emptiness test is per-type - an empty string, an empty list, an empty map all read as absent, while false is a perfectly good boolean value. So implemented cannot be a bool that the rework edge resets to false: the reset would leave it present, rework would be satisfied the moment it was entered, and the machine would loop straight back to review without the engineer ever being asked. It’s a silent failure - no error, just a rework state nobody works in. Make a re-armable field a string (or an enum) and blank it, or make the presence of a different field the trigger. The same trap catches a settled: bool in a watch loop, and it’s the one shape in the DSL where the intuitive modelling choice is the wrong one.
What the reducer does to the failure points
There are four common failure points. Here’s what each one looks like when the pipeline is a reactive reducer instead of a chain of agents messaging each other.
Early mistakes compound downstream. The pipeline doesn’t hide this - it makes it structural. Every stage’s output is a named context field, so a bad value is traceable to the exact stage that wrote it. And the review back-edge is the containment mechanism: a changes verdict routes work back with the notes preserved in context, so the rework agent gets the actual feedback, not a paraphrase of a paraphrase. The compounding is real, but it’s now bounded by an explicit edge you drew, instead of emerging from whatever the agents happened to say to each other.
Context degrades during handoffs. In a message-passing pipeline, the handoff is whatever the previous agent chose to summarize. Here the handoff is the artifact itself. The next state’s objective interpolates the full value - templating is claim-safe and uncapped, unlike the truncated CONTEXT block - and every turn the runtime re-injects the whole set context store, so the acting agent sees what the earlier stages actually produced rather than a lossy relay. If a stage’s output is big, declare the field blob and it gets claim-checked: the reference sits in context, the content is staged, and the machine still advances on the reference.
Slowest agent determines latency. True, and the FSM makes it visible instead of buried. Each stage is a park: the machine sits at research until research is set, at design until design is set. No busy-wait, no timeout guessing - the parked record is journaled, so a process restart resumes the same stage with the same context. You can see which stage is the bottleneck because the machine is literally at that stage. (And if that stage can be decomposed, that’s next article’s problem.)
Recovery may require replaying earlier stages. A parked machine is a replay point. The (state, context) pair is the complete run - journaled to disk - so recovery is “re-summon the agent at the current state,” not “re-derive what state we’re in.” And if one stage’s output is bad, you don’t replay the whole pipeline: you declare the back-edge (as review does), consume the trigger with an assign, and the machine re-parks at exactly the stage that needs to redo its work.
One more property falls out for free: legality is structural. The pipeline can only move along declared edges. An agent can’t skip a stage, a stage can’t be entered out of order, and an edge that isn’t declared simply can’t happen. The “predictable, linear direction” from the pattern description isn’t a property of the agents’ discipline anymore - it’s a property of the graph.
The whole thing, one idea
Step back and look at what actually moved the machine, start to finish:
| Moment | Who | What happened in context |
|---|---|---|
| research | researcher | research set |
| design | designer | design set (objective read research in full) |
| implement | engineer | implemented = true |
| review | reviewer | review (+ optional review_notes) set |
| rework | engineer | implemented re-staged (notes read, then cleared) |
| done | - | terminal |
No requestTransition. No handoff messages. No agent deciding it may move. Every single advance was a context write - by a delegated agent - that a pure guard reacted to. The handoff was a field in a store. The wait was a park. The rework loop was a declared edge plus the discipline of consuming the trigger and keeping the feedback.
That’s the reactive reducer doing its job on the simplest pattern in the book: the workflow is a structural system the agents write into, and the machine is the only thing that moves.
Up next: 2. Parallel Fan-Out / Fan-In
Series:
- Intro
- 1. Sequential Pipeline
- 2. Parallel Fan-Out / Fan-In
- 3. Supervisor / Router
- 4. Hierarchical Manager / Worker
- 5. Peer Handoff / Swarm
- 6. Debate / Consensus
- 7. Blackboard / Shared-State
- 8. Planner / Executor
- 9. Generator / Critic / Refiner
- 10. Auction / Contract-Net
- 11. Event-Driven / Publish-Subscribe
- 12. Quorum / Voting
- 13. Dynamic Team Formation