Steer an active agent execution
Use x-runtype-concurrency: steer to send another message while your saved
agent is working. If the conversation has an active execution, Runtype adds the
input to that execution. If the conversation is idle, the message starts an
execution.
Choose the admission policy that matches your application:
The default policy is reject. A conversation holds at most one waiting
execution, so a second queued request returns CONVERSATION_QUEUE_FULL.
Steer never cancels the active execution. The in-flight model or tool call finishes first, and the agent sees the input at the next safe boundary. Other frameworks use “steer” for different behaviors. For a mapping, see Concurrency modes across agent frameworks.
Before you begin
Steering requires the following:
- An agent that you created and saved in Runtype. Inline agent definitions and Claude Managed agents don’t support steering.
- A turn that runs durably. Don’t set
durability.forcedto"in_process"on the agent or on the request. For details, see Control stream lifetime. - A stable
conversationIdthat you send with every message in the conversation. - A Runtype API key in the
RUNTYPE_API_KEYenvironment variable. The key needsDISPATCH:*to send throughPOST /v1/dispatch, orAGENTS:EXECUTEto send throughPOST /v1/agents/{agentId}/execute. For key permissions, see Authentication.
Use POST /v1/agents/{agentId}/execute when the agent uses client tools or can
reach an approval gate. Dispatch doesn’t support those agents for steering and
returns STEER_UNSUPPORTED_LANE. Dispatch also returns that error when the
request carries dispatch-only inputs, such as secrets, inputs, or credential
proxies.
To let Runtype choose between steering a message and queueing it as the next turn, see Route a busy message automatically.
Send only additional user messages
Send the same agent, conversationId, tenant and end-user identity, API key,
and execution options on every message. Send only the new user messages, not
the full transcript or replacement system instructions. Don’t combine steer
with x-runtype-coalesce. Use a different idempotency key for each logical
message.
To send additional input asynchronously with the TypeScript SDK, use this example:
Replace agent_YOUR_AGENT_ID with your saved agent’s ID.
The Python example uses a dedicated client for this message. This keeps its idempotency header separate from other sends:
To send the same input with cURL, use this request:
To steer through the saved-agent endpoint, put the agent ID in the path and omit
the agent wrapper:
The TypeScript SDK takes the same admission options, concurrency and
idempotencyKey, as the last argument of these methods:
client.dispatch.execute(request, options)client.agents.execute(agentId, request, options)client.agents.executeAsync(agentId, request, options)client.agents.executeStream(agentId, request, init)client.dispatch.executeStream(request, init)
Track the delivery, not a separate execution
An asynchronous response includes executionId, deliveryId, deliveryStatus,
and deliveryStatusUrl. Multiple deliveries can name the same execution.
To check a delivery, send GET to the deliveryStatusUrl, which has the form
/v1/executions/{executionId}/deliveries/{deliveryId}. The API key needs
AGENTS:READ, AGENTS:EXECUTE, or DISPATCH:*. With the TypeScript SDK, use
client.executions.getDelivery(executionId, deliveryId). The response is a
delivery receipt:
The receipt names its state status. The asynchronous admission response names
the same value deliveryStatus. The outcome field appears after the execution
ends. A 404 response means the receipt is unknown or has expired.
The initial field is true when this delivery started its execution. It’s
false when the delivery joined an execution that was already starting or
running. To confirm that a steer landed on the execution you expected, compare
executionId values instead of timestamps.
Use the delivery state to tell acceptance apart from use:
pending: Runtype accepted the input, but the agent hasn’t seen it yet.applied: The input is part of the agent’s context. This doesn’t promise that the model follows the message.settled: The execution ended. The receipt carries the execution’soutcome, including failure or cancellation.not_applied: The execution ended before the agent saw the input. Runtype doesn’t restart it automatically.
To recover a delivery receipt, retry the same message with the same idempotency
key. If you reuse that key with different messages or options, Runtype returns
DELIVERY_IDEMPOTENCY_CONFLICT. Runtype keeps receipts for a limited time. They
aren’t a permanent transcript archive.
Without Prefer: respond-async, blocking and server-sent events (SSE) callers
follow the execution’s answer. SSE responses carry an X-Runtype-Delivery-Id
header and replay the execution’s event stream. To avoid displaying the same
answer twice, merge events by execution ID and event cursor.
Steer a conversation that answered busy
With the default reject policy, an overlapping send returns 409 with code
CONVERSATION_BUSY. The body names the active execution and its phase:
The phase field has one of these values:
claiming: The execution is starting. A steer sent now is added when the execution starts.running: The execution is working. A steer is added at the next safe boundary.paused: The execution is waiting for an approval, client-tool result, or other resume. A steer is added after the resume.queued: Another request is already waiting in the queue. A steer can’t overtake it and returnsCONVERSATION_QUEUE_FULL.
The response also carries a Retry-After header. To follow the active
execution, poll /v1/executions/{executionId}/status. To add your message to it
instead, send the same message again with x-runtype-concurrency: steer. The
steer’s receipt names the same executionId with initial: false.
Safe boundaries and limits
Review these constraints before accepting additional input in your application:
In-flight calls and pauses
Input follows first-in, first-out order and can’t overtake an earlier queued request. A model call or tool batch in flight finishes first, and Runtype saves completed tool results before it adds the input.
Runtype doesn’t withdraw streamed output or roll back external side effects. Approval, client-tool, and elicitation pauses remain paused. Text can’t approve a tool or replace a structured resume response.
Completion races and execution budgets
Steering doesn’t reset turn, time, cost, recovery, or pause-expiry limits. It doesn’t reserve a second execution. Several messages can share one answer.
If the execution is already finishing, later input starts or queues a
separate execution with the usual admission rules and quotas. If a queued
steer follows a failed or cancelled execution, its input becomes
not_applied. If you cancel a queued execution before it starts, Runtype
removes it and marks its input not_applied. It never runs later.
Input sent while the execution is starting becomes not_applied if that
execution never starts, for example because another request supersedes it.
Message size and conversation history
Each delivery accepts one to 16 user messages and at most 64 KB. Runtype limits how many deliveries can wait on one conversation.
A steer uses the conversation’s earlier successful history. A very long conversation can exceed the history that Runtype keeps. In that case, start a new conversation.
Steer from a browser client
Browser clients use a separate protocol: POST /v1/client/chat with
submitMode: "steer", not the concurrency header. Never put a Runtype API key
in browser code.
Set behavior.durableTurns.enabled to true on the chat surface and bind a
saved Runtype agent. Initialize with the surface’s public client token and
durableRecovery: true. Retain the visitor token returned by /v1/client/init.
Then initialize again with that visitor token and session ID to claim the
conversation. Only offer steering when init returns durableRecovery.steer: true.
A surface with history disabled can still support durable recovery and steering.
A client token that isn’t bound to a surface carries the same opt-in on the
token itself: set config.durableTurns.enabled to true. Each token kind reads
exactly one opt-in. A surface-bound token always reads the surface behavior, so
setting config.durableTurns.enabled on it has no effect.
Each browser send contains exactly one additional user message. Its id
must equal the request’s turnId. When you retry an uncertain send, keep that
ID and the entire payload unchanged. Mint a new ID for a genuinely new message.
Don’t resend the transcript, alter execution options, or replace structured tool
answers with chat text.
Use the apiUrl, session ID, and visitor token from your client configuration
and init flow. The browser supplies its Origin header, and the surface must
allow that origin.
Handle both successful admission shapes:
- 200 SSE: A new execution streams its answer. Retain both the
X-Runtype-Execution-IdandX-Runtype-Delivery-Idresponse headers. - 202 JSON: Retain
executionId,deliveryId,deliveryStatus,deliveryStatusUrl, andeventsUrl. When this receipt names the execution that you already follow, keep the existing stream and any open tool question. An uncertain initial send can also return this shape. If you don’t already follow that execution, attach toeventsUrl.
If steering isn’t available, the request returns 409 with code
STEER_UNAVAILABLE. This happens when the surface doesn’t have durable turns
enabled, the agent isn’t a saved Runtype agent that runs durably, or the
conversation doesn’t belong to this visitor. Fall back to a normal send, or wait
for the current reply to finish.
Read the returned delivery URL with ?sessionId=... and the same
X-Visitor-Token header. Attaching to execution events uses the same
credentials. To avoid displaying replayed events twice, retain the SSE cursor.
Browser recovery URLs are scoped under
/v1/client/conversations/{conversationId}/executions/{executionId}. A different
visitor or conversation can’t use the receipt.
Disconnecting, reloading, or closing the HTTP stream does not cancel the
execution. To stop it, send POST to
/v1/client/conversations/{conversationId}/executions/{executionId}/cancel?sessionId=...
with the visitor header. Cancellation can leave pending input not_applied.
Resubmit it only when the user sends it again. A steer never cancels the
execution, closes an elicitation, or consumes a tool’s pending resume response.
Troubleshoot steer errors
The following table lists common errors that a steer request can return:
Next steps
Use these guides to route messages, configure tools, and preserve caller scope:
- Route a busy message automatically: let a classifier choose between steer and queue.
- Concurrency modes across agent frameworks: compare Runtype’s policies with other frameworks.
- Detached streams: reconnect to a durable execution’s event stream.
- Working with tools: configure the work your agent can perform.
- End-user identity and credentials: keep end-user identity consistent across sends.
- Run flows and agents with dispatch: run a Flow or Agent through the dispatch API, with streaming and async execution.