> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.runtype.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.runtype.com/_mcp/server.

# Steer an active agent execution

> Deliver additional user input at a safe boundary without interrupting an in-flight model or tool call.

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:

| Policy      | Active conversation                 | Typical use                  |
| ----------- | ----------------------------------- | ---------------------------- |
| `reject`    | Returns a busy error.               | Avoid overlapping requests.  |
| `queue`     | Starts a later execution.           | Keep requests separate.      |
| `supersede` | Cancels and replaces the execution. | Replace the active request.  |
| `steer`     | Adds input to the active execution. | Send additional information. |

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](/developer-guides/guides/concurrency-modes-across-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.forced` to `"in_process"` on
  the agent or on the request. For details, see
  [Control stream lifetime](/developer-guides/guides/runtime-tools#control-stream-lifetime).
* A stable `conversationId` that you send with every message in the conversation.
* A Runtype API key in the `RUNTYPE_API_KEY` environment variable. The key needs
  `DISPATCH:*` to send through `POST /v1/dispatch`, or `AGENTS:EXECUTE` to send
  through `POST /v1/agents/{agentId}/execute`. For key permissions, see
  [Authentication](/developer-guides/getting-started/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](/developer-guides/guides/busy-message-auto-routing).

## 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:

**`TypeScript SDK`**

```typescript title="TypeScript SDK"
import { RuntypeClient } from '@runtypelabs/sdk'

const client = new RuntypeClient({ apiKey: process.env.RUNTYPE_API_KEY! })
const receipt = await client.dispatch.executeAsync(
  {
    agent: { agentId: 'agent_YOUR_AGENT_ID' },
    conversationId: 'conversation-123',
    messages: [{ role: 'user', content: 'Also compare the delivery times.' }],
  },
  { concurrency: 'steer', idempotencyKey: 'user-message-42' }
)
console.log(receipt.executionId, receipt.deliveryId)
```

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:

**`Python SDK`**

```python title="Python SDK"
import os
from runtype import RuntypeClient

with RuntypeClient(
  api_key=os.environ["RUNTYPE_API_KEY"],
  headers={
    "Prefer": "respond-async",
    "x-runtype-concurrency": "steer",
    "Idempotency-Key": "user-message-42",
  },
) as client:
  receipt = client.dispatch(
    {
      "agent": {"agentId": "agent_YOUR_AGENT_ID"},
      "conversationId": "conversation-123",
      "messages": [
        {"role": "user", "content": "Also compare the delivery times."}
      ],
    },
    stream=False,
  )
  print(receipt["executionId"], receipt["deliveryId"])
```

To send the same input with cURL, use this request:

**`cURL`**

```bash title="cURL"
curl https://api.runtype.com/v1/dispatch \
  -H "Authorization: Bearer $RUNTYPE_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Prefer: respond-async' \
  -H 'x-runtype-concurrency: steer' \
  -H 'Idempotency-Key: user-message-42' \
  -d '{
    "agent": { "agentId": "agent_YOUR_AGENT_ID" },
    "conversationId": "conversation-123",
    "messages": [
      { "role": "user", "content": "Also compare the delivery times." }
    ],
    "options": { "streamResponse": false }
  }'
```

To steer through the saved-agent endpoint, put the agent ID in the path and omit
the `agent` wrapper:

**`cURL: saved-agent endpoint`**

```bash title="cURL: saved-agent endpoint"
curl https://api.runtype.com/v1/agents/agent_YOUR_AGENT_ID/execute \
  -H "Authorization: Bearer $RUNTYPE_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Prefer: respond-async' \
  -H 'x-runtype-concurrency: steer' \
  -H 'Idempotency-Key: user-message-43' \
  -d '{
    "conversationId": "conversation-123",
    "messages": [
      { "role": "user", "content": "Only include next-day options." }
    ],
    "streamResponse": false
  }'
```

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:

**`Delivery receipt`**

```json title="Delivery receipt"
{
  "deliveryId": "ain_01m1yr2k7xq3f9a8b6c4d2e0gh",
  "executionId": "aex_01m1yr0hf1e65vpnb0zp8qj8jm",
  "status": "settled",
  "sequence": 2,
  "initial": false,
  "outcome": "completed"
}
```

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's
  `outcome`, 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:

```json
{
  "error": "This conversation already has a running turn. Retry after it settles.",
  "code": "CONVERSATION_BUSY",
  "executionId": "aex_01m1yr0hf1e65vpnb0zp8qj8jm",
  "phase": "claiming"
}
```

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 returns `CONVERSATION_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.

