---
title: "Agent steps and handoffs"
description: "Split an agent into steps with their own instructions, tools and collected values, and hand calls between agents mid-call."
---

A single prompt gives the model the whole job at once. **Steps** give it one
phase of the call at a time: each step has its own instructions, its own tools,
an optional line spoken word for word, and values it must collect before moving
on. **Handoffs** let another agent in your organization take over mid-call,
with the transcript intact, so the caller never repeats themselves.

Every agent starts as a single prompt. Nothing about it changes until you add
steps or handoffs.

## Steps or a single prompt?

| Use steps when | Keep a single prompt when |
| --- | --- |
| The call has phases that must happen in order: verify, then book. | The conversation is open-ended: FAQ, reception, general support. |
| A value must be captured before the agent moves on. | Nothing has to be collected in a fixed order. |
| A rule must never bend, such as "callers under 18 go to a guardian". | The model's judgment is good enough for every branch. |
| A lookup has to run at a fixed point in the call. | Tools can be called whenever the caller asks. |
| Part of the call belongs to a different agent, with its own prompt and voice. | One persona handles everything. |

Steps are not a scripted phone tree. Inside a step the model runs the
conversation as it does with a single prompt. The step decides what it is
working on, which tools it can use and what it needs before it can leave.

## How a stepped agent is built

When an agent has steps, its `prompt` holds the **shared instructions**: the
persona and rules that apply in every step. Each step adds its own
instructions on top. On every turn the model sees the shared instructions, the
current step's name and instructions, and the values collected so far.

In the builder, switch the agent from **Single prompt** to **Steps**. You get a
card per step with its name, instructions, say line, tools, collected values
and transitions, a handoffs picker, and a read-only flow graph of the steps.
Switching back to a single prompt offers a flattened prompt that merges the
shared instructions and every step.

Over the API, `steps` and `handoffs` are fields on the
[agent](/api-reference/agents). Like every agent field, a `PATCH` stages them
in the draft; `POST /v1/agents/{agent_id}/deploy` makes them live. Sending
`"steps": null` switches the agent back to a single prompt. Test calls and
**Talk** run the draft.

<Tip>
  The [copilot](/guides/ask-the-copilot) can build steps from a description
  ("add a step that verifies date of birth before booking") and edit one step
  without touching the others.
</Tip>

### Step fields

