Skip to navigation

Route a busy message automatically

Use auto when you don’t know whether a message sent mid-run belongs to the work in progress. While a durable native agent turn is running, a classifier compares the running request with the incoming message and picks one route:

  • Steer: The message joins the running execution at the next safe boundary, like x-runtype-concurrency: steer. Corrections, constraints, extra facts, and changes of approach usually steer.
  • Queue: The message runs as its own turn after the current one finishes, like x-runtype-concurrency: queue. A clearly independent task usually queues.

When nothing is running, an auto message is an ordinary turn. Runtype makes no classifier call and charges nothing for routing. auto is opt-in. The default admission policy stays reject.

Before you begin

auto routes only where steering works. You need the following:

  • A saved native Runtype agent that runs durable turns.
  • A conversationId on every send.
  • New user messages only, not the full transcript.

Send a message with auto routing

From a server, send x-runtype-concurrency: auto on POST /v1/dispatch or POST /v1/agents/{agentId}/execute. The body has the same shape as a steer: one or more new user messages for the same agent and conversation. Use a different idempotency key for each logical message.

To send a message with the TypeScript SDK and read where it went, use this example:

TypeScript SDK
import { RuntypeClient } from '@runtypelabs/sdk'
const client = new RuntypeClient({ apiKey: process.env.RUNTYPE_API_KEY! })
const result = await client.dispatch.execute(
{
agent: { agentId: 'agent_YOUR_AGENT_ID' },
conversationId: 'conversation-123',
messages: [{ role: 'user', content: 'Use the metric units instead.' }],
options: { streamResponse: false },
},
{ concurrency: 'auto', idempotencyKey: 'user-message-43' }
)
console.log(result.executionId, result.busyMessageRoute)

Replace agent_YOUR_AGENT_ID with your saved native agent’s ID.

The Python example uses a dedicated client, so its admission headers apply only to this message:

Python SDK
import os
from runtype import RuntypeClient
with RuntypeClient(
api_key=os.environ["RUNTYPE_API_KEY"],
headers={
"x-runtype-concurrency": "auto",
"Idempotency-Key": "user-message-43",
},
) as client:
result = client.dispatch(
{
"agent": {"agentId": "agent_YOUR_AGENT_ID"},
"conversationId": "conversation-123",
"messages": [
{"role": "user", "content": "Use the metric units instead."}
],
},
stream=False,
)
print(result["executionId"], result.get("busyMessageRoute"))

To send the same message with cURL and print the response headers, use this request:

cURL
curl -i https://api.runtype.com/v1/dispatch \
-H "Authorization: Bearer $RUNTYPE_API_KEY" \
-H 'Content-Type: application/json' \
-H 'x-runtype-concurrency: auto' \
-H 'Idempotency-Key: user-message-43' \
-d '{
"agent": { "agentId": "agent_YOUR_AGENT_ID" },
"conversationId": "conversation-123",
"messages": [
{ "role": "user", "content": "Use the metric units instead." }
],
"options": { "streamResponse": false }
}'

Replace agent_YOUR_AGENT_ID with your saved native agent’s ID, and set RUNTYPE_API_KEY to your Runtype API key. Don’t send Prefer: respond-async with auto: an asynchronous request keeps its 202 handle and isn’t routed.

Send a message with auto routing from a browser

Browser clients send submitMode: "auto" on POST /v1/client/chat. The setup and the request shape are the same as for submitMode: "steer": enable durable turns, send exactly one user message, and set that message’s id to the request’s turnId. For the full browser setup, see Steer from a browser client.

The following example sends one message with auto routing:

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: 'auto',
messages: [{ id: turnId, role: 'user', content: 'Use the metric units instead.' }],
}),
})
const route = response.headers.get('X-Runtype-Busy-Message-Route')

While a turn runs, a steered message returns 202 with the steer delivery receipt. A queued message returns 202 with the queued execution’s handle. When nothing is running, the message starts a turn and streams its answer. Retry an uncertain send with the same turn ID and message ID.

Read the routing decision

