Runtime tools
Define runtime tools directly in a dispatch request without saving them to your account. Use them for dynamic configurations, testing, multi-tenant applications, and one-off tasks.
When to use runtime tools
Use runtime tools for the following cases:
Basic example
The following example adds an external runtime tool to a flow prompt:
Set RUNTYPE_API_KEY to your Runtype API key. Replace WEATHER_API_KEY with the
name of the managed secret that stores the weather API key.
The Python SDK equivalent is:
Set RUNTYPE_API_KEY to your Runtype API key. Replace WEATHER_API_KEY with the
name of the managed secret that stores the weather API key.
The cURL equivalent is:
Replace YOUR_API_KEY with your Runtype API key.
Add runtime tools to an agent dispatch
To give a saved agent extra tools for one request, put them in agent.tools.runtimeTools on POST /v1/dispatch. The agent’s saved tools stay available:
Replace YOUR_API_KEY with your Runtype API key and YOUR_AGENT_ID with the ID of the saved agent.
POST /v1/agents/{id}/execute also accepts runtime tools in tools.runtimeTools. That route accepts the external, custom, flow, subagent, local, and search tool types. It rejects a flow tool with an inline config.flow, so reference a saved flow with config.flowId instead.
Tool search is on by default for multi-turn agents (loopConfig.maxTurns of 2 or more). When such an agent has 20 or more tools, Runtype holds the agent’s runtime tools back from the model and adds a tool_search tool. The model searches, and the tools that it finds load on the next turn. Saved tools, built-in tools, and MCP server tools are always sent. To send every runtime tool on every turn, set agent.tools.toolSearch to { "enabled": false }. To keep specific runtime tools loaded, list their names in agent.tools.toolSearch.alwaysLoaded. For the full behavior, see Tool search.
Tool types
Runtime tools support the following types.
External tools
Call an HTTP API with an external tool:
Replace API_TOKEN with the name of the managed secret that stores the API token.
An external tool’s config accepts the following fields:
For a POST example with a body, see Receive binary results from other tools.
Receive binary results from other tools
When a tool returns binary media, such as a browser screenshot or a generated image, Runtype stores the bytes and adds a runtype-asset:// handle to the tool result. The model passes the handle to the next tool as a plain string instead of re-typing the base64 content.
To receive those bytes in an external tool, declare contentEncoding: 'base64' on the parameter. Runtype resolves a handle in that parameter to the stored bytes, as base64, before the request is dispatched:
A parameter without contentEncoding receives the handle string unchanged. Handles are scoped to your organization and expire after 7 days. A handle that is missing, expired, or from another organization fails that tool call with an error. The same rule applies to MCP server tools that declare the keyword in their input schema, and to the built-in store_asset.content, publish_page.content, and send_email.attachments[].content parameters.
Custom tools
Run code in a sandbox with a custom tool:
A custom tool’s config requires code. The value that the code returns is the tool result. The optional timeout is in milliseconds, with a maximum of 30000 (30 seconds) for runtime custom tools. Saved custom tools have a different range, described in Creating custom tools.
Network access and managed secrets
Custom tool code has no network access unless config.networkAccess turns it on. Set it to { allowedHostnames: [...] } to allow specific hosts, or to 'on' to allow any public host. Omitting it, or setting 'off', blocks every request.
To send a managed secret, declare it in config.secrets by its bare name. Your code never receives the value: it reads a placeholder token from secrets.NAME, and the egress proxy replaces the placeholder with the real value only on requests to the declared host and path. networkAccess must allow that host.
An API key that dispatches a runtime tool with secrets needs the SECRETS:READ permission. {{secret:NAME}} is never allowed in custom code. These settings work with the cloudflare-worker (default) and runtype-sandbox providers. For every field, see Call an API from custom code.
Flow tools
Run another Runtype flow as a tool. The tool returns the value that the flow’s final executed step wrote to its output variable.
Replace YOUR_FLOW_ID with the ID of the flow that you want to run.
A flow tool’s config accepts the following fields:
flowId: the ID of a saved flow. You can reference a saved flow tool withtoolIdinstead.flow: an inline flow definition with a non-emptystepsarray. Only an agent dispatch throughPOST /v1/dispatchaccepts it. A flow dispatch andPOST /v1/agents/{id}/executereject it, so reference a saved flow withflowIdthere.parameterMapping: renames tool parameters before the flow runs. Each key is a tool parameter name, and each value is the flow input name that receives it. Unmapped parameters pass through with their own names.outputMapping: a dot path into the flow’s output value.outputVariable: a flow variable to return instead of the final step’s output.
Subagent tools
Delegate a self-contained task to a focused child agent. The child runs in its own context window and can’t see the parent conversation. It returns only its final answer. Runtype intersects the child’s allowedTools with the parent’s resolved tools.
Replace YOUR_AGENT_ID with the ID of the saved agent. To define the child inline, replace agentId with an agent object and keep only one of these fields.
A subagent tool’s config follows these rules:
- Set exactly one of
agentIdoragent. - An inline
agentrequiresallowedTools. Set it to[]to grant the child no tools. maxTurnsmust be a positive integer.outputFormatistext,json, orlast_message.
To let an agent decide what to delegate at runtime, add subagentConfig to its
tools configuration. Runtype synthesizes a spawn_subagent tool. The model can
use it to choose the task, tools from toolPool, and an optional system prompt:
subagentConfig also accepts defaultMaxTurns, defaultModel, and defaultTimeoutMs. maxSpawnsPerRun accepts 1 to 50. maxTurnsLimit and defaultMaxTurns accept 1 to 100. defaultTimeoutMs accepts 1,000 to 600,000 milliseconds.
Attached subagents use the parent’s approval policy when the parent explicitly
sets tools.approval.require, including false or [] to require no approvals.
Otherwise, the child’s own policy applies. This rule is the same for in-process
and durable execution. Detached subagents use their own approval policy.
The parent stream shows one tool bubble for each subagent call. The child’s internal turns and
tool calls don’t appear in the parent stream. If a child tool requires approval, the parent
pauses with an approval_start event. The event includes a subagent field that identifies the
parent subagent tool and the child agent’s name. The toolName and parameters fields identify
the inner tool that needs approval. Each subagent call counts as one tool call against the
parent’s maxToolCalls, and the child’s cost counts toward the parent’s total.
Local tools
Run a local tool in your client instead of on Runtype. Set toolType to local.
The dispatch pauses and emits await, then your client runs the handler and
resumes the run. A local tool doesn’t require a config object:
A local tool and a client tool both run in the caller and resume the same way. Declare a local tool in runtimeTools when it belongs to a flow step or agent tool configuration. Send client tools in the top-level clientTools field to add caller-run tools per request without changing the flow or agent.
Pass secrets
Store sensitive values in Settings > Secrets. Then use a managed reference in an external tool’s URL, headers, or body:
Replace API_TOKEN with the name of the managed secret that stores the API token.
Secret references follow these rules:
- Only
externaltools can reference a secret. A{{secret:NAME}}reference in any other tool type returns HTTP400. - The API key that sends the request needs the
SECRETS:READorSECRETS:*permission. Without it, the request returns HTTP403.
Runtype doesn’t support a top-level dispatch secrets map. A non-empty map returns HTTP 400 with RUNTIME_AGENT_TRANSIENT_SECRETS_UNSUPPORTED on an agent dispatch, or RUNTIME_FLOW_TRANSIENT_SECRETS_UNSUPPORTED on a flow dispatch. An empty map is accepted and does nothing. Use {{secret:NAME}} references for credentials instead. Runtype encrypts managed values at rest and doesn’t log them or return them in API responses. For more information, see Managing secrets.
Variable substitution
Tool templates resolve against the tool call’s arguments and the managed secret store. The following table lists the template forms:
Execution variables such as {{_record.field}}, {{_flow.id}}, and {{_user.id}} do not resolve in a tool’s URL, headers, or body. Runtype sends an unresolved template as literal text. To pass an execution variable, list its name in hiddenParameterNames, as described in Hide parameters from the model.
The following tool configuration fills the URL from an argument, the authorization header from a secret, and a tenant header from a hidden parameter:
Replace API_TOKEN with the name of the managed secret that stores the API token. For the fields each tool type templates and how unresolved values behave, see Tool template variables.
Combine with saved tools
Combine runtime tools with saved tools in one tools configuration:
Replace YOUR_TOOL_ID with the ID of the saved tool.
SDK helper functions
Use the TypeScript SDK helper function to create an external tool:
Replace WEATHER_API_KEY with the name of the managed secret that stores the
weather API key.
Client-side tools (WebMCP)
Runtime tools run on Runtype servers. Runtype calls the external URL, runs the
sandboxed code, or invokes the nested flow. Client tools run in your client,
such as a browser page or an SDK process. Define them per request in the top-level
clientTools field. The field works with POST /v1/dispatch and
POST /v1/agents/:id/execute, with the same admission rules. When the model calls
a client tool, Runtype pauses the run and streams a unified await event. Run the
tool locally, then resume the run with the result. For the pause event details, see
Pause and resume on an agent dispatch.
Each entry has a name, description, parametersSchema, and an origin:
origin: 'sdk': a tool that your SDK process executes. Runtype accepts it automatically.origin: 'webmcp': a tool that a browser page registers through WebMCP (document.modelContext). On the API-key paths (/v1/dispatchand/v1/agents/:id/execute), Runtype accepts WebMCP tools by default. Include a tool inclientToolsto opt in.
Reference a Runtype MCP catalog tool
When the tool is one Runtype’s own MCP catalog already defines, send a reference instead of a full manifest:
Runtype resolves description and parametersSchema from its own copy of the
catalog, so you don’t send (or keep in sync) either one. A name the catalog does
not define returns a 400 rather than being dropped. The resolved entry is admitted
as origin: 'webmcp' and goes through the same allowlist, naming, and duplicate
rules as any other entry.
References and full manifests mix freely in one array:
The client tool size limits in Limits apply to manifests you send. A reference carries no description or schema of its own, so it isn’t measured against them.
Set untrustedContentHint: true when a tool’s output contains untrusted
third-party data. Runtype wraps that result in a nonce-delimited envelope with the
UNTRUSTED_TOOL_OUTPUT marker before the model reads it. WebMCP output receives
this treatment without the hint. Set the hint for SDK tools whose results can
contain user-supplied or externally fetched content.
Client tools merge into the model’s tool set with the precedence
saved < runtimeTools < clientTools. A later turn can override a saved tool with
the same name without editing the flow.
The API-key /v1/dispatch examples use the lower-level path. Use the Persona
widget for browser-embedded chat. Use direct dispatch when you build a non-Persona
chat UI, run local tools from an SDK or native process, or proxy browser tool calls
through a server that stores the Runtype API key. Don’t expose a Runtype API key in
a browser page that you don’t control.
The following TypeScript example runs a local tool through the SDK:
Replace YOUR_FLOW_ID with the ID of the flow that you want to run. Set
RUNTYPE_API_KEY to your Runtype API key.
The following request registers a browser tool for an API-key dispatch:
Replace YOUR_API_KEY with your Runtype API key and YOUR_AGENT_ID with the ID of
the saved agent.
Restrict which client tools are admitted
Runtype accepts every submitted client tool by default. To narrow the accepted
origin: 'webmcp' tools, pass clientToolsPolicy with an allowlist of glob
patterns. Use an exact name or a name with one trailing *. The allowlist doesn’t
gate SDK-origin tools.
Configure the policy as follows:
The policy accepts search_products and drops delete_account. Omit
clientToolsPolicy to admit every WebMCP tool. Replace YOUR_AGENT_ID with the ID
of the saved agent.
clientToolsPolicy applies to API-key callers on both POST /v1/dispatch and
POST /v1/agents/:id/execute. It applies across the request and matches bare tool
names. It doesn’t provide an origin-scoped surface policy. Persona and other
client-token chat surfaces use behavior.webmcp.allowlist instead.
Persona chat surfaces and WebMCP
Persona chat widgets use the public client-token path (/v1/client/chat), so the
chat surface’s behavior.webmcp policy controls WebMCP admission. Enable page-tool
discovery with webmcp: { enabled: true } in the Persona configuration.
Client Chat can pause for a browser-side client tool and resume that same execution through POST /v1/client/resume. This isn’t a human tool-approval channel. If the selected Agent, Flow, nested
Flow, saved tool, or subagent can reach a human approval gate, POST /v1/client/chat rejects the
request before execution with HTTP 501 and code APPROVAL_MODE_UNSUPPORTED. Use an
authenticated dispatch or dashboard execution host when a person must approve a server-side tool.
The one exception is a root Runtype Agent gate that sets tools.approval.approver to end-user.
See Let the chat visitor approve a tool.
Let the chat visitor approve a tool
By default, only the Agent’s owner can approve a gated tool. Ask one question before you change that: does this gate ask for the visitor’s consent, or enforce your policy? If the gate confirms what the visitor wants (for example, placing a pickup order the visitor pays for), the visitor is the right person to answer it. If it enforces your policy or budget (refunds, discounts, credits, payouts, or spend on your account), keep it an owner gate, because a visitor must not approve their own exception. Let the visitor approve a consent gate in the widget:
The run pauses with an approval_start event that carries the tool name and its exact parameters. Your chat client shows them with Approve and Deny and sends the decision to POST /v1/client/approve with { sessionId, executionId, approvalId, decision }; the run then continues in the same conversation. Persona 4.24.0 and earlier do not call this route yet, so enable an end-user gate only with a client that does. Runtype enforces the following rules:
- Only the browser session that started the turn can answer, and only once.
- The approval covers the reviewed parameters only. If the arguments change, the visitor is asked again.
- Visitors can’t create Always allow grants.
- A gate on a nested Flow, subagent, or Skill still returns
APPROVAL_MODE_UNSUPPORTED. Claude Managed Agents keep refusing all gates on Client Chat. - Your owner approval routes (
POST /v1/dispatch/approve) still answer any pause.
Enable WebMCP in the Persona configuration as follows:
Replace YOUR_CLIENT_TOKEN with the client token for the chat surface.
Set the WebMCP policy on the chat surface as follows:
On a Persona surface, the following rules apply:
- The embedding page registers tools with
document.modelContext.registerTool(toolDefinition). - Persona snapshots those tools per turn and forwards them as
clientTools[]withorigin: 'webmcp'. - Runtype applies a server-owned
webmcp:prefix before the model sees the tool. behavior.webmcp.enabledmust be true, and anyallowlistrules must match the validated request origin and the bare tool name.- The client token’s
allowedOriginsremains the enforced CORS boundary for which browser origins may call the surface. - Persona also has SDK-owned client tools that require no page registration:
expose
ask_user_questionwithfeatures.askUserQuestion.expose: trueorsuggest_replieswithsuggestions.followUps.expose: true(features.suggestReplies.exposeis a deprecated alias that still works).
The dashboard WebMCP tab shows page tools and origins observed on the chat surface over the trailing 7 days. Use the tab to promote a discovered tool to an origin-scoped allowlist rule.
Pause and resume on an agent dispatch
When you dispatch with an agent or flow payload, the execution uses these
pause events:
- A client-tool pause emits
await. - A tool-approval pause emits
approval_start. - A question for the person emits
awaitwith anawaitReasonand anelicitationpayload. An MCP server usesmcp_elicitation. An external A2A agent usesa2a_input_requiredora2a_auth_required.
Use the fields in the await event to run the tool and resume the execution:
Replace YOUR_EXECUTION_ID, YOUR_TOOL_ID, and YOUR_TOOL_CALL_ID with the
values from the event.
A tool-approval pause uses approval_start. When a child agent calls the gated
tool, the event also carries a subagent object. The toolName and parameters
fields describe the inner tool that needs approval. The subagent.toolName field
names the parent subagent tool:
Replace the placeholders with the IDs from the event. Use dana@example.com only
as an example recipient.
Values injected for protected parameters appear as [REDACTED] in approval
payloads. Display the placeholder as protected context; approving or denying
the call doesn’t require resubmitting those values.
Approve an approval pause through the approval endpoint. For a client-tool pause,
run the tool locally, then post its result to /v1/dispatch/resume:
Replace YOUR_API_KEY, YOUR_EXECUTION_ID, YOUR_TOOL_CALL_ID, and
YOUR_TOOL_RESULT with values from your request and the tool result.
Use these fields when you resume a client-tool pause:
executionId: the value from theawaitevent. It identifies the paused turn.toolOutputs: an object keyed by the per-calltoolCallIdfrom the event. You can also key a single call by tool name.
The resumed stream continues with events such as text_delta and ends with
execution_complete or execution_error, or pauses with another await. That
await can identify an unanswered call from the current batch or a new call.
A resume of a saved-agent run can return { "executionId": "...", "status": "running" }
or a detached await instead of the continued stream. Follow the run with
GET /v1/executions/{executionId}/events?after=, as described in
Detached streams.
Partial batches and resume conflicts
When an agent pauses with multiple client-tool calls, you can submit results for
only the calls that have finished. Runtype retains accepted answers and re-emits
the outstanding calls without requesting another model response. It resumes the
model only after every call in that batch has an answer. Match each result to its
exact toolCallId, even when several calls use the same tool name; don’t resend
already accepted answers or treat unknown call IDs as completed work.
Serialize resume submissions for a flow execution. Concurrent resumes of the same
flow execution return HTTP 409. Check the response body’s code:
CONTINUATION_IN_PROGRESS: another request owns this continuation. Wait for that request’s response or stream. If it fails before advancing the checkpoint, you can retry the still-pending calls; don’t blindly retry while it is running.RESUME_STATE_CHANGED: the submitted pause was replaced or already consumed. Don’t replay its tool results. Use the latestawaitevent or JSON paused response from the winning continuation and answer only its outstanding calls.- HTTP
404means no resumable checkpoint remains, including a completed or expired execution. Replaying old results can’t restart it.
These checks also apply when streamResponse is true: a rejected admission is
an HTTP error, not a successful resumed stream. Resume isn’t a general
idempotency key for side effects; keep the original tool result while recovering
rather than rerunning the local action automatically.
For client-token authentication, use the browser resume endpoint instead of sending a secret API key from a page.
Elicitation pauses: when the question is for the human
Not every await event represents a tool that you run. An MCP server can request
input during a tools/call. An external A2A
agent can return
input-required or auth-required. In both cases, the peer asks the person,
not your process. These cases use the same await event. Distinguish them with
awaitReason:
Replace YOUR_EXECUTION_ID, YOUR_TASK_ID, and YOUR_CONTEXT_ID with the values
from the event.
Use these fields to handle the elicitation:
elicitation.messageis peer-controlled, display-only text. Show it to the person, but don’t branch your logic on its content. TreatawaitReasonas UX context, not a control signal.elicitation.modeisform(answer with text) orurl(send the human toelicitation.urlto authenticate or consent, then continue). A2A pauses useform. MCP servers can useurl.externalAgentcarries the peer’s conversation handles (contextId/taskId) so you can correlate the paused run with the external agent’s conversation. You don’t need to send these handles in the resume request. The resume uses the saved conversation checkpoint.
Resume an MCP-server elicitation
An MCP-server elicitation (awaitReason: "mcp_elicitation") resumes through
POST /v1/dispatch/resume, not through a re-dispatch. Key toolOutputs by the
toolCallId from the await event, the same key a client-tool resume uses, and
send the person’s answer as an mcpElicitation entry:
Replace YOUR_API_KEY, YOUR_EXECUTION_ID, and YOUR_TOOL_CALL_ID with your API
key and the identifiers from the await event. Replace YOUR_FIELD and
YOUR_VALUE with a property from elicitation.requestedSchema and the person’s
answer.
The entry differs from a client-tool result in three ways:
kindmust be the literal stringmcpElicitation. The value marks the entry as a person’s answer. A plain{ "result": ... }entry is consumed as the tool’s output instead, which sends the wrong value to the model.response.actionisaccept,decline, orcancel. Sendcontentonly withaccept, and match its properties toelicitation.requestedSchemafrom theawaitevent. Adeclineorcancelresponse carries nocontent.pauseCountechoeselicitation.pauseCountfrom theawaitevent. Runtype retains the checkpoint’s cumulative count; a smaller submitted count can’t reset the per-call cap.
Form acceptance requires an object that validates against the saved request
schema. Runtype doesn’t coerce values or insert defaults, and rejects
unsupported schema constraints rather than ignoring them. Invalid answers do
not advance the paused invocation. For URL acceptance, send action: "accept"
without content: this acknowledges the out-of-band interaction, and the MCP
server must confirm its outcome when the call is re-issued. It doesn’t grant
tool approval or certify successful authentication.
Runtype re-issues the paused MCP call with the answer as input to that call. The
answer doesn’t become the tool’s result, so the model sees whatever the
re-issued call returns. The server can elicit again on the re-issued call, which
pauses the run once more with a new await event. Answer each pause the same
way, keyed by the toolCallId of the event you received.
One tool call can pause for input at most three times without making progress. After that, Runtype declines the elicitation and fails the tool call with a message that asks the person to complete the requested action out of band and try again.
Resume an A2A elicitation
An external agent elicitation (awaitReason: "a2a_input_required" or
"a2a_auth_required") resumes by re-dispatching the agent with the person’s
answer as the message and resumeFrom naming the paused execution. Don’t send
toolOutputs because no local tool runs:
Replace YOUR_API_KEY, YOUR_AGENT_ID, and YOUR_EXECUTION_ID with your API key,
the saved agent ID, and the paused execution ID.
The continuation sends your answer to the external agent on the same
conversation and streams the rest of the run. Checkpoints expire after 7 days,
and a peer that pauses without assigning a contextId can’t continue the saved
conversation. The await event still lets your UI display the question. A
re-dispatch starts a fresh conversation.
Pause and resume without streaming
You can handle a pause without consuming the SSE stream. Set
streamResponse: false when you dispatch through POST /v1/agents/:id/execute
or POST /v1/dispatch with an agent payload. A paused run returns a JSON
envelope with status: "paused" and a pausedReason object:
Replace the YOUR_* values with identifiers from the paused execution.
The API sets success to true for a resumable run. Branch on
status === "paused" or stopReason === "paused", not on success.
Protected values in pausedReason.parameters also appear as [REDACTED].
Resolve a paused run according to the pausedReason fields:
-
Approval gate (
awaitReason: "approval_required", anapprovalIdis present): submit the decision toPOST /v1/agents/:id/approvewith theexecutionIdandapprovalIdfrompausedReason:cURLReplace
YOUR_AGENT_ID,YOUR_API_KEY,YOUR_EXECUTION_ID, andYOUR_APPROVAL_IDwith the values for the paused run. -
Local/client tool call (no
awaitReason/approvalId): run the tool yourself. Then resume with the result throughPOST /v1/dispatch/resume, using theexecutionIdfrompausedReasonand the tool call:cURLReplace
YOUR_API_KEY,YOUR_EXECUTION_ID,YOUR_TOOL_CALL_ID, andYOUR_TOOL_RESULTwith values from the paused run and the local tool. -
A question for the human (
awaitReason: "a2a_input_required","a2a_auth_required", or"mcp_elicitation"):pausedReason.elicitationcarries the peer’s question. For an external agent, the envelope also carriesexternalAgentwith the conversation handles at the top level, alongsidestatus, not insidepausedReason:Non-streaming paused response (external agent asked a question)Replace the
YOUR_*values with identifiers from the paused response.Resume an external agent (
a2a_input_requiredora2a_auth_required) by re-dispatching with the person’s answer andresumeFrom, as described in Resume an A2A elicitation. Resume an MCP server (mcp_elicitation) with anmcpElicitationentry onPOST /v1/dispatch/resumeinstead, as described in Resume an MCP-server elicitation.
Set streamResponse: false on the approval or resume request to receive the
continuation as JSON instead of an SSE stream.
Resume from a browser (client-token path)
With an API key, complete a paused tool call by posting its result to
/v1/dispatch/resume. That route requires a secret API key with DISPATCH:*
scope. A browser page, including the embedded Persona widget, can’t use it.
Browsers authenticate with a client token through the /v1/client/* routes.
To complete a paused local-tool turn, post the result to /v1/client/resume:
Replace YOUR_SESSION_ID, YOUR_EXECUTION_ID, YOUR_TOOL_CALL_ID, and
YOUR_TOOL_RESULT with values from the client session, paused event, and local
tool result.
Use these fields to resume the browser session:
sessionId: the session from/v1/client/init. It authenticates the request with an active session, an active client token, and a matchingOrigin.executionId: the value from theawaitevent. It scopes the resume to the session’s user. A client token can resume only that user’s executions.toolOutputs: an object keyed by the per-calltoolCallIdfrom theawaitevent. You can also key a single call by tool name.assistantMessageId: an optional ID, up to 128 characters, for the assistant message your page renders from this resumed leg. Sending an ID you already minted lets a later re-send deduplicate by ID instead of text.
A resume doesn’t count as a new execution. The turn was counted when it started.
For agent batches and flow conflict recovery, follow Partial batches and resume conflicts. The same per-call identity rules apply to this client-token endpoint.
What the resumed leg produces is persisted to the conversation. Runtype assembles
the leg’s assistant text from the stream and appends it as an assistant message,
and it resolves the paused tool calls the conversation already holds from the
toolOutputs you submit. The completion is stored whether or not you send
assistantMessageId; without that field, it is stored under a server-minted ID.
Don’t resend the clientTools definitions on resume unless the page tool set
changes. Runtype carries the definitions from the originating /v1/client/chat
dispatch forward unchanged.
Client tool results in later turns
A client tool call that the browser answers through /v1/client/resume stays in
the conversation. On every later /v1/client/chat turn, Runtype replays that call
and its result to the model, so the agent can answer a follow-up question from the
earlier result without calling the tool again.
- Only calls the browser answered are replayed. A call that paused and was never answered is dropped, and results from server-executed tools aren’t replayed.
- A call replays only while the page still offers that tool in
clientTools. If a later page no longer registers the tool, earlier calls to it are left out. - WebMCP results, and results from tools with
untrustedContentHint, keep their untrusted-content markers on replay. Each replayed result is identical on every turn, so the conversation prefix stays in the prompt cache. - Runtype doesn’t refresh a replayed result. If the underlying data can change, the agent must call the tool again to get current data.
Refresh page tools mid-run
If a paused page tool navigates, the destination page registers its own WebMCP
tools on document.modelContext. The dispatch-time snapshot doesn’t include
those tools. To make them callable on the next model turn, send the page’s current
registry with the resume. Use the same send-once protocol as /v1/client/chat:
- Send the full registry: Include
clientToolswith the page’s complete registry and includeclientToolsFingerprintfor the set. - Send only the fingerprint: If the registry is unchanged since the last full
send, send only
clientToolsFingerprint. If it doesn’t match the stored registry, the request returns409 { "error": "client_tools_resend_required" }. Retry with the fullclientToolsarray and the fingerprint.
Replace YOUR_SESSION_ID, YOUR_EXECUTION_ID, YOUR_TOOL_CALL_ID, and
YOUR_CLIENT_TOOLS_FINGERPRINT with the values for the active client session,
paused event, tool call, and current registry.
The refreshed set replaces the run’s persisted tool set for the rest of the
run. It doesn’t merge with the saved set, so a dispatch-time tool that you omit
is no longer callable. Runtype revalidates and regates the set against the
surface’s behavior.webmcp policy for the request Origin, as it does during
dispatch.
The Persona chat widget sends the /v1/client/resume request in client-token mode. You implement
only the local tools. This endpoint is documented for custom client-token integrations.
Detached streams
A pause isn’t the only reason an agent stream ends early. The socket an agent
turn streams over carries a watch lease, not the turn’s lifetime. When the
lease expires, Runtype emits an await event with awaitReason: "detached" and
closes the stream. The run is still going server-side. Treat it as “still
working”, never as an error and never as a finished turn:
To keep receiving events, reopen the run through its events route with the last
SSE id: you saw as the cursor. The server replays strictly after that cursor,
so you get no duplicates:
Replace YOUR_AGENT_ID, YOUR_EXECUTION_ID, and YOUR_LAST_EVENT_ID with the
agent, the executionId from the detached frame, and the last SSE id: value
your client received.
The TypeScript SDK does this for you. client.agents.executeStream() follows a
detached turn automatically and keeps yielding frames until a real terminal
arrives, so executeWithCallbacks() inherits the behavior. The local-tool loop
gets it on both legs: its first stream comes from executeStream(), and each
resume after a local tool is followed the same way, so a lease that expires
mid-continuation never ends the session early. The detach frame is still
forwarded, so a UI that renders await states keeps rendering it:
This applies to agent streams from client.agents.executeStream() and POST /v1/agents/:id/execute. Flow dispatch streams don’t detach. Raw HTTP callers and the Python SDK
reattach manually with the ?after= request in this section.
For a run that you started through POST /v1/dispatch with a saved agent, or any
run whose agent ID you don’t have, reconnect by execution ID with
GET /v1/executions/{executionId}/events?after=. It replays and tails events the
same way. An execution of an inline agent definition returns HTTP 404 on this
route.
Control stream lifetime
An agent turn keeps running after its stream closes. The exception is a short
turn that carries no conversationId, no tools, skills, or sandbox, no client
tools, no approval gate, and no Prefer: respond-async header. Runtype runs that turn in the request, so it answers
sooner but can’t detach or reconnect with ?after=. To control the stream, set
durability in the POST /v1/agents/{id}/execute request body:
watchLeaseMs sets how long the stream stays attached before the turn detaches,
from 10 seconds to 30 minutes. The default is 10 minutes. A longer lease keeps
one stream open for longer and doesn’t change how long the run can take.
Set durability.forced to "in_process" to run any turn in the request, with
the same trade-off. Setting forced to "durable" doesn’t guarantee a turn that
can detach. A request value outranks the agent’s saved durability.forced
setting.
Tool replay after an interrupted turn
An agent turn can be interrupted while a tool call is in progress. When the turn resumes, Runtype knows that the call might have started but not whether it finished.
Runtype doesn’t silently re-run an interrupted call. If the model calls that tool again on the resumed turn, it receives an error that says the effects are unknown and to verify before retrying, and Runtype doesn’t run the tool. The model decides what to do next: check whether the write landed, then retry or move on.
Re-running a read-only tool has no side effects. To allow Runtype to run an
interrupted call again, set idempotent: true on a runtime tool in a
POST /v1/dispatch request or a saved agent. Every tool type accepts this field.
The tools.runtimeTools override on POST /v1/agents/{id}/execute drops it:
Leave it unset for anything that writes, sends, charges, or creates. Built-in
catalog tools carry the flag already, derived from what the tool does: reads and
searches are re-dispatched, and writes, sends and metered job-creating calls are
not. MCP tools follow the server’s own idempotentHint / readOnlyHint
annotations, and a server that sends neither is treated as not safe to re-run.
Require a committed intent before a tool runs
Before an Agent calls a server-side tool, Runtype records an intent: a durable note that the call is about to start. By default the intent is best-effort. If Runtype can’t record it, the tool still runs.
Set tools.intent to mandatory on an Agent so that a tool that isn’t idempotent runs only after its intent is recorded:
tools.intent accepts mandatory or best-effort. The default is best-effort.
With mandatory, if Runtype can’t record the intent, it doesn’t call the tool. The tool call fails, and the model receives an error that starts with TOOL_INTENT_NOT_COMMITTED:
The call had no effect, so the model can call the tool again.
The setting has these limits:
- It applies to Agents only. A flow prompt step doesn’t record tool intents, so the flow validator warns with
TOOL_INTENT_POLICY_AGENT_ONLYwhen a prompt step sets it. - It applies only where the run keeps a tool journal, which durable Agent runs do. A run without a tool journal calls the tool as it would without the setting.
- Tools marked
idempotentkeep the best-effort intent, because calling them again is safe. - A dispatch request can set
tools.intenttomandatoryfor a saved Agent. A request can’t lower a saved Agent’smandatorytobest-effort.
Each intent also records whether the tool was idempotent when the call started. When a run resumes after an interruption, Runtype calls an interrupted tool again only if it was idempotent then and is still idempotent now. Marking a tool idempotent later doesn’t make an interrupted call run again. Removing idempotent stops an interrupted call from running again.
Tool approval grants
When you enable tool approval, a gated tool call pauses the run for a person to approve or deny. If the approval prompt offers Always allow, the person can select it instead of Allow once. The selection creates a grant, so future dispatches skip the approval prompt for that tool.
A grant stores an Always allow decision for the owner, agent, and approving end user. An account-level grant applies to every end user and appears as All users in the dashboard.
For an agent with a Tenancy Strategy, the end-user key is the durable projected end-user ID (eu_YOUR_END_USER_ID). Without a Tenancy Strategy, the key is the raw endUser.id that you pass at dispatch. The lookup matches both forms for compatibility. If you remove an agent’s Tenancy Strategy, grants recorded with the strategy no longer match, so the person approves the tool again. Scoped memory and records use the same re-scoping behavior.
A grant skips only the approval prompt, not authorization. Tool resolution, ownership scoping, and secret access are unchanged. Runtype doesn’t remember skill-load approvals, so loading a skill prompts for approval.
Create a grant
Select Always allow in an approval prompt to create a grant.
With the raw API, pass remember: true when you resolve the approval. The authenticated dashboard
execution UI sets this field when the person selects Always allow. The public Client Chat path
doesn’t host human approval prompts.
Replace YOUR_AGENT_ID, YOUR_API_KEY, YOUR_EXECUTION_ID, and
YOUR_APPROVAL_ID with the values for the paused run.
To offer the choice, enable tools.approval.choices.alwaysAllow in the saved
agent configuration or set it per dispatch. See Dispatch-time approval overrides.
List grants
List active grants for the authenticated owner. Pass agentId to filter the results
to one agent.
Replace YOUR_AGENT_ID with the ID of the agent to filter. Set
RUNTYPE_API_KEY to your Runtype API key.
The following request lists grants for one agent:
Replace YOUR_AGENT_ID with the agent ID and YOUR_API_KEY with your Runtype API
key.
The response is { "data": [] }. Each grant has these fields:
Revoke a grant
Revoke a grant to prompt for approval again on future dispatches. The API returns
{ "revoked": true }.
Replace YOUR_GRANT_ID with the ID of the grant to revoke.
Replace YOUR_GRANT_ID with the ID of the grant and YOUR_API_KEY with your
Runtype API key.
You can also view and revoke grants from the dashboard in the agent editor Safety section, under Remembered approvals.
Dispatch-time approval overrides
When you dispatch an agent through POST /v1/dispatch, agent.tools.approval
overrides the saved approval configuration for that run. It accepts the following
fields:
require: tools that need approval. Set it totruefor all tools, or to an array of tool names and patterns such as["send_email", "mcp:*"].requestReason: whether the prompt asks the model for a reason for the call.choices: choices that the prompt offers. Setchoices.alwaysAllow: trueto show Always allow for this dispatch.
Configure an approval override as follows:
Replace YOUR_AGENT_ID with the ID of the saved agent.
Hide parameters from the model
Set hiddenParameterNames on a runtime tool to fill parameters from execution variables instead of from the model. Runtype removes each listed parameter from the model-facing schema and sets it to the execution variable with the same name. The resolved value replaces any value the model sent.
The following tool hides _tenant and sends its ID in a header:
Replace ORDERS_API_KEY with the name of the managed secret that authenticates Runtype to your API. Runtype merges hidden values into the tool arguments before it fills the URL, headers, and body.
The name decides how Runtype resolves a hidden parameter:
- No leading
_. Runtype reads the execution variable, then the same field in_record.metadata, then the same field inside another object-valued variable without a leading_. If nothing matches, the parameter stays visible and the model supplies it. - Leading
_. Runtype reads only the top-level execution variable. If it isn’t set, Runtype doesn’t call the tool and returns an error to the model as the tool result. - Leading
_internal. Runtype never resolves these names and rejects them when you save an agent or send them on an agent execute request.
Use _tenant and _endUser for identity. Runtype sets them from the execution’s end-user identity, as described in End-user identity and credentials. Other names with a leading _ don’t get that protection, so also authenticate Runtype to your backend with a {{secret:NAME}} header.
To let your API verify the identity instead of trusting a plain ID, reference {{_identity.token}} in the URL, a header, or the body. Runtype signs a short-lived assertion for each call. For the claims and how to verify them, see Send a verifiable identity assertion to your API.
To grade such an agent in an eval suite, give each case an endUser (or set the suite’s identityDefaults). For details, see Grade an agent that runs as an end user.
In tool_start and tool_input_complete events, Runtype replaces resolved hidden values with [REDACTED] and lists the names in hiddenParameterNames. The field is accepted on runtime tools in /v1/dispatch, on saved agents, and on the tools.runtimeTools override of an agent execute request. For the dashboard view of this feature, see Runtime tools.
Ask for input before dispatch
Saved and inline external and custom tools can declare
config.elicit: { message, requestedSchema, merge: 'parameters' }. The final
parametersSchema and requestedSchema must be closed objects. Requested fields
belong to the person: their primitive types, constraints, and requiredness must
match the final schema. Supported field types are string, number, integer, and
boolean, with titles, descriptions, enums, string length bounds, and numeric
bounds. Runtype rejects references, unsupported keywords, cross-field
constraints, reserved names, and overlap with hidden parameters.
The model sees only the remaining parameters. Runtype validates those arguments, collects the person’s fields, validates the merged object, then requests any ordinary approval for the final arguments. An approval resume reuses the validated input. If you change the tool definition or the model arguments, the invocation must restart. A cancel or decline skips the tool call and returns a cancellation result for the original call.
Tool elicitation works on AG-UI surfaces for top-level agent calls authenticated
with a credential other than a client token, such as a Surface key. It doesn’t work in subagent calls
or other nested executions, in Claude Managed Agents, or in Persona. Server tool
arguments stay private unless you list the tool in the AG-UI surface’s
behavior.discloseToolArguments.tools, for example ['schedule_meeting']. That
setting exposes the model’s arguments in interrupt metadata and tool-call events.
Runtype still removes hidden values before it stores or discloses them.
The pause emits an await event with awaitReason: 'tool_elicitation' and a
form elicitation payload. Resume follows the normal authorization and routing
rules for the surface. MCP elicitation keeps its own awaitReason and resume
behavior.
Limits
The following limits apply to runtime and client tools:
Runtype doesn’t limit the number of runtime tools or client tools in a request.
It limits their size instead. Runtype checks the runtimeTools size when you
dispatch, run an eval, validate a Flow, and create, update, or execute an Agent.
AG-UI frontend tools follow the client tool limits. A frontend tool set that is
too large, or that contains an invalid tool, returns a 400 error.
A local in-browser model accepts at most 50 tools. This limit comes from the local inference protocol.
A model provider can still reject a long tool list. For example, OpenAI accepts
at most 128 tools. Runtype reports that rejection with the error code
TOOL_COUNT_REFUSED_BY_PROVIDER. To run a large tool set, turn on
tool search for a multi-turn Agent.
Tool search sends the model a small set of tools and a tool_search tool, and
loads other runtime tools as the model finds them.
For MCP tool call timeouts, see MCP servers.
Troubleshooting
Runtype validates runtime tools before the run starts. An invalid tool returns an
HTTP error with an error message, and nothing runs. The following table lists
common causes:
Before Runtype checks for a reserved name, it lowercases the tool name and
replaces each non-alphanumeric character with _. A name such as
Runtype-Set-State matches the reserved runtype_set_state name.
Best practices
Apply these practices when you configure runtime tools:
Save frequently used tools
If you use the same runtime tool repeatedly, save it to your account through the API or dashboard to keep the configuration in one place.
Test before production
Use runtime tools to test configurations, then save working tools for production use.
Validate schemas
Check that parametersSchema is valid JSON Schema with type: "object" at the top level. Runtype rejects an invalid tool before the run starts.
Next steps
Continue with one of these resources: