# map node

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](/docs/nodes/consensus), which votes (boolean,
confidence, or answer, toward a quorum), and
[round_robin](/docs/nodes/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

```yaml
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: escalate
```

## Configuration

| 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.

```mermaid
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.

```yaml
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.** `over` is capped at 12 items; if the list resolves to
> more, the node maps the first 12 and surfaces `resolvedCount` / `truncated` in
> 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](/docs/running/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

```yaml
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 `count` and `over` (or neither).** They're mutually exclusive:
  pick exactly one. The schema rejects zero or both.
- **Using `{{ item }}` in `count` mode.** It only resolves in `over` mode; every
  `count` branch 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 `over` as a normal `failure`.** 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 through
  `failure`.
- **Relying on more than 12 items.** `over` caps at 12; check `truncated` /
  `resolvedCount` if you might exceed it, and pre-filter upstream if you genuinely
  have more.
- **Missing API key for server-side runs.** When `map` runs server-side
  (standalone, or driven from Claude desktop chat), branches, and any
  extraction or reduce, need `ANTHROPIC_API_KEY` or the `claude` CLI. Driven
  from Claude Code or Cowork, it runs entirely on your subscription and needs
  neither.

## See also

- [consensus](/docs/nodes/consensus) — N agents **vote** on one question (quorum),
  rather than collecting N raw outputs.
- [round_robin](/docs/nodes/round-robin) — **rank** several options via pairwise
  matchups, rather than mapping a task across them.
- [Running on your Claude subscription](/docs/running/subscription) — how the
  fan-out (and extraction / reduce) run as host subagents.
