ai_agent node

One-shot Claude call; records the response and continues.

Sends a prompt to a Claude model and records the raw text response into the workflow's variables. Unlike ai_judge, ai_agent does not parse, validate, or branch on the response. It always proceeds along its single outgoing transition.

Purpose#

Use ai_agent when you want the model to produce content that a later step will consume: a summary, a draft, a translation, a chunk of generated text. If you need the model to make a routing decision, use ai_judge (one agent) or consensus (N agents) instead.

YAML schema#

states:
  summarize:
    type: ai_agent # discriminator
    label: Summarize logs # required, shown in UI / logs
    model: claude-haiku-4-5 # required, must be in roscoe.yaml allow-list
    prompt: | # required, supports {{node.key}} interpolation
      Summarize the following in one sentence:

      {{collect.stdout}}
    maxRetries: 1 # optional, 0-5, default 1
    timeoutSeconds: 600 # optional; overrides the global default for this node
    allowedTools: [Read, Edit, Bash] # optional; turns this into a tool-enabled agent
    on: # exactly one outgoing key — name is yours
      next: report

Configuration#

Field Type Required Default Meaning
type 'ai_agent' yes Discriminator.
label string yes Human-readable name for the step.
model string yes Model id; the runner enforces it against roscoe.yaml.
prompt string yes Sent to the model verbatim after {{...}} interpolation.
maxRetries integer (0-5) no 1 (or defaults.maxRetries from roscoe.yaml) Retries on empty response or transient backend errors. Timeouts are not retried.
timeoutSeconds integer (1-2147483) no global default (defaults.timeoutMs, else 5 min) Hard wall-clock timeout for this node. Overrides the global default; raise it for long agentic edits. See Timeouts.
allowedTools string[] no none (plain text completion) When set, runs this node as a tool-enabled agent via the claude CLI with these tools allowed. See Tool-enabled agents.
maxIterations integer (1-50) no Per-run iteration cap when this node sits in a feedback loop. See the closed-loop feedback recipe.
detectStall boolean no false Fail the run early if the node produces an identical output three times in a row. Most useful on reviewer-shaped nodes.
on transition map no At most one key; the executor takes the first.

Outputs#

Key Type Notes
response string The model's full text reply.

Reference downstream as {{nodeId.response}}. See Output chaining for how to chain it into a later node's prompt or condition.

Transitions#

ai_agent is a single-transition node. The executor takes the first key of the on map regardless of what the model said. The schema integrity check forbids more than one outgoing edge from an ai_agent for this reason. If you need branching, switch to ai_judge.

The transition key name is arbitrary; common choices are next or done.

Worked example#

name: summarize-system
version: 1
description: Collect uname/date and ask Claude for a one-line summary.
initial: collect
states:
  collect:
    type: script
    label: Collect system info
    script: |
      import { $ } from 'bun';
      const res = await $`uname -a && date`.nothrow();
      export default res.exitCode === 0;
    validator:
      kind: boolean
    on:
      'true': summarize
      'false': done

  summarize:
    type: ai_agent
    label: Summarize
    model: claude-haiku-4-5
    prompt: |
      Briefly summarize the following system output in one sentence:

      {{collect.stdout}}
    on:
      next: report

  report:
    type: script
    label: Print summary
    script: |
      // Read the upstream response from the environment — safer than
      // interpolating it into source, since it can't break the syntax.
      console.log(process.env.ROSCOE_OUT_summarize_response ?? '');
      export default true;
    validator:
      kind: boolean
    on:
      'true': done
      'false': done

  done:
    type: end
    label: Done
    outcome: success

Tool-enabled agents#

By default ai_agent is a one-shot text completion: prompt in, text out, no side effects. Set allowedTools to turn it into an agentic step that can read and modify files and run commands in the run's working directory, useful for "apply this refactor", "fix the failing test", or "update the shared onboarding doc" steps.

edit_readme:
  type: ai_agent
  label: Update the README
  model: claude-sonnet-5
  allowedTools: [Read, Edit, Write]
  prompt: |
    Update README.md to document the new --json flag.
  on:
    next: verify

The allowed tool names are a curated set of Claude Code tools:

Tool Grants
Read Read files.
Edit Modify existing files in place.
Write Create or overwrite files.
Glob / Grep Search the working tree.
Bash Run shell commands.
WebFetch / WebSearch Fetch URLs / search the web.