| Field | Meaning |
| --- | --- |
| `id` | Required. Lowercase letters, digits and underscores, starting with a letter, up to 40 characters. Unique within the agent. |
| `name` | Required display name, 1 to 80 characters. |
| `instructions` | Required, up to 20,000 characters. Supports `{{var}}` [call variables](/guides/call-variables), like `prompt`. |
| `say` | Optional line spoken word for word when the step is entered, up to 1,000 characters. Supports `{{var}}`. |
| `tools` | `null` gives the step every tool attached to the agent. A list limits the step to those tools, by name. Built-in actions such as `end_call` and `transfer_call` follow the agent's settings in every step. |
| `collect` | Up to 20 values the step gathers. Each has a `name`, a `type`, a `description` and `required` (default `true`). |
| `on_enter_tool` | Optional tool name, called automatically when the step is entered. See [Webhook steps](#webhook-steps). |
| `transitions` | Ordered list of where the step can go next. See [Transitions](#transitions). |
| `global_when` | Optional. Makes the step reachable from any step. See [Global steps](#global-steps). |
| `voice` | Optional voice id for this step. Ignored when the agent uses a custom voice configuration. |

The graph itself is `{"start_step": "<step id>", "steps": [...]}`. The call
begins in `start_step`.

## Transitions

Each transition has `to`, a step id, and exactly one of:

- **`when`**: plain language, and the **model decides**. The agent gets a
  built-in `go_to_step` tool listing the steps it may move to, with each
  `when` as the reason to pick it.
- **`condition`**: a rule on collected values, and **code decides**. The model
  cannot skip it or talk its way around it.
- **`"always": true`**: move on without a check. Code decides first: as soon
  as a step is entered, or once its required values are collected, the first
  matching `condition` or `always` transition wins, before the model can pick
  a `when`. So put `always` after the conditions as their fallback, or alone
  to chain a step straight into the next one. A step with `always` never
  waits for a `when` choice.

A `condition` is `{"value": <collected name or dotted path>, "op": <op>, "target": <literal>}`:

| `op` | True when the value |
| --- | --- |
| `eq`, `ne` | equals, or does not equal, `target` |
| `lt`, `lte`, `gt`, `gte` | is less than, at most, greater than, or at least `target` |
| `in`, `not_in` | is, or is not, one of the values in the `target` list |
| `exists`, `missing` | has, or has not, been collected (no `target`) |

`eq`, `ne`, `in` and `not_in` compare text case-insensitively after trimming
spaces. Numbers compare as numbers and dates as `YYYY-MM-DD` strings. A value
that was never collected makes every op false except `missing`.

Write `when` and `global_when` as a condition, such as "The caller wants to
book an appointment", not as something to say. The agent sees them as internal
routing notes and is told never to read them out or mention that it is
changing steps.

When the model calls `go_to_step`:

1. Every `required` value in the step must be present. If one is missing, the
   model is told which one, and asks the caller for it.
2. The values are stored.
3. `condition` and `always` transitions are checked in order. The first match
   wins.
4. Otherwise the agent moves to the step the model chose.
5. If nothing matches and the model named no step, the agent stays where it is.

A jump to a [global step](#global-steps) is the exception: the caller changed
topic, so it goes ahead even when the current step still has required values
missing, and before that step's conditions are checked.

`condition` and `always` transitions are also checked every time a step is
entered, so a run of code-decided steps plays out in one go. The chain stops:

- at a step it has already visited on this run;
- after 8 hops;
- at a step that still has required values to collect, unless the step has an
  `on_enter_tool` (a webhook step always routes on its result).

Only the `say` line of the step where the chain stops is spoken.

A step with no transitions is **terminal**. The agent finishes there and ends
or transfers the call with its normal built-in actions.

## Collecting values

List what a step needs in `collect`. The model passes the values when it calls
`go_to_step`, and they stay available for the rest of the call.

| `type` | Holds |
| --- | --- |
| `string` | Any text |
| `number` | A number |
| `boolean` | `true` or `false` |
| `date` | A date as `YYYY-MM-DD` |

Collected values are call content. They appear on the call next to the
transcript and follow the same retention, deletion and redaction rules. When
the agent's call recording is turned off, collected values are neither stored
nor sent in webhooks; the step timeline still is. See
[Where to see it on a call](#where-to-see-it-on-a-call).

## Say lines

`say` is spoken exactly as written when the agent enters the step. The model
then carries on in the new step in the same reply, knowing the line was just
said, so it does not repeat it. Use `say` for greetings, required disclosures
and fixed wording, and leave the rest of the reply to the model.

The start step's `say` is the agent's opening line when it speaks first, after
any consent announcement. When the caller speaks first, it opens the agent's
first reply to a person, never to an IVR or a voicemail greeting.

A step's `say` is not spoken again when the call returns to it through
`__previous__` or a hand-back. Write it the way it should sound: spell out
anything the voice could misread. Say lines support `{{var}}` call variables
but not collected values.

## Webhook steps

Set `on_enter_tool` to one of the agent's tools and it runs as soon as the step
is entered, without waiting for the model. Its arguments are the collected
values whose names match the tool's parameter names. The result is stored as a
collected value named `<tool>_result`, truncated to 4 KB, and the step's
`condition` and `always` transitions are checked straight away, so the lookup
can route the call on its own result.

The stored value has two fields:

| Field | Holds |
| --- | --- |
| `status` | The HTTP status code your endpoint returned. |
| `response` | The response body. |

Route on a body field with `lookup_account_result.response.<field>`, and on
the status code with `lookup_account_result.status`.

The lookup runs for up to 8 seconds, or the tool's own timeout if that is
shorter. If it takes longer than 3 seconds, the agent says a short line so the
caller is not left in silence. If your endpoint returns a status of 400 or
above, or does not answer in time, the agent tells the caller it could not
check that just now and carries on with what it can do.

## Global steps

A step with `global_when` can be reached from any step whenever that
description holds. The model decides, as with `when`. Use it for topics a
caller can raise at any point: billing questions, a request for a human, a
complaint.

A transition with `"to": "__previous__"` on a global step returns the call to
the step that was active before it, so the caller picks up where they left
off. The step resumes: its `on_enter_tool` does not run again, its `say` is
not repeated, and the agent continues it without starting over.

## Handoffs

`handoffs` lists other agents in your organization that can take over the call:

```json
[{"agent_id": 413, "when": "The caller asks about billing or a refund"}]
```

The agent gets a built-in `hand_off` tool with those agents and reasons. As
with transitions, each `when` is an internal routing note, never read out.

When it hands off, the agent says one short line telling the caller where they
are going, such as "Sure, I'll send you over to our billing team", in its own
voice. Then the target agent's prompt, steps, tools and voice take over and
the call continues with the same transcript. The new agent introduces itself
briefly and answers the caller's last request; it does not announce a
transfer. The target's knowledge base is available from then on.

The first time a call reaches an agent, it starts in that agent's
`start_step`: the step's `on_enter_tool` runs and its `say` is spoken. When
the call is handed back to an agent it already visited, that agent resumes the
step it was on, without running `on_enter_tool`, repeating its `say` or
introducing itself again. Collected values are shared by every agent on the
call.

- A target must be an agent in the same organization, and an agent cannot hand
  off to itself.
- Everything call-level stays with the agent that answered: product,
  languages, who speaks first, voicemail, consent announcement and every other
  call setting.
- Targets can hand off further, and back. One call can reach up to 8 agents.
- A test call runs the draft of the agent you test. The agents it hands off to
  run their deployed versions.
- If two agents have a tool with the same name and different definitions, only
  one is kept for the call, and the answering agent's always wins. The builder
  warns you when you save.
- An agent can have handoffs without steps. Its whole prompt is used as-is.

`go_to_step` and `hand_off` are reserved on agents with steps or handoffs, so
your own tools cannot use those names.

## Where to see it on a call

Open the call in **Call History**. A step timeline next to the transcript shows
each step the call entered and why, and the collected values are listed with
it.

The [call object](/api-reference/calls#retrieve-a-call) and the call-complete
webhook ([`telephony.complete` / `web.complete`](/webhooks/call-complete)) carry
the same data as `steps_trace` and `collected_values`. Both are absent on calls
without steps.

```json
"steps_trace": [
  {"t_ms": 0, "agent": "a412", "step": "greet", "via": "start"},
  {"t_ms": 51230, "agent": "a412", "step": "verify", "via": "model"},
  {"t_ms": 98410, "agent": "a412", "step": "minor", "via": "condition"}
],
"collected_values": {"full_name": "Dana Reyes", "dob": "2010-04-02", "age": 15}
```

`agent` is `a` followed by the agent id. `via` says what moved the call:

| `via` | Meaning |
| --- | --- |
| `start` | The call began in this step. |
| `model` | The model chose the step through `go_to_step`, a `when` or a `global_when`. |
| `condition` | A `condition` or `always` transition matched. |
| `on_enter` | A transition matched on a webhook step's result. |
| `handoff` | Another agent took over. |

## Where steps run

Steps run natively on **Spark**, **Bolt** and every **Storm** product, on
inbound, outbound and campaign calls, test calls, and web and widget calls.

Everywhere else the agent still works, just without the structure: it runs a
flattened prompt that merges the shared instructions and every step, in order.
That covers:

- the experimental Bolt products;
- [Realtime](/guides/use-with-openai-realtime) sessions, including sessions
  for a saved agent;
- inline configurations returned by a [dynamic call config](/guides/dynamic-call-config)
  webhook. A webhook that returns an `agent_id` uses that agent's steps.

## Limits

| Limit | Value |
| --- | --- |
| Steps per agent | 100 |
| Collected values per step | 20 |
| Handoff targets per agent | 8 |
| Agents reachable in one call | 8 |
| Size of the `steps` JSON | 200 KB |
| Step `instructions` | 20,000 characters |
| `say` line | 1,000 characters |
| Stored `on_enter_tool` result | 4 KB |

A save that breaks a rule returns `400` with the path of the bad field, such
as `steps.steps[2].transitions[0].to`. Tool names in `tools` and
`on_enter_tool` must be tools attached to the agent.

Per-step languages, scripted steps that skip the model, and collected values
inside say lines are not supported.

## Imported agents

[Importing](/guides/import-agents) from Retell, Bland, Vapi or ElevenLabs
brings multi-prompt agents, flows, pathways and workflows over as steps, and
squads and agent transfers over as handoffs between the imported agents. The
review page shows both before you import.

## Example: dental intake

Three steps in order: greet, collect the patient's details, book.

```json
{
  "prompt": "You are Ava, the front desk assistant at Acme Dental. Be warm and brief. Ask one question at a time.",
  "steps": {
    "start_step": "greet",
    "steps": [
      {
        "id": "greet",
        "name": "Greeting",
        "instructions": "Greet the caller and ask how you can help.",
        "say": "Thanks for calling Acme Dental, this is Ava. How can I help you today?",
        "tools": null,
        "collect": [],
        "transitions": [
          {"to": "details", "when": "The caller wants to book an appointment"}
        ]
      },
      {
        "id": "details",
        "name": "Patient details",
        "instructions": "Ask for the caller's full name, date of birth and the reason for the visit. Read the date of birth back and ask the caller to confirm it.",
        "tools": null,
        "collect": [
          {"name": "full_name", "type": "string", "description": "Caller's full name", "required": true},
          {"name": "dob", "type": "date", "description": "Date of birth", "required": true},
          {"name": "reason", "type": "string", "description": "Reason for the visit", "required": true},
          {"name": "new_patient", "type": "boolean", "description": "First visit to the practice", "required": false}
        ],
        "transitions": [
          {"to": "book", "when": "The caller has confirmed their details"}
        ]
      },
      {
        "id": "book",
        "name": "Book",
        "instructions": "Offer the two earliest open slots from book_slot and book the one the caller picks. Confirm the date and time, then say goodbye and end the call.",
        "tools": ["book_slot"],
        "collect": [],
        "transitions": []
      }
    ]
  }
}
```

Stage it with `PATCH /v1/agents/{agent_id}`, try it with **Talk**, then deploy.
The `details` step cannot hand over to booking until the name, date of birth
and reason are collected; `new_patient` is optional.

## Example: an age-gated branch

The model can talk to anyone, but code decides who gets booked. Collect the
age, then route on it before the model's own choice is considered.

```json
{
  "id": "verify",
  "name": "Verify age",
  "instructions": "Ask for the caller's age.",
  "tools": null,
  "collect": [
    {"name": "age", "type": "number", "description": "Caller's age in years", "required": true}
  ],
  "transitions": [
    {"to": "guardian", "condition": {"value": "age", "op": "lt", "target": 18}},
    {"to": "book", "when": "The caller has given their age"}
  ]
}
```

A caller who says fifteen goes to `guardian`, whatever the model decides.
Everyone else goes to `book`. Both are ordinary steps elsewhere in the graph.

## Example: account lookup webhook step

The caller gives an account number, the lookup runs on entering the next step,
and its result picks the route. The `not_found`, `collections` and `support`
steps are left out for brevity.

```json
[
  {
    "id": "identify",
    "name": "Identify",
    "instructions": "Ask for the caller's account number and read it back digit by digit.",
    "tools": null,
    "collect": [
      {"name": "account_number", "type": "string", "description": "Account number", "required": true}
    ],
    "transitions": [
      {"to": "lookup", "when": "The caller has confirmed their account number"}
    ]
  },
  {
    "id": "lookup",
    "name": "Look up account",
    "instructions": "Tell the caller what you found on their account and ask how you can help.",
    "tools": ["lookup_account"],
    "collect": [],
    "on_enter_tool": "lookup_account",
    "transitions": [
      {"to": "not_found", "condition": {"value": "lookup_account_result.response.found", "op": "eq", "target": false}},
      {"to": "collections", "condition": {"value": "lookup_account_result.response.account_status", "op": "in", "target": ["past_due", "suspended"]}},
      {"to": "support", "when": "The caller needs help with an active account"}
    ]
  }
]
```

`lookup_account` receives `account_number` because its parameter has that
name. If the response body has `"found": false`, the call goes to
`not_found`. If its `account_status` is `past_due` or `suspended`, it goes to
`collections`. An active account stays in `lookup`, where the model helps and
moves on to `support`. To send a failed lookup to its own step, add
conditions on the status first: an error response stores its status code, so
`{"value": "lookup_account_result.status", "op": "gte", "target": 400}`
catches it, and a timeout or no answer stores `"error"`, so
`{"value": "lookup_account_result.status", "op": "eq", "target": "error"}`
catches that.

## Example: front desk to billing

Two agents. The front desk (agent `412`) answers every call and hands billing
questions to a billing agent (agent `413`) with its own prompt, tools and voice.

On the front desk:

```json
{
  "handoffs": [
    {"agent_id": 413, "when": "The caller asks about a bill, a payment or a refund"}
  ]
}
```

On the billing agent, so it can hand the caller back:

```json
{
  "handoffs": [
    {"agent_id": 412, "when": "The billing question is resolved and the caller needs something else"}
  ]
}
```

The front desk says a short line such as "Let me get you to our billing
team", and the billing agent introduces itself and picks up the conversation
where the front desk left it. When billing hands back, the front desk resumes
where it was, without greeting the caller again. The call's product,
languages, consent announcement and other call settings stay with the front
desk, because it answered.

If billing only needs its own instructions and not its own agent, use a global
step on the front desk instead. When the question is answered, the call goes
back to whatever step the caller was in:

```json
{
  "id": "billing",
  "name": "Billing questions",
  "instructions": "Answer questions about bills and payments. Offer to send a payment link by email.",
  "tools": null,
  "collect": [],
  "transitions": [
    {"to": "__previous__", "when": "The billing question is answered"}
  ],
  "global_when": "The caller asks about a bill or payment at any point"
}
```

## Next steps

<CardGroup cols={2}>
  <Card title="Prompting guide" icon="pen" href="/guides/prompting">
    Write shared instructions and step instructions that hold up on the phone.
  </Card>
  <Card title="Build a tool integration" icon="code" href="/guides/build-tool-integration">
    Create the tools your steps and webhook steps call.
  </Card>
  <Card title="Test your agents" icon="flask" href="/guides/test-agents">
    Run the same scenarios against every branch before you deploy.
  </Card>
  <Card title="Agents API" icon="robot" href="/api-reference/agents">
    Every agent field, draft and deploy.
  </Card>
</CardGroup>
