map step runs the same body once per item of a list; a reduce step folds a list into a single value. Both take over (anything that renders to an array) and do (the same body grammar as a decide branch: a single tool call or an inline step list). Where decide answers “which action is correct next?”, map and reduce answer “do this for each of these” — update every stale issue, summarize each incident, roll a page of results into one report.
⟳ junction with one cyan body row — every iteration lands on that row, like a breakpoint on a loop line:
A map paused mid-run by a loop-line breakpoint: the first iteration done, the second awaiting the debugger's decision on the one structural body row.
The body and its scope
Insidedo, two pseudo-roots exist per item: {{item}} (the element) and {{index}} (0-based position). A reduce body gets a third, {{accumulator}} — the running value, starting at initial — and each run’s result becomes the next {{accumulator}}. The body may also reference plan input and any earlier top-level step, exactly like a decide branch.
The body is either a single tool call ({tool_name, input}, no id — any catalog tool, including plan__* and plan_and_execute) or an inline step list with same-iteration dataflow: each step may reference earlier steps in the same iteration, and the last step’s result is the iteration’s output. Body step ids must not reuse top-level ids and are invisible outside the body. Iterations are isolated — an item’s body never sees another item’s results.
Bodies may contain agent and ask steps, whose prompt or question reaches {{item}}, {{index}}, and {{accumulator}}, and filter steps — whose own {{item}}/{{index}} shadow the body’s inside the gate, so reference the outer element in the filter’s over. An ask inside a body is asked once per item, always serialized — even under concurrency — so prefer asking once about the whole list. Bodies must not contain exit, decide, map, or reduce. For nested control flow — including a map inside a map — put it in a plan and call plan__* from the body; cycle detection and the depth cap apply as usual, and an error-exit inside that sub-plan fails the map/reduce step.
The step result
over array is not an error: map yields {count: 0, results: []} and reduce yields initial untouched — the plan continues. Guard with an exit gate when empty input should stop the plan instead. A non-array over is a plan defect (hard failure in your plans, replan for the planner).
Only over renders up front
Like a decide step, map/reduce defer rendering: over (and reduce’s initial) render against prior results first; the body renders per item, only when that item’s scope exists. {{item.id}} before the first item exists would be meaningless — and validation rejects pseudo-roots in over/initial for exactly that reason. EmptyData raised inside an item’s body degrades normally (errors).
Concurrency
map accepts concurrency (default 1): the maximum items in flight. At 1 items run strictly in order; above 1, up to that many run at once, and results still comes back in input order. Raise it only when the per-item calls are independent — it is a throughput knob, not a semantics knob.
reduce has none: every iteration reads the previous accumulator, so a fold is sequential by definition. For concurrent aggregation, map first (concurrently), then reduce over {{Ex.results}}.
Per-item inference
When a step runs inference over a list — classify each issue, summarize each incident, score each finding — prefer amap whose body calls builtin__infer (or a user__ prompt tool) with {{item}} interpolated per call, over one inference step carrying the whole list in its instruction. Small, focused contexts are cheaper and more accurate, and concurrency recovers the speed. Reserve whole-list interpolation for genuinely cross-item questions — ranking, deduplication, aggregation. The planner is instructed the same way, so planner-authored plans default to this shape too.
Two things to know when raising concurrency:
- Same-server MCP calls serialize anyway. Each MCP server connection handles one call at a time, so concurrency pays off across different servers,
plan__*sub-plans, and LLM-call tools — not for ten calls to one server. - Failures drain, not cancel. When an item fails, items already in flight run to completion (cancelling mid-call would leave server-side work orphaned and unreported); items not yet started are skipped. The lowest-index failure is the one reported.
Failure and replanning
A failing item fails the whole step — attributed asstep E1 (map) with the item index and inner tool named in the message (`do` item 3 (linear__update_issue): …). Human-authored plans fail hard as always; planner-authored plans replan with that context.
Body steps count in steps_executed — a map over 3 items with a two-step body reports seven executed steps (the map plus six).
Semantics summary
plan_and_execute’s planner also has both tools, so LLM-authored plans can fan out over their own intermediate results.
A body step may also be an agent — the per-item scope ({{item}}, {{index}}, {{accumulator}}) reaches its prompt. That runs one agent per item, so mind the inference multiplier.