**`Browser admission`**

```typescript title="Browser admission"
const turnId = crypto.randomUUID()
const response = await fetch(`${apiUrl}/v1/client/chat`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Visitor-Token': visitorToken,
  },
  body: JSON.stringify({
    sessionId,
    turnId,
    assistantMessageId: crypto.randomUUID(),
    submitMode: 'steer',
    messages: [{ id: turnId, role: 'user', content: 'Also compare delivery times.' }],
  }),
})
```

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-Id` and `X-Runtype-Delivery-Id` response headers.
* **202 JSON:** Retain `executionId`, `deliveryId`, `deliveryStatus`,
  `deliveryStatusUrl`, and `eventsUrl`. 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 to `eventsUrl`.

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:

| Code                            | Status | Cause                                                                                               | What to do                                                                                                                                 |
| ------------------------------- | ------ | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `CONVERSATION_BUSY`             | 409    | A non-steer request overlapped an active execution.                                                 | Wait for `Retry-After`, or resend with `x-runtype-concurrency: steer`.                                                                     |
| `CONVERSATION_QUEUE_FULL`       | 409    | Another request is already waiting in the queue.                                                    | Wait for the queued execution to start or finish.                                                                                          |
| `DELIVERY_CHECKPOINT_FULL`      | 409    | The execution has no room for more input.                                                           | Wait for the execution to finish, or use a new conversation.                                                                               |
| `DELIVERY_IDEMPOTENCY_CONFLICT` | 409    | The idempotency key already names different input.                                                  | Use a new idempotency key for a new message.                                                                                               |
| `DELIVERY_INBOX_FULL`           | 409    | Too many deliveries are waiting on this conversation.                                               | Wait for pending input to settle.                                                                                                          |
| `DELIVERY_TOO_LARGE`            | 413    | The messages exceed 64 KB.                                                                          | Send less content.                                                                                                                         |
| `INVALID_CONCURRENCY_MODE`      | 400    | On dispatch, the `x-runtype-concurrency` value isn't `reject`, `supersede`, `queue`, or `steer`.    | Use one of the four supported values.                                                                                                      |
| `INVALID_STEER_ADMISSION`       | 400    | The request has no `conversationId`, doesn't name a saved agent, or also sets `x-runtype-coalesce`. | Send a `conversationId` for a saved agent, and remove `x-runtype-coalesce`.                                                                |
| `INVALID_STEER_MESSAGES`        | 400    | The request doesn't contain one to 16 `user` messages.                                              | Send only the new user messages.                                                                                                           |
| `STEER_CONFIG_MISMATCH`         | 409    | The steer's options, end-user identity, or API key differ from the active execution.                | Resend with the original values, wait for the execution to finish, or use a new conversation. Don't drop identity fields to force a match. |
| `STEER_HISTORY_UNAVAILABLE`     | 409    | The conversation's history exceeds what Runtype keeps.                                              | Start a new conversation.                                                                                                                  |
| `STEER_HOST_UNSUPPORTED`        | 409    | The active execution can't accept additional input.                                                 | Wait for the execution to finish, then send the message again.                                                                             |
| `STEER_UNSUPPORTED_LANE`        | 400    | The agent turn doesn't run durably, or dispatch can't steer this agent or request.                  | Remove `durability.forced: "in_process"` and dispatch-only inputs, or use the saved-agent execute endpoint.                                |
| `STORED_HISTORY_UNSUPPORTED`    | 400    | The request combines steer with `history: "stored"`.                                                | Remove `history: "stored"`.                                                                                                                |

## Next steps

Use these guides to route messages, configure tools, and preserve caller scope:

* [Route a busy message automatically](/developer-guides/guides/busy-message-auto-routing): let a classifier choose between steer and queue.
* [Concurrency modes across agent frameworks](/developer-guides/guides/concurrency-modes-across-frameworks): compare Runtype's policies with other frameworks.
* [Detached streams](/developer-guides/guides/runtime-tools#detached-streams): reconnect to a durable execution's event stream.
* [Working with tools](/developer-guides/guides/working-with-tools): configure the work your agent can perform.
* [End-user identity and credentials](/developer-guides/guides/end-user-identity): keep end-user identity consistent across sends.
* [Run flows and agents with dispatch](/developer-guides/guides/dispatch): run a Flow or Agent through the dispatch API, with streaming and async execution.