# Multi-step AI analysis

Use this when you want one agent to do open-ended work (analysis, drafting,
classification) and a second agent to check the first's output before the
workflow proceeds. The pattern catches the "looks plausible but wrong" class
of mistakes that single-shot prompts miss, without paying for full
`consensus` voting.

## The workflow

Two AI nodes chained together. `summarize` is an `ai_agent` (free-form
response, single forward edge). `verify` is an `ai_judge` (validator-driven
routing) that reads the summarizer's response via the
`{{ summarize.response }}` template and decides whether the summary holds up.

```yaml
name: multi-step-analysis
version: 1
description: Summarize then verify, second agent checks the first's work.
initial: summarize
inputs:
  - name: researchDoc
    type: string
    description: The research document to summarize.
states:
  summarize:
    type: ai_agent
    label: Summarize the document
    model: claude-haiku-4-5
    prompt: |
      Summarize the following research document in three bullet points,
      focused on the findings that would change a decision. Be specific
      with numbers; do not editorialise.

      Document: {{ researchDoc }}
    on:
      next: verify

  verify:
    type: ai_judge
    label: Verify the summary
    model: claude-sonnet-5
    prompt: |
      Here is a summary of a research document:

      {{ summarize.response }}

      Original document: {{ researchDoc }}

      Does the summary accurately reflect the document's findings AND cite
      numbers consistent with the source? Answer with the JSON shape
      requested below.
    validator:
      kind: boolean
    maxRetries: 2
    maxIterations: 3
    on:
      'true': accept
      'false': summarize

  accept:
    type: end
    label: Accepted
    outcome: success
    message: Verified summary ready.
```

`researchDoc` is declared under `inputs:` because the prompts read it as a
bare `{{ researchDoc }}`; an undeclared bare reference fails validation even
when you plan to supply it with `--var`. The `'false': summarize` edge also
makes `summarize` ↔ `verify` a cycle, so one node in it needs `maxIterations`
(here, 3 on `verify`) or the loader rejects the workflow outright. See
[/docs/recipes/closed-loop-feedback](/docs/recipes/closed-loop-feedback)
for the full cap and stall-detection rules.

Save as `multi-step-analysis.workflow.yaml` in `~/.roscoe/workflows/` or
`<repo>/.roscoe/workflows/`.

## Running it

```bash
roscoe run multi-step-analysis --var researchDoc='Q3 churn study: ...'
```

Or interactively in the web UI. With Claude Code via MCP, the run pauses at
each AI node and Claude itself supplies the response via `advance_run`.

## Why these node types

`ai_agent` suits the summarizer: free-form output, and the single outgoing
transition (`next`) only means "continue when done"; there's no branching
decision. Output goes to `variables.summarize.response`. `ai_judge` suits the
verifier because its validator (`kind: boolean`) parses the model's JSON
envelope into a routing key; this example deliberately uses a stronger
model (`claude-sonnet-5`) for verification, since it's the call that has
to catch mistakes.

The `'false': summarize` edge re-runs the summarizer when the judge
disagrees. It sees the same prompt again, but a fresh model call usually
produces a different response. Two separate knobs bound this: `maxRetries`
on the judge caps retries of a single model call after a transient failure,
while `maxIterations` caps how many times the whole `summarize → verify`
cycle can repeat before the run fails.

## The `{{ nodeId.field }}` template

After every node completes, its output is merged into a shared `variables`
bag and made available to downstream prompts and scripts. The fields by
node type:

| Node type   | Fields                                                          |
| ----------- | --------------------------------------------------------------- |
| `ai_agent`  | `<id>.response`: raw text                                       |
| `ai_judge`  | `<id>.result`, `<id>.reasoning`, `<id>.score` (confidence only) |
| `script`    | `<id>.result`, `<id>.stdout`, `<id>.exitCode`                   |
| `condition` | `<id>.result`: `'true'` or `'false'`                            |
| `consensus` | `<id>.outcome`, `<id>.approvals`, `<id>.votes[]`, etc.          |

Anything passed via `--var name=value` (or the `context` field on the REST
`POST /api/runs`) is reachable as `{{ name }}`, provided it's declared under
the workflow's `inputs:`.

## Variations

For a three-stage version, chain summarize → critique (`ai_agent`) → judge
(`ai_judge`); the middle critique step gets the summarizer's output, drafts
objections, and the judge weighs both before deciding. For a validator with
a confidence score, switch the judge's validator to `kind: confidence` with
a threshold, so borderline outputs go to a human review step instead of
being silently rejected. To pull data from a script step first, insert a
`script` node before `summarize` that fetches the document (curl, or reads a
file) and reference its stdout in the summarizer's prompt (see
`/docs/recipes/script-then-ai`).

## See also

- `/docs/nodes/ai-agent` — single AI completion node
- `/docs/nodes/ai-judge` — validator-routed AI node
- `/docs/recipes/consensus-voting` — when one verifier isn't enough
