Runtime Tools
Runtime tools let you define tools directly in dispatch requests, without saving them to your account first. This is useful for dynamic tool configurations, testing, and multi-tenant applications.
When to Use Runtime Tools
Basic Example
Tool Types
Runtime tools support the same types as saved tools:
External Tools
Call any HTTP API:
Custom Tools (Code)
Execute JavaScript in a secure sandbox:
Flow Tools
Execute another Runtype flow as a tool:
Subagent Tools
Delegate a self-contained sub-task to a focused child agent. The child runs in its own context window — it cannot see the parent’s conversation — and returns only its final answer, which keeps the parent’s context clean for work that needs it. The child’s allowedTools are always intersected with the parent’s resolved tools, so a subagent can never use a tool the parent lacks.
To let an agent decide what to delegate at runtime, add a subagentConfig to its tools configuration. Runtype synthesizes a spawn_subagent tool the model can call, choosing the task, the tools to grant (from toolPool), and an optional system prompt:
The parent emits a single tool bubble per subagent call — the child’s internal turns and tool
calls do not appear in the parent’s stream, with one exception: if a child tool requires human
approval, the parent pauses with an approval_start event that carries a subagent field
identifying which subagent tool triggered the pause and the child agent’s name (the toolName and
parameters still describe the inner tool the human is approving). Each subagent call counts as
one tool call against the parent’s maxToolCalls, and the child’s cost rolls up into the parent’s
total.
Passing Secrets
Use the secrets field for sensitive values:
Reference in tool config: {{secrets.api_token}}
Variable Substitution
Tool configurations support template variables:
Example:
Combining with Saved Tools
Mix runtime tools with saved tools:
SDK Helper Functions
The TypeScript SDK provides helper functions:
Client-side tools (WebMCP)
Runtime tools (above) execute server-side — Runtype calls the URL, runs the
sandboxed code, or invokes the nested flow. Client tools are the opposite:
they execute in your own client (a browser page or your SDK process). You
declare them per-request in the top-level clientTools[] field — accepted on
both POST /v1/dispatch and POST /v1/agents/:id/execute (a saved agent by
id), with identical admission rules. When the model calls one, Runtype pauses
the run and streams a unified await event (see
Pause and resume on an agent dispatch
below) — you run the tool locally, and you resume with the result.
Each entry has a name, description, parametersSchema, and an origin:
origin: 'sdk'— a tool your SDK process executes. Admitted automatically.origin: 'webmcp'— a tool registered by a browser page via WebMCP (document.modelContext). On the API-key paths (/v1/dispatchand/v1/agents/:id/execute) the caller holds a secret key and controls the page, so webmcp tools are admitted by default — their presence inclientTools[]is the opt-in.
You can also set untrustedContentHint: true on an entry to declare that the
tool’s output is untrusted third-party data. The server then wraps the
result in a nonce-delimited spotlight envelope (<<UNTRUSTED_TOOL_OUTPUT …>>)
before the model reads it, providing indirect-prompt-injection protection.
origin: 'webmcp' output is spotlighted unconditionally; use this hint for
origin: 'sdk' tools whose results may relay user-supplied or externally
fetched content. Descriptions are capped at 2KB to limit the same injection
surface in the tool manifest itself.
Client tools merge into the model’s tool set with the precedence
saved < runtimeTools < clientTools, so a later turn can override a saved tool
of the same name without editing the flow.
The API-key /v1/dispatch examples below are the lower-level path. Prefer the
Persona widget for browser-embedded chat. Use direct dispatch when you are
building a non-Persona chat UI, running local tools from an SDK/native process,
or proxying browser tool calls through your own server that safely holds the
Runtype API key. Do not expose a Runtype API key in an untrusted browser page.
Restricting which client tools are admitted
By default every submitted client tool is admitted. To narrow which
origin: 'webmcp' tools are accepted, pass an optional clientToolsPolicy with
an allowlist of glob patterns (an exact name, or a single trailing *).
SDK-origin tools are never gated by the allowlist.
Here only search_products is admitted; delete_account is dropped. Omit
clientToolsPolicy entirely to admit every webmcp tool.
clientToolsPolicy applies to API-key callers on both POST /v1/dispatch and
POST /v1/agents/:id/execute. It is a
global self-restriction over bare tool names, not an origin-scoped surface
policy. Persona and other client-token chat surfaces use
behavior.webmcp.allowlist instead.
Persona chat surfaces and WebMCP
The examples above use the API-key /v1/dispatch path. Persona chat widgets use
the public client-token path (/v1/client/chat), so WebMCP admission is
controlled by the chat surface’s behavior.webmcp policy instead of only the
request payload. The widget must also enable page-tool discovery with
webmcp: { enabled: true } in Persona config.
On a Persona surface:
- The embedding page registers tools with
document.modelContext.registerTool(...). - 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_replieswithfeatures.suggestReplies.expose: true.
The dashboard WebMCP tab shows page tools and origins observed on the chat surface over the trailing 7 days. You can promote a discovered tool into an origin-scoped allowlist rule from there.
Pause and resume on an agent dispatch
When you dispatch with an agent or flow payload, every execution uses the
same unified pause vocabulary:
- A client-tool pause emits
await. - A tool-approval pause emits
approval_start.
The await event carries everything you need to run the tool and resume:
A tool-approval pause looks similar but uses approval_start. When the
gated tool was called inside a subagent’s child agent, the event also carries a
subagent object — toolName/parameters still describe the inner tool the
human is approving, while subagent.toolName names the parent subagent tool the
top-level agent invoked:
You resume an approval pause with approvedTools / deniedTools rather than
toolOutputs — the value you approve is the inner toolName. Run a client tool
locally instead, then resume from the API-key path by posting the result to
/v1/dispatch/resume (the secret-key sibling of the client-token
/v1/client/resume below):
executionIdis the value from theawait(orapproval_start) event — it addresses the paused turn.toolOutputsis keyed by the per-calltoolCallIdfrom the event (preferred — this is what makes parallel calls of the same tool addressable), or by tool name (legacy, single-call only).- The resumed stream continues with unified events such as
text_deltaand finishes withexecution_complete/execution_error(or anotherawaitif the model calls a further client tool).
Pause and resume without streaming
You do not need to consume the SSE stream to handle a pause. When you dispatch
with streamResponse: false (POST /v1/agents/:id/execute or POST /v1/dispatch
with an agent payload), a paused run returns a JSON envelope with
status: "paused" instead of a completed result. The pausedReason object
carries everything you need to resolve it:
success stays true because the run is resumable, not failed — branch on
status === "paused" (or stopReason === "paused"), not on success.
-
Approval gate (
awaitReason: "approval_required", anapprovalIdis present): submit the decision toPOST /v1/agents/:id/approvewith theexecutionIdandapprovalIdfrompausedReason:cURL -
Local/client tool call (no
awaitReason/approvalId): run the tool yourself, then resume with the result viaPOST /v1/dispatch/resume, keyed by theexecutionIdfrompausedReasonand the tool call (as with the streaming path above):cURL
Set streamResponse: false on the approve/resume call too if you want the
continuation as JSON rather than an SSE stream.
Resuming from a browser (client-token path)
The examples above use the API-key /v1/dispatch path, where you complete a
paused tool call by posting the result back to /v1/dispatch/resume. That route
requires a secret API key with DISPATCH:* scope, so a browser page (the
embedded Persona widget, or your own client-token integration) cannot use it.
Browsers authenticate with a client token (ct_live_…) against the
/v1/client/* routes instead. To complete a paused local-tool turn, resume via
/v1/client/resume — the session-authenticated sibling of
/v1/dispatch/resume:
sessionIdis the session from/v1/client/init; it authenticates the request (active session, active client token, matchingOrigin).executionIdcomes from the await event of the paused run. It scopes the resume to your session’s user — a client token can only resume its own user’s executions.toolOutputsis keyed by the per-calltoolCallIdfrom the await event (preferred — this is what makes parallel calls of the same tool addressable), or by tool name (legacy, single-call only).- Resume does not consume additional execution quota — the turn was already counted when it started.
By default you do not re-send the clientTools[] definitions on resume;
Runtype already has them from the originating /v1/client/chat dispatch and
carries that set forward unchanged.
Refreshing page tools mid-run
The exception is a paused page tool that navigated. The destination page
registers its own WebMCP tools on document.modelContext, and the run’s
dispatch-time snapshot does not include them. To make the new tools callable on
the next model turn of the same run, send the page’s current registry with the
resume, using the same send-once protocol as /v1/client/chat:
- Full send: include
clientTools[](the page’s complete current registry) plus aclientToolsFingerprintfor the set. - Fingerprint only: when the registry is unchanged since the last full
send, send just
clientToolsFingerprint. If the fingerprint does not match the stored registry, the request returns409 { "error": "client_tools_resend_required" }; retry once with the fullclientTools[]plus the fingerprint.
The refreshed set replaces the run’s persisted tool set for the rest of the
run; it never merges into it, so any dispatch-time tool you omit is no longer
callable. It is re-validated and re-gated against the surface’s
behavior.webmcp policy for the request Origin, exactly like a dispatch, so
a mid-run refresh cannot admit tools the original dispatch would have rejected.
The Persona chat widget drives this /v1/client/resume round-trip for you when it runs in
client-token mode — you only implement the local tools themselves. The endpoint is documented here
for custom client-token integrations.
Tool approval grants
When an agent has tool approval turned on, each gated tool call pauses the run and waits for a person to approve or deny it. If the agent also offers the Always allow choice, the person approving can pick Always allow instead of Allow once. That records a durable grant so future dispatches skip the approval prompt for that tool.
A grant is a remembered “Always allow” decision. It is keyed to the owner, the agent, and the end user who approved it, with an account-level fallback that applies to every end user. Account-level grants show as All users in the dashboard.
A grant skips only the approval prompt, not authorization. Tool resolution, ownership scoping, and secret access are unchanged. Skill-load approvals are never remembered, so loading a skill always prompts.
Create a grant
You do not create grants directly. A grant is written when an end user picks Always allow at an approval prompt.
On the raw API, pass remember: true when you resolve the approval. This is the field Persona sets when the user picks Always allow.
To offer the choice in the first place, the agent must enable it, either in the agent’s saved config (tools.approval.choices.alwaysAllow) or per dispatch (see agentInput.tools.approval below).
List grants
List the authenticated owner’s active grants. Pass agentId to filter to a single agent.
The response is { "data": [...] }. Each grant has these fields:
Revoke a grant
Revoke a grant so the tool prompts for approval again on future dispatches. This returns { "revoked": true }.
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, agentInput.tools.approval mirrors the agent’s saved approval config for that single run. It accepts:
require: which tools need approval (truefor all, or an array of tool names and patterns such as["send_email", "mcp:*"]).requestReason: whether to ask the model for a per-call justification.choices: the persistent choices offered at the prompt. Setchoices.alwaysAllow: trueto show the Always allow affordance for this dispatch, the same option you would otherwise turn on in the saved agent config.
Limits
API Format
The API uses camelCase for all field names:
Best Practices
Save frequently-used tools
If you use the same runtime tool repeatedly, save it to your account via the API or dashboard for cleaner code.
Use lowercase headers
Use lowercase header names (e.g., authorization not Authorization) to avoid issues with
automatic case conversion.
Test before production
Use runtime tools to test configurations, then save working tools for production use.
Validate schemas
Ensure parametersSchema is valid JSON Schema. Invalid schemas cause tool calls to fail.