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.
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
for the full cap and stall-detection rules.
Save as multi-step-analysis.workflow.yaml in ~/.roscoe/workflows/ or
<repo>/.roscoe/workflows/.
Running it#
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