Fans a prompt out across N parallel branches: either the same prompt run
count times, or one branch per item in a list (over). It collects each
branch's full text output and, optionally, runs a built-in reduce step
that synthesizes the results into one. map routes to success when at
least one branch returns a result, and to failure only when every branch
fails.
Purpose#
Use map when you want N independent attempts at the same task, or the
same task applied across a list, run in parallel and optionally merged
afterward. count mode (same prompt × N) suits best-of-N drafting,
multiple independent analyses, or sampling several takes before reducing
to the best one. over mode (one branch per item) suits summarizing each
section of a document, drafting a reply per support ticket, describing
each file in a changeset, or scoring each option.
It differs from consensus, which votes (boolean,
confidence, or answer, toward a quorum), and
round_robin, which ranks options via pairwise
matchups. map keeps every branch's raw text output and hands the
list downstream. If you don't need a vote or a ranking, and you need the
actual N outputs, map is the node.
YAML schema#
states:
draft:
type: map # discriminator
label: Draft candidate replies # required
model: claude-haiku-4-5 # required, must be in roscoe.yaml allow-list
prompt: | # required — sent to every branch
Draft a warm, concise reply to this refund request:
{{ request }}
count: 5 # exactly ONE source: count | over
# over: "{{ split.sections }}" # the other source (see below)
# itemVar: item # over mode only — the per-item variable name
reduce: # optional — one synthesis call over the outputs
prompt: |
Merge these drafts into the single best reply:
{{ outputs }}
maxRetries: 1 # optional, 0-3 (per branch)
on: # exactly two keys: success | failure
success: send
failure: escalateConfiguration#
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
type |
'map' |
yes | — | Discriminator. |
label |
string | yes | — | Human-readable name for the step. |
model |
string | yes | — | Model id; checked against roscoe.yaml at pre-flight. |
prompt |
string | yes | — | Sent to every branch. Supports {{ node.key }}; in over mode also {{ item }} (see below). |
count |
integer (2-12) | one of | — | Run this many parallel branches with the same prompt. |
over |
string | one of | — | A {{ … }} ref to a list to map over, one branch per item (capped at 12). Accepts prose (see below). |
itemVar |
string | no | item |
over mode: the variable each item binds to in the prompt, e.g. {{ item }}. |
reduce |
{ prompt } |
no | — | Optional synthesis over the collected outputs. Omit to collect-only. |
maxRetries |
integer (0-3) | no | 1 |
Per-branch retries on a transient failure. |
timeoutSeconds |
integer (1-2147483) | no | min(120s, defaults.timeoutMs) |
Per-branch deadline. When set it is authoritative. |
maxIterations |
integer (1-50) | no | — | Per-run iteration cap when this node sits in a feedback loop. |
detectStall |
boolean | no | false |
Fail the run early if the outcome is identical three times in a row. |
on |
transition map | yes | — | Must contain success and failure. |
Exactly one of count or over must be set. The schema rejects zero or
both. {{ item }} is only meaningful in over mode; using it with count
is flagged at validation time (in count mode every branch gets the
identical prompt).
Fan-out modes#
| Mode | Use it when… |
|---|---|
count |
you want N independent attempts at the same task (best-of-N drafts, repeated analyses). |
over |
you have a list and want the same task applied to each item (per-section, per-ticket). |
The two are mutually exclusive. The editor presents them as a segmented toggle and clears the other field when you switch, so an invalid both-set state can't be saved.
The shape is the same regardless of mode: one list in, N branches out, and an optional reduce back down to one result.
flowchart LR L[Input list] --> I1[item 1] L --> I2[item 2] L --> I3[item 3] I1 --> R[reduce] I2 --> R I3 --> R R --> O[Output]
Each item fans out to its own parallel branch; the optional reduce step
then folds every branch's output into one result. Skip reduce and the
branch outputs flow downstream as a list instead.
Mapping over a generated list (over)#
The most common shape is an earlier node producing a list, then map
fanning out over it. The upstream node needs no special formatting: over
accepts an array value (a json-typed workflow input, a subworkflow
output, or another fan-out node's array output, e.g. {{ prev.outputs }}),
a JSON array embedded in text, or free prose, a bulleted list, or script
stdout. For that last case, Roscoe extracts the list with one model call,
so an ai_agent that brainstorms options in plain English, or a script
node that prints one item per line, both work directly.
states:
brainstorm:
type: ai_agent
model: claude-haiku-4-5
prompt: Brainstorm 5 distinct marketing channels for a coffee subscription.
on: { done: pitch }
pitch:
type: map
model: claude-haiku-4-5
over: '{{ brainstorm.response }}' # the agent's prose — no "return JSON" needed
itemVar: channel
prompt: 'Write a one-line pitch for the {{ channel }} channel.'
on: { success: plan, failure: failed }A real array (or a JSON array embedded in text) is used directly and skips the extraction call. The extraction step is told to return nothing rather than invent items, so an upstream node that misunderstands the ask (posing a question back, apologizing, writing a single paragraph) won't fabricate a list. Instead the node fails with an actionable error naming the ref, distinct from an "all branches failed" outcome.
Item shape: when over resolves to a real array, each {{ item }} is
the original element, so {{ item.title }} works if the items are
objects. When the list is extracted from free prose, each item is the
option's text string ({{ item }} is the whole text; {{ item.field }}
resolves to empty).
No silent caps.
overis capped at 12 items; if the list resolves to more, the node maps the first 12 and surfacesresolvedCount/truncatedin its outcome (and logs a warning) so the cap is visible, never silent.
Optional reduce#
When reduce is set and at least one branch succeeded, map runs one more
call over the collected outputs and stores the result as reduced. The
reduce prompt sees two injected variables: {{ outputs }}, a JSON array
of the successful branch responses (e.g. ["draft one","draft two"]), not
newline-joined text, so word the prompt to expect a JSON blob; and
{{ items }}, the mapped items in over mode (empty in count mode).
Omit reduce to collect only: the raw outputs flow downstream as
{{ <nodeId>.outputs }} for a later node to consume.
Outputs#
map writes a MapOutcome into variables.<nodeId>:
| Key | Type | Notes |
|---|---|---|
outcome |
'success' | 'failure' | 'cancelled' |
Mirrors the chosen transition. |
outputs |
string[] |
The successful branch responses, in branch order. The headline output. |
reduced |
string | null | The synthesis, when reduce ran; null otherwise. |
branches |
MapBranch[] |
Per-branch audit: index, item?, status, response, attempts, durationMs. |
successes |
number | Count of completed branches. |
failures |
number | Count of failed / timed-out branches. |
count |
number | Resolved branch count. |
mode |
'count' | 'over' |
Echo of the fan-out mode. |
resolvedCount |
number (over mode) | Items over resolved to before the 12-cap. |
truncated |
boolean (over mode) | true when the list was capped to 12. |
Downstream nodes read {{ map.outputs }} (a JSON array via interpolation),
{{ map.reduced }}, {{ map.successes }}, etc.
Transitions#
| Transition | When |
|---|---|
success |
at least one branch returned a non-empty response. |
failure |
every branch failed (or, in over mode, the list was empty). |
Both keys must be present in on. Partial success is success: failed
branches are recorded in branches[], but they don't fail the node as long
as one branch produced output. An over ref that resolves to nothing, or
isn't a list at all, is a separate, actionable run failure that names the
ref, not the failure transition.
Execution mode notes#
When the run is driven from Claude Code or Cowork over MCP, the fan-out is
handed back to the host, which runs each branch as an independent subagent
on your Claude subscription and returns the outputs via advance_map. If
over needs list extraction, that call is also handed back as an
extract round, and the optional reduce runs as a final single-subagent
round. Every model call runs on your subscription this way; the server
makes none, and no API key is required. See
Running on your Claude subscription.
Server and web UI runs, and runs driven from Claude desktop chat, execute
branches in parallel against the configured LLM backend (a bounded
concurrency pool). Any list extraction or reduce call also runs
server-side, so it needs ANTHROPIC_API_KEY set or the claude CLI
installed.
Worked example#
name: map-draft-replies
version: 1
description: Five parallel drafts of a customer reply, merged into the best one.
initial: draft
inputs:
- name: request
type: string
optional: true
description: The customer's refund request to reply to.
default: |
I bought the annual plan two days ago but my company already has a team
licence. Can I get a refund? I haven't used it at all.
states:
draft:
type: map
label: Draft five candidate replies
model: claude-haiku-4-5
count: 5
prompt: |
A customer wrote in with this request:
"{{ request }}"
Draft a warm, concise reply (3-5 sentences). Vary your angle from other drafts.
reduce:
prompt: |
Here are several independent draft replies to the same customer:
{{ outputs }}
Merge them into the single best reply. Return only the reply.
on:
success: done
failure: failed
done:
type: end
label: Reply ready
outcome: success
message: Synthesized the best reply from the drafts.
failed:
type: end
label: All drafts failed
outcome: failure
message: Every branch failed to produce a draft.Common pitfalls#
- Setting both
countandover(or neither). They're mutually exclusive: pick exactly one. The schema rejects zero or both. - Using
{{ item }}incountmode. It only resolves inovermode; everycountbranch gets the identical prompt, so using it there is flagged at validation time. - Expecting
{{ outputs }}to be newline-joined text. It's a JSON array of branch responses; word the reduce prompt to expect a JSON blob. - Treating an empty
overas a normalfailure. A list that resolves to nothing (or isn't a list at all) is a separate, actionable run failure that names the ref. Fix the upstream node rather than routing it throughfailure. - Relying on more than 12 items.
overcaps at 12; checktruncated/resolvedCountif you might exceed it, and pre-filter upstream if you genuinely have more. - Missing API key for server-side runs. When
mapruns server-side (standalone, or driven from Claude desktop chat), branches, and any extraction or reduce, needANTHROPIC_API_KEYor theclaudeCLI. Driven from Claude Code or Cowork, it runs entirely on your subscription and needs neither.
See also#
- consensus — N agents vote on one question (quorum), rather than collecting N raw outputs.
- round_robin — rank several options via pairwise matchups, rather than mapping a task across them.
- Running on your Claude subscription — how the fan-out (and extraction / reduce) run as host subagents.