Agents
Steps
Steps let an agent move through distinct phases of a session while preserving the full conversation history. When the agent transitions from one step to the next, the history carries over but the system prompt effectively changes.
Most agents do not need steps. A single step with well-written instructions handles the overwhelming majority of use cases. Reach for steps when the agent genuinely moves through phases that benefit from different prompts — for example, triage → research → draft → review.
If you want a fresh context rather than a prompt change, use sub-agents instead. Steps preserve history; sub-agents start fresh.
Define steps on the agent
Steps live in the steps map on the agent, keyed by step name. The
agent entry point is named via first_step_name.
Agent with multiple steps
Code example with json syntax.1
Each step carries its own instructions, reminders,
allowed_tools, and allowed_skills — the same building blocks as a
single-step agent, scoped to just that step. Leaving allowed_tools
unset exposes every tool; an empty list exposes none.
Two fields are specific to multi-step agents: next_steps controls
transitions (covered below), and reentry_step picks which step the
session resumes at when a new user message arrives on a later turn.
Without reentry_step, re-entry happens at whichever step the prior
turn ended on — usually fine, but set it explicitly when you want every
new user turn to start from (say) a triage step.
An output_parser of structured tells the step to emit JSON instead
of free-form text. This pairs especially well with next_steps: a
classifier step emits { "intent": "sales" } and a downstream step
routes on get('$.output.intent') without string parsing. See
Structured outputs for the schema
format.
Tool calls a step makes
A step can make a fixed list of tool calls each time the session
enters it, before anything else in the step runs. Use them to fetch
data the step needs, or to run a chain of tools without an LLM turn,
such as analyzing an uploaded file and indexing it. Like
session enrichment, the LLM never
sees these calls or their outputs. They are recorded as tool_input
and tool_output events, and next_steps conditions and later calls
read their outputs at $.tools.<tool_config_name>.outputs.latest.
Each such tool_input event carries a step_tool_call field naming
the step and the call's position in its tool_calls.
Steps that run tool calls
Code example with json syntax.1
Step types
type | What the step does |
|---|---|
conversational (default) | Runs its tool calls, then the LLM handles the input following the step's instructions. next_steps are evaluated after the LLM's turn. |
tool_calls | Runs its tool calls, then evaluates next_steps. The LLM is not called. |
A tool_calls step cannot set instructions, a structured
output_parser, reminders, or allowed_skills,
because the LLM never reads them. Creating or updating an agent with
one of them on a tool_calls step fails with 400.
When the calls run
A step's calls run once each time the session enters the step:
- when the session transitions into the step, through
next_steps, anon_failureroute, an input that names the step as its entry step, or another step'sreentry_step, or - when the session starts at the step, after its first input arrives,
so the calls can read
$.inputs.user.latest.
A new input that finds the session already at the step does not run
the calls again. It evaluates next_steps again, and when none
matches, the turn ends without a response. A workflow of tool_calls
steps is meant to end, so when the conversation should continue after
it, route its last step to a step without tool calls. To run a step's
calls on every user turn, set the
reentry_step of the step that answers the user to the step with the
calls.
Calls start in the order listed. A call whose arguments reference an
earlier call's output with a $ref to
tools.<tool_config_name>.outputs.latest waits for that call to
finish. Calls without such a reference run in parallel.
Each call names one of the agent's tool_configurations. Its
arguments are literals or $ref objects, resolved against the same
context as an argument_override, and a $ref can set $default as a
fallback. Because no LLM fills in arguments, the call's arguments
together with the parameters its tool configuration fixes must cover
every required parameter of the tool. Creating or updating an agent
fails with 400 when a call:
- names a tool that is not in
tool_configurations, isenrichment_only, or is not permitted by the step'sallowed_tools, - sets an argument its tool configuration already fixes in
argument_override, - references a tool that does not exist, or one the step calls only at or after this call, or
- names an
on_failurestep that does not exist.
When a call fails
A failed call is recorded as its tool_output, with the failure
message in its error field. The call's on_failure decides what
happens next:
on_failure.type | What happens |
|---|---|
end_turn (default) | Once the step's calls finish, the turn ends with an error event and next_steps are not evaluated. The session stays at the step, and its next input does not run the step's calls again. |
continue | The step carries on as if the call had succeeded. A later call that references this call's output uses the reference's $default, and fails if the reference has none. |
next_step | Once the step's calls finish, the session transitions to step_name in the same turn. The transition tells the LLM which call failed and why, and the failure step can route on $.tools.<tool_config_name>.outputs.latest.error. Naming the current step runs all of its calls again. |
When several of a step's calls fail, the first recorded failure whose
on_failure is not continue decides. A failure that a client
reports for a client tool the step called
counts the same way.
Retrying through next_step counts toward the
transition limit for the turn. Route to a step that checks
the failure, rather than straight back to the failing step, when you
want to stop after a set number of attempts.
Transitions
The next_steps array on a step is evaluated after the step's LLM
turn completes, or, in a tool_calls step, once its tool calls
finish. Each entry has a step_name and an optional
condition; the first matching condition wins, and a next_steps
entry with no condition is the catch-all. If nothing matches, the
agent ends the turn in the current step.
Conditions are UserFn expressions
using the get() function with JSONPath. UserFn is the same
expression language used by
UDF rerankers
and pipeline verification
— the language reference covers operators, if (cond) a else b,
and the built-in time/math functions. The step-transition context
exposes six top-level keys:
{
"agent": { ... },
"session": { ... },
"currentDate": "2024-01-15T10:35:00Z",
"inputs": { "user": { "latest": { "text": "...", "artifacts": [ ... ] } } },
"tools": { "<tool_config_name>": { "outputs": { "latest": { } } } },
"output": { ... }
}
Agent and session metadata you set elsewhere is reachable under
$.agent.metadata.* and $.session.metadata.*. $.currentDate is the
evaluation time in ISO 8601. The most recent call
of any tool the step used lives at $.tools.<name>.outputs.latest.
The session's most recent user input lives at $.inputs.user.latest:
its text, absent when the input has none, and an artifacts array of
the files uploaded or shared with it, each with artifact_id,
filename, mime_type, size_bytes, and purpose when the sharer
gave one. Before the first input, $.inputs.user.latest is absent.
$.output is the LLM output of the current step — a text field
under the default parser, or your JSON fields directly under $.output
with a structured parser.
get('$.output.intent') == 'sales'
get('$.session.metadata.tier') == 'enterprise' and get('$.output.urgent') == true
get('$.inputs.user.latest.artifacts[0].mime_type') == 'application/pdf'
Two get() behaviors to design around:
get()returns scalars only — a string, number, boolean, or null. Pointing it at an object or array (get('$.tools.approval.outputs.latest')) errors and the transition fails for that turn. Read a specific field, not the wrapping object. If a tool legitimately returns nested data, expose the routing value at the top level of the tool's output.- A missing path returns null, not an error. If the agent never
called the tool a condition reads from,
get('$.tools.approval.outputs.latest.decision')is null. A comparison likeget(...) == 'approved'cleanly evaluates tofalse, so a catch-allnext_stepsentry handles the "never called" case. There is no separate signal for "called this turn" versus "called earlier in the session"; if you need to distinguish, have the tool always return an explicit value (such as"pending") and test the string rather than null.
The key under $.tools is the tool configuration name from the
agent's tool_configurations map — not the underlying tool ID.
next_steps are evaluated once, after the step's final text turn (or
after its tool calls in a tool_calls step), not between tool calls
within a step.
Session history across steps
When the agent transitions between steps, the session history is preserved in full. The new step sees all prior user messages, agent outputs, tool calls, and tool outputs — it just applies a different system prompt on top of them. This is the main thing that distinguishes steps from sub-agents, which run with a fresh history.
When to reach for steps
Typical uses:
- Classification and routing. A
classifierstep reads the user input, decides which specialist step should handle the rest, and transitions. - Phase-structured workflows. An investigation that genuinely has distinct gather → analyze → report phases, where each phase benefits from different guidance and tool access.
- Gated escalation. A
supportstep that transitions to anescalate_to_humanstep once a condition is met. - Plan-then-execute. See the dedicated section below.
Many workflows do not require steps at all. A single-step agent with the right combination of skills, tools, and instructions can handle several task types without any explicit routing logic — the LLM picks the right tool and loads the right skill based on the user's request. Reach for steps when phases need different tool access and the transition between them must be driven by a condition you define, not a decision the model makes on the fly.
Plan-then-execute
A plan step with read-only tools produces a structured plan and
writes it to an artifact via artifact_create. An execute step with
write access calls artifact_read to load the plan and carries it
out. Front-loading the thinking keeps the implementation turns focused
and leaves a durable plan the agent can re-read on later turns. This
is the same pattern Claude Code's plan mode uses.
{
"first_step_name": "plan",
"steps": {
"plan": {
"instructions": [{ "type": "inline", "template": "Produce a plan. Do not act. Call artifact_create with the plan." }],
"allowed_tools": ["artifact_create", "search"],
"next_steps": [{ "step_name": "execute" }]
},
"execute": {
"instructions": [{ "type": "inline", "template": "Call artifact_read to load the plan, then carry it out." }],
"allowed_tools": ["artifact_read", "write_file"]
}
}
}
When not to use steps:
- If the phases differ only in tone or style, a single step with good instructions is simpler.
- If you want isolated context for a side task, use a sub-agent.
- If you just need to remind the agent of something as the session grows, use a reminder.
Approval gates
An approval gate is a step that must complete before the agent can reach the tools on a later step. Because each step has its own allowed_tools list, tools on a later step are unreachable until the agent transitions through the gate. This happens only when the gate's next_steps condition is satisfied. It is more reliable than an instruction such as "ask before doing X": the model cannot skip a step boundary.
What the gate step can do
Choose an approach that produces the right approval signal for your
situation. Prefer routing on a tool's return value over routing on the
model's own output: the model can hallucinate a {"confirmed": true}
emit, but it cannot fabricate the return value of a tool it actually
called.
Hand off to an approval tool. Attach a tool that talks to the approval system — a Wolken lookup, a ServiceNow workflow, a custom webhook, or a lambda tool wrapping an internal endpoint. Advance the step when that tool returns an approved decision. This is the recommended pattern.
Ask the user in the chat. The gate step's instructions tell the
agent to summarize what is about to happen and require the user to
reply CONFIRM, then emit a structured {"confirmed": true} so the
next_steps condition can test a typed field. Even with this gate,
the model can confuse itself and emit confirmed: true without a real
CONFIRM, so reserve this pattern for low-stakes flows. The escalated
tools are still unreachable from the gate step, so the worst case is a
spurious transition, not a spurious tool call.
Build approval into the tool. For actions that must be gated regardless of agent behavior, push the approval check into the tool itself. The tool pauses, notifies the appropriate person, and only executes once approved. This is custom scoping work for specific high-risk tools.
Gate on tool output
Register the approval tool under a stable key in the agent's
tool_configurations map — that key is what the next_steps
condition reads from $.tools.<name>.outputs.latest. The gate step
exposes only the approval tool; downstream provisioning tools live on
the next step and are unreachable until the condition fires.
Design the tool to return a top-level scalar field for the routing
decision (an enum string like "approved" | "rejected" | "needs_review"
beats a boolean — it leaves room for a third state without breaking
the schema). Then branch on it:
{
"first_step_name": "request_approval",
"steps": {
"request_approval": {
"instructions": [{ "type": "inline", "template": "Summarize the request and call the approval tool. Do not respond to the user until you have a decision." }],
"allowed_tools": ["approval"],
"next_steps": [
{ "condition": "get('$.tools.approval.outputs.latest.decision') == 'approved'", "step_name": "provision" },
{ "condition": "get('$.tools.approval.outputs.latest.decision') == 'rejected'", "step_name": "notify_rejection" },
{ "step_name": "ask_for_info" }
]
},
"provision": {
"instructions": [{ "type": "inline", "template": "Execute the approved request." }],
"allowed_tools": ["create_ticket", "send_notification"]
},
"notify_rejection": {
"instructions": [{ "type": "inline", "template": "Tell the user the request was declined and cite get('$.tools.approval.outputs.latest.reason')." }]
},
"ask_for_info": {
"instructions": [{ "type": "inline", "template": "Ask the user for whatever information the approval tool needs." }]
}
}
}
The catch-all ask_for_info entry covers both "needs_review" and
the case where the agent never called the tool — both look like
"decision is not 'approved' and not 'rejected'" to the condition.
latest is the most recent call across the whole session, not just
the current step. If a later step re-asks the same tool, the gate's
condition will read the newer result. Give the per-step tool a
distinct tool_config_name (for example approval_followup) if you
need to route on each call independently.
Async external approval (fire and forward)
For approval flows where a human acts in an external system (for example, an approvals Slack channel), the intake session ends after the gate step posts the request. There is no in-session next_steps transition waiting for approval. Instead, a separate handler agent receives the approval event through a connector and acts on it. In this pattern the gate step has no next_steps at all — its purpose is to post the notification and tell the user their request has been submitted.
See Integrations for how to set up a Slack connector that feeds replies into a handler agent.
Notifications
Notifications are tool calls like any other: attach a tool that posts to the target surface (a webhook, a ticket comment), and lock the destination in argument_override so the LLM cannot change it. Instruct the agent when to call it, typically on request submission, on approval, and on completion.
For time-based escalation (following up if no approval arrives within a set window), use the agent's Schedules tab to trigger a follow-up invocation after a configured interval.
Limits
A single session turn can transition between steps at most 500
times before the platform stops execution with a
step_transition_limit_exceeded event. In practice this only triggers
when two steps ping-pong at each other via mis-written conditions — a
real workflow rarely crosses a dozen transitions. Treat the ceiling as
a loop-breaker, not a budget to spend.
Transitions run serially; an agent is never in two steps at once. Use sub-agents when you need parallel work.
Related
- Context engineering overview
- Sub-agents — the alternative when you want a fresh history instead of a new prompt.
- Instructions · Reminders · Skills — all are step-scoped.