> ## Documentation Index
> Fetch the complete documentation index at: https://graph-unify-model-roles.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent steps

> A bounded tool-calling loop inside a plan step, with guaranteed structured output

An `agent` step runs a bounded tool-calling loop and returns **structured JSON conforming to a schema you declare** — or says it couldn't, via `final: false`. It is a functional component, not a conversation: a prompt goes in, a validated object comes out.

Use it for the one thing plans genuinely can't express — a task where *which* tools to call, and how many times, depends on what earlier calls returned. Everything else belongs in ordinary steps, where dataflow is typed and inference-free.

```yaml theme={null}
steps:
  - id: E0
    tool_name: linear__list_issues
    input: { limit: 50 }

  - id: E1
    tool_name: agent
    input:
      prompt: |
        Review these issues and identify which are blocked, and why:
        {{E0.issues}}
      tools: ["linear__*"]                 # patterns, resolved at validate time
      max_iterations: 5
      output_schema:
        type: object
        required: [blocked]
        properties:
          blocked:
            type: array
            items:
              type: object
              properties:
                id: { type: string }
                reason: { type: string }
```

## Input

| Field            | Required | Meaning                                                                                                       |
| ---------------- | -------- | ------------------------------------------------------------------------------------------------------------- |
| `prompt`         | yes      | The task. Renders against prior results like any step input.                                                  |
| `output_schema`  | yes      | JSON Schema for the result. Must be `type: object`.                                                           |
| `tools`          | no       | Names or wildcard patterns to expose. Omit for the whole catalog; `[]` is a validation error, not "no tools". |
| `max_iterations` | no       | Inference rounds. Default 8.                                                                                  |
| `model`          | no       | A configured [model role](/models/models-and-providers#roles), standard or custom. Default: the `chat` role.  |
| `system_prompt`  | no       | Extra guidance appended to the built-in system prompt. Renders against the scope, like `prompt`.              |

Write `prompt` as a plain task description — what to do and what a complete
answer looks like. The built-in system prompt handles *how it is being run*:
the `output_schema` it must match, that it should accomplish the task in as few
turns as it can, and that independent tool calls belong in one turn. Repeating
any of that in `prompt` is redundant; if an agent is misbehaving in a way that
would apply to every agent, that belongs in the harness, not in your plan.

A round is **one model call**, including the one that produces the final answer — so `max_iterations: 1` can answer from the prompt alone but can never call a tool *and* answer. A malformed answer (schema miss, or no text and no tool call) also costs a round; retries and provider failover do not.

Field names are snake\_case, like every other part of a [plan file](/reference/plan-schema). The camelCase spellings (`outputSchema`, `maxIterations`, `systemPrompt`) still load, but any authoring command that rewrites the file normalizes them.

`output_schema`'s *value* is yours: graph never rewrites the property names inside the schema, so an agent can be held to a camelCase contract if that is what its consumer expects.

The whole input is checked at **load time**, wherever the step appears — top level or inside a `decide`/`map`/`reduce` body: unknown fields, a non-string prompt, `output_schema` that isn't valid object-typed JSON Schema, a `max_iterations` under 1, an empty `tools`, and template references that point forward or at nothing. A malformed agent step never reaches the run.

## Result

```json theme={null}
{
  "output":       { "blocked": [ … ] },
  "iterations":   3,
  "tools_called": [ { "tool": "linear__list_issues", "round": 1 } ],
  "final":        true
}
```

Later steps reference `{{E1.output.blocked}}` — `output` is the schema-conforming payload; the rest is provenance.

## The last round is the answer round

An agent's final round is handled differently: its tools are **withdrawn**, the
provider is told to force `output_schema`, and the agent is told plainly that
this is its last turn and it should answer with what it has.

That makes running out of budget degrade instead of collapse. The agent
returns `final: true` carrying whatever it managed to establish, rather than
throwing the work away — a partial dossier is worth something; an empty object
is worth nothing. It also means the forced round never needs a schema-repair
pass, because the provider guarantees the shape.

So `final: false` is now rare: it means the final round produced neither
structured output nor usable text. Keep checking it — the guarantee is still
"conforming output, or `final: false`", never a fabricated result — but expect
a real answer from a budget that runs out.

One consequence worth planning around: the agent cannot call a tool on its
last round, so a budget of *N* buys *N−1* rounds of tool use.
`max_iterations: 1` answers from the prompt alone, as documented above.

The agent is **not** told how many rounds it has. It is told to accomplish the
task in as few turns as it can, and the forced final round is the backstop.
Naming a number invites a model to treat it as an allowance to spend, and it
competes with whatever your own `prompt` says about how much ground to cover.

`final` is `false` when even the forced final round produced nothing usable. In
that case `output` is `{}` and **does not** conform to `output_schema`. A later
step reaching into `output` then fails as a bad path, so **check it**, or gate
on it:

```yaml theme={null}
  - id: E2
    tool_name: exit
    input:
      when: { value: "{{E1.final}}", op: eq, to: false }
      status: error
      message: "agent could not finish within its budget"
```

## Tool selection

`tools` accepts exact names and `*` wildcards anywhere: `linear__*`, `*__search`, `linear__list_*`, or `*` for everything.

Patterns are **resolved against the catalog at validate time**, so a plan naming tools that cannot load fails before it runs — like any other step tool name:

```
step E1: `tools` pattern 'linear__*' needs MCP server 'linear',
which is not configured under [mcp.linear]
```

`builtin__*`, `user__*`, and `plan__*` resolve exactly. MCP patterns resolve at the *server* level only, because listing a server's tools means connecting to it; the individual tool is still checked at dispatch. A pattern that matches nothing at run time is an error naming that pattern — a typo shrinks the catalogue silently otherwise.

`plan_and_execute` is **never** available inside an agent: nested planning loops have no coherent cost boundary. It is not advertised, and a model that asks for it anyway gets a tool error rather than a nested planner run. `plan__*` tools are available and work normally, so compose with plans instead.

## What it costs

An agent step is the most expensive thing in the pipeline: **one inference per round**, plus its tool calls. A `max_iterations: 8` agent can cost 8 inferences where an ordinary step costs zero. Reach for `map` with a per-item inference before reaching for an agent — see [iteration](/plans/iteration).

Rounds are not the whole bill. Two multipliers are easy to miss:

* **Each round re-sends the conversation so far.** Input tokens grow with the
  square of the round count, not linearly, so a long agent's cost is dominated
  by its input. [Prompt caching](/models/models-and-providers#prompt-caching)
  is on by default and takes most of that back — round *N* reads everything
  through round *N−1* at \~0.1× — but only while the prefix stays byte-stable.
* **Schema repair is uncapped.** Output that misses `output_schema` gets a
  `repair`-role fix-up that does *not* consume a round, so a `max_iterations: 8`
  agent can make up to 16 calls. Repairs are attributed to the agent's step, so
  the reported figure is the real one.
* **Inside a `map`, multiply by the item count.** Six items at
  `max_iterations: 18` is up to 108 rounds.

`max_iterations` is a ceiling the agent never sees, not a target it aims at —
set it to what the task could plausibly need in the worst case, not to what you
expect it to use. A well-scoped task finishes well inside it.

Don't estimate any of this — measure it. Every run reports per-step tokens and
cost:

```bash theme={null}
graph plan run my_plan --json | jq '.usage.by_step[0]'
```

The agent step will usually be first, since `by_step` sorts most expensive
first. See [what a run spent](/reference/scripting-contract#what-a-run-spent).

## Inside control-step bodies

`agent` is a legal body step for [`decide`](/plans/branching), [`map`, and `reduce`](/plans/iteration), and the body scope reaches its prompt:

```yaml theme={null}
  - id: E1
    tool_name: map
    input:
      over: "{{E0.incidents}}"
      concurrency: 4
      do:
        tool_name: agent
        input:
          prompt: "Diagnose incident {{item.id}}: {{item.summary}}"
          tools: ["grafana__*", "user__git_log"]
          output_schema:
            type: object
            properties:
              cause: { type: string }
```

`{{item}}`, `{{index}}`, and `{{accumulator}}` all resolve. Note the multiplier: this runs one agent *per item*.

The other control steps (`exit`, `decide`, `map`, `reduce`) still cannot nest in a body — call a `plan__*` for that.

## Errors

* **Tool failures return into the loop** as error results, so the agent can explain or route around them. They do not fail the step.
* **Reasoning carries across rounds.** The model's own thinking blocks are preserved and replayed, so it builds on what it worked out rather than re-deriving it each round.
* **Output that doesn't match `output_schema`** gets one `repair`-role fix-up pass; if that fails, the error goes back to the agent and consumes a round.
* **Transient LLM errors** retry with backoff and never consume a round.
* **A gate abort** during an inner tool call is a hard stop, exactly as elsewhere — see [errors and replanning](/plans/errors-and-replanning).
* **Empty data** while rendering `prompt` or `system_prompt` degrades rather than failing, consistent with every other step — at the top level and inside a body alike.

Every tool call an agent makes goes through the same dispatch path as any other step, so gates, events, the shape cache, and plan-cycle detection all apply at agent depth too. Inner calls report a nested step path — `E1/agent.2/linear__list_issues` at the top level, `E1/do.3/agent.2/linear__list_issues` for an agent in a map body — so a breakpoint on the agent step pauses each call it makes, and concurrent map items stay distinguishable.
