<Sequence>, <Parallel>, <Branch>, <Loop> — these are the only control-flow components you need to wire together any workflow. They compose like building blocks: nest them, combine them, and the execution graph writes itself.
Let’s build up from the simplest case.
Sequential Execution: <Sequence>
You have three tasks. Each one needs the previous one’s result. This is the most common pattern in programming, and <Sequence> does exactly what you’d expect: run children top to bottom, one at a time.
fetch runs first. Only after it completes does transform start. store runs last.
So if <Workflow> already sequences things, why does <Sequence> exist at all? Because you’ll want to put ordered steps inside the other primitives. You’ll see this in a moment.
Parallel Execution: <Parallel>
Now suppose you’re running a CI pipeline. Linting, type-checking, and tests don’t depend on each other. Why run them one at a time?
Limiting Concurrency
What if you’re calling an API with a rate limit of two concurrent requests? You don’t want four agent calls hammering it at once. UsemaxConcurrency to cap it:
Combining Parallel and Sequential
Here’s where composition gets interesting. Remember the question about why<Sequence> exists? This is the answer:
deploy-web waits for build-web, but build-api does not wait for build-web.
Two pipelines. Running side by side. Each internally ordered. That’s a CI matrix in six lines of JSX.
Conditional Logic: <Branch>
Tests passed? Deploy. Tests failed? Notify the team. You’ve written this if/else a thousand times. <Branch> makes it declarative:
Branching into Complex Sub-graphs
Each branch can contain any workflow element — not just a single task. A critical bug might need a hotfix and an emergency deploy. A minor bug just goes to the backlog:JSX Conditions
Because Smithers re-renders the tree each frame, you can also branch with plain JSX conditions:<Branch> vs a ternary? Use <Branch> when you want both paths explicitly declared in the graph — it documents the fork. Use JSX conditions for simpler gating on whether a task should exist at all.
Looping: <Loop>
Some work isn’t done until it’s done. You write a draft, get feedback, revise, get more feedback. This is the pattern <Loop> is built for:
- The
untilcondition is evaluated at render time - If
false, the loop body runs again - Outputs are persisted per-iteration (keyed by
iterationcolumn) - The tree re-renders with updated context
- The
untilcondition is re-evaluated
until returns true or when maxIterations is hit — whichever comes first. Without maxIterations, a stubborn reviewer could keep you looping forever.
Accessing Previous Iteration Output
The interesting question: how does iteration N+1 know what iteration N produced? Usectx.latest() to feed the previous iteration’s output back into the next:
latestReview is undefined, so the writer gets the original topic. On every subsequent iteration, the writer gets the reviewer’s feedback. This is how iterative refinement works: each pass incorporates what the previous pass learned.
Max Iterations
onMaxReached | Behavior |
|---|---|
"return-last" | Stop looping, keep the final iteration’s output, continue the workflow. This is the default. |
"fail" | Stop looping and fail the workflow. |
Choosing the Right Pattern
You have four primitives and you’ve seen them individually. Now the question is: which one do I reach for?Quick Reference
| Primitive | Purpose | Use when… |
|---|---|---|
<Sequence> | Run tasks in order | Each step depends on the previous step’s completion |
<Parallel> | Run tasks concurrently | Tasks are independent and can run at the same time |
<Branch> | Choose one path | The next step depends on a runtime condition |
<Loop> | Repeat until done | Work needs iterative refinement (implement -> review -> fix) |
<Parallel> vs Dynamic Tasks
This trips people up. When do you use <Parallel> and when do you use .map()?
Use <Parallel> when you have a fixed set of different operations on the same data:
<Parallel> is for heterogeneous fan-out (different work, same time). .map() is for homogeneous fan-out (same work, different data).
Composition Patterns
| Pattern | What happens | Use case |
|---|---|---|
<Sequence> -> <Sequence> | Flat sequential chain | Simple pipelines |
<Parallel> -> <Task> | Fan-out, then combine | Run parallel work, aggregate results |
<Loop> -> <Sequence> | Iterative pipeline | Implement-review-fix cycles |
<Branch> -> <Sequence> | Conditional multi-step | Different pipelines for different conditions |
<Parallel> -> <Sequence> inside each | Parallel pipelines | Build + deploy web AND api simultaneously |
Synchronization
Both<Parallel> and <Loop> are synchronization points. The next task after them only runs after all their children complete:
Conditional Skipping
All control-flow components supportskipIf to bypass them entirely:
skipIf is true, the component returns null and none of its children are mounted.
Next Steps
- Sequence — Component API for ordered execution.
- Parallel — Component API for concurrency and
maxConcurrency. - Branch — Component API for conditional paths.
- Loop — Component API for iterative workflows.
- Workflow State — How outputs and
ctx.latest()drive control flow. - Implement-Review Loop — See these primitives in a production pattern.