When a turn is running, or auto cannot route on this request, the answer reports where the message went. An auto message that arrives while nothing is running is an ordinary turn and carries no report. The following table lists the response headers:

HeaderValuePresent
X-Runtype-Busy-Message-Routesteer or queueWhenever a report is present
X-Runtype-Busy-Message-SourceWhy this route applied (see the following list)Whenever a report is present
X-Runtype-Busy-Message-ProbabilityProbability that the message belongs to the running turnOnly when the source is classifier
X-Runtype-Busy-Message-ModelThe classifier model that answeredOnly when the source is classifier

The source takes one of these values:

  • classifier: The classifier answered, and its answer chose the route.
  • timeout: The classifier didn’t answer in time. The message queued.
  • error: The classifier call failed. The message queued.
  • unanswered: The classifier returned no usable answer. The message queued.
  • abstained: The decision model declined to answer. The message queued.
  • circuit_open: Runtype skipped the classifier after repeated failures. The message queued.
  • disabled: Routing didn’t run, because the agent’s mode is off or the request can’t be routed.

A JSON response body repeats the decision as a busyMessageRoute object with route, source, and, for a classifier answer, probability and model. Streamed responses carry the headers only. The following excerpt shows a steered message’s receipt, with its other fields omitted:

{
"executionId": "aex_01m1yr0hf1e65vpnb0zp8qj8jm",
"busyMessageRoute": {
"route": "steer",
"source": "classifier",
"probability": 0.91,
"model": "typesafe-ai/jev"
}
}

The object also carries steerUnavailable: true when the classifier chose steer but the running turn could no longer accept input. In that case the message queued instead, and route is queue. If the running turn finished while the classifier was deciding, the message runs as the next turn. Neither route loses the message.

Configure routing on the agent

The agent’s config.busyMessageRouting object controls the classifier. Every field is optional:

  • mode: How Runtype applies the decision.
    • enforced (default): The decision routes the message.
    • off: Every busy auto message queues without a classifier call. The answer reports source disabled.
  • classifierModel: The decision model that classifies the message. Use an id from GET /v1/decision-models. Runtype rejects any other value when you save the agent. It defaults to Jev (typesafe-ai/jev). A model that needs your own provider key, such as OpenAI Decisions, fails without one, and those messages queue with source error.

The following agent configuration routes with Clef Flash:

{
"busyMessageRouting": {
"mode": "enforced",
"classifierModel": "@cf/cloudflare/clef-flash"
}
}

Runtype bills each classifier call to your organization, in the same way as other model usage. A message sent while nothing is running makes no call.

Fallbacks and limits

Review these behaviors before you rely on auto routing:

The classifier has 3 seconds to answer, including one quick retry after a provider server error. A timeout or an error queues the message, because queueing never changes work that’s already running.

After three timeouts or provider server errors in a row for one classifier model on one credential, Runtype stops calling that model for 60 seconds. Models on Runtype’s platform credentials share one count across organizations, because a provider outage affects all of them. Your own provider key has its own count. Credential and spending errors never count. During that time, busy auto messages queue at once with source circuit_open. After the pause, Runtype lets one call through. An answer resumes normal routing, and another failure starts a new pause.

A conversation holds one waiting message. A second auto message sent while the first is still being classified, or is already queued, returns 409 with code CONVERSATION_QUEUE_FULL. Retry it after the waiting message starts.

auto never returns a 4xx error because a request can’t be routed. It keeps the lane’s own behavior and reports source disabled instead.

On the server API, the following requests keep the admission they would get without the x-runtype-concurrency header:

  • A request that runs in process, or that targets a Claude Managed agent.
  • A request that runs a flow.
  • A request without a conversationId.
  • A request with history: "stored".
  • A request with x-runtype-coalesce: true.
  • A request with Prefer: respond-async. It still gets its 202 handle.

A server API request whose messages can’t steer, such as a full transcript, queues when the conversation is busy.

On POST /v1/client/chat, a request that can’t be routed runs as submitMode: "normal". That includes a surface without durable turns and a request that isn’t one user message whose id equals turnId.

Next steps