How it works and what to know:

  • The claude CLI is required. Tool-enabled nodes always run on the CLI backend (the API/SDK backend has no built-in file/bash tools), regardless of whether ANTHROPIC_API_KEY is set. If the CLI isn't installed the node fails with a clear message: install Claude Code.
  • The agent operates in the run's working directory (cwd). For an MCP-launched run that's the project root the call was scoped to; tool grants are pre-approved so the agent runs without interactive permission prompts.
  • Only the listed tools are allowed. Grant the narrowest set the step needs, e.g. [Read, Grep] for an analysis step that must not write.
  • response still holds the agent's final message. It's a summary of what it did, not a transcript of every tool call.
  • Omitted or empty allowedTools = plain text completion on the default backend, with one exception: a single oversized interpolated value auto-grants read-only Read/Grep and routes the node through the CLI so the model can query the spilled data. See Oversized inputs.

Oversized inputs#

Interpolating a big value into a prompt (say {{ extract.stdout }} from a verbose script step, or a large JSON array of records) can blow past the model's context window. Roscoe handles that for you instead of silently dropping data:

  • Under 16 KiB, an interpolated value is inlined unchanged.
  • Over 16 KiB, on a node that can run on the claude CLI, the full value is spilled to a file in the run directory and replaced in the prompt by a short manifest: the spill file's absolute path, its size, and (for a JSON array) the record count and field names. The surrounding instruction text is left intact. A JSON array is written as JSONL (one record per line) so the model can Grep it line-wise; anything else is written as plain text. The node is auto-granted read-only Read and Grep (never Bash or Write) so it can query the file instead of guessing from a truncated excerpt.
  • When the CLI isn't available (e.g. an API/SDK node with no tools), the value is head/tail truncated instead, with a marker pointing at the full spilled file (the older, deterministic fallback).

Two things worth knowing. The auto-grant is a real (read-only) privilege increase: a no-tools node gains filesystem read access once its input crosses the threshold, and the CLI's Read/Grep aren't sandboxed to the run directory. That's why it's limited to those two read-only tools. And this only kicks in for CLI-backed nodes, which already run non-deterministic tool loops; below-threshold values and API/SDK nodes are untouched.

Retrieval, not synthesis. This path is built for finding things: a value, a matching record, the lines about a topic. The model greps the spill file and reads around the hits, which is reliable at any size.

It's much weaker at questions that need the whole input at once, like "summarize everything", "how many of these rows are over the threshold", or "total up every amount". Grep can't shortcut those, and the model won't reliably read the entire file. For whole-document synthesis or aggregation, use a map node instead: it splits the input into chunks, gives each chunk to its own agent that reads it in full, then reduces the per-chunk results. In a head-to-head on a ~49 KB document, a single queryable agent's "count the rows over N" answer was wildly off while a map + reduce landed within one of the true count.

Timeouts#

ai_agent has a 5-minute default wall-clock timeout (or defaults.timeoutMs from roscoe.yaml if set). Set timeoutSeconds on the node to override it for that step, and raise it for tool-enabled agents that do real work (multi-file edits, builds), since those take far longer than a one-shot completion. A timed-out node fails; timeouts are not retried.

Common pitfalls#

  • Trying to branch on the response. ai_agent cannot route. Adding multiple keys to on is a schema violation. Use ai_judge when the content of the answer needs to determine the next step.
  • Forgetting to allow the model. model: must appear in the roscoe.yaml model allow-list. Pre-flight rejects unknown models with a clear error before any node runs.
  • Empty responses. If the model returns no text, ai_agent retries up to maxRetries times before failing. Timeouts (5 min default) are not retried; they indicate a systemic hang, not a flaky completion.
  • Long prompts that overflow. There is no per-node prompt-length cap, but interpolating {{somenode.stdout}} from a verbose script step can blow past the model's context window. Roscoe spills or truncates oversized values automatically (see Oversized inputs), but summarizing upstream still gives the model a cleaner, cheaper prompt.
  • Accidentally relying on the response shape. ai_agent returns free-form text. If you need structured output, prompt for JSON in ai_judge and let its validator parse the envelope.

Where to next#

  • consensus — vote across N agents instead of trusting one response.
  • map — fan the same prompt across branches, or map over a list, and reduce the results.
  • Output chaining — read {{node.response}} from a later prompt or condition.
  • LLM backends — how Roscoe picks between the SDK and the claude CLI.

View this page as Markdown

Predictable workflows from unpredictable AI