Skip to navigation

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:

Use caseDescription
TestingTry tool configurations before saving.
Dynamic configsVary tool URLs or parameters per request.
User-providedAccept tool definitions from end users.
One-off tasksUse a tool for one execution.

Basic example

The following example adds an external runtime tool to a flow prompt:

TypeScript SDK
import { FlowBuilder, RuntypeClient } from '@runtypelabs/sdk'
const client = new RuntypeClient({
apiKey: process.env.RUNTYPE_API_KEY,
})
const result = await new FlowBuilder()
.createFlow({ name: 'Weather Agent' })
.prompt({
name: 'Agent',
model: 'claude-sonnet-5',
userPrompt: 'What is the weather in Tokyo?',
tools: {
runtimeTools: [
{
name: 'get_weather',
description: 'Get current weather for a city',
toolType: 'external',
parametersSchema: {
type: 'object',
properties: {
city: { type: 'string', description: 'City name' },
},
required: ['city'],
},
config: {
url: 'https://api.example.com/v1/current?city={{city}}',
method: 'GET',
headers: {
Authorization: 'Bearer {{secret:WEATHER_API_KEY}}',
},
},
},
],
},
})
.run(client, { streamResponse: true })

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:

Python SDK
import os
from runtype import RuntypeClient
client = RuntypeClient(api_key=os.environ["RUNTYPE_API_KEY"])
runtime_tool = {
"name": "get_weather",
"description": "Get current weather for a city",
"toolType": "external",
"parametersSchema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"}
},
"required": ["city"]
},
"config": {
"url": "https://api.example.com/v1/current?city={{city}}",
"method": "GET",
"headers": {
"authorization": "Bearer {{secret:WEATHER_API_KEY}}"
}
}
}
for event in client.dispatch({
"flow": {
"steps": [{
"type": "prompt",
"config": {
"model": "claude-sonnet-5",
"userPrompt": "What is the weather in Tokyo?",
"tools": {
"runtimeTools": [runtime_tool]
}
}
}]
}
}):
print(event)

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:

cURL
curl https://api.runtype.com/v1/dispatch \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"flow": {
"steps": [{
"type": "prompt",
"config": {
"model": "claude-sonnet-5",
"userPrompt": "What is the weather in Tokyo?",
"tools": {
"runtimeTools": [{
"name": "get_weather",
"description": "Get current weather for a city",
"toolType": "external",
"parametersSchema": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
},
"config": {
"url": "https://api.example.com/v1/current?city={{city}}",
"method": "GET"
}
}]
}
}
}]
}
}'

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:

cURL
curl https://api.runtype.com/v1/dispatch \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent": {
"agentId": "YOUR_AGENT_ID",
"tools": {
"runtimeTools": [{
"name": "get_weather",
"description": "Get current weather for a city",
"toolType": "external",
"parametersSchema": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
},
"config": {
"url": "https://api.example.com/v1/current?city={{city}}",
"method": "GET"
}
}]
}
},
"messages": [{"role": "user", "content": "What is the weather in Tokyo?"}]
}'

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:

External tool
{
name: 'fetch_user',
description: 'Fetch user details by ID',
toolType: 'external',
parametersSchema: {
type: 'object',
properties: {
userId: { type: 'string' }
},
required: ['userId']
},
config: {
url: 'https://api.example.com/users/{{userId}}',
method: 'GET',
headers: {
'Authorization': 'Bearer {{secret:API_TOKEN}}'
}
}
}

Replace API_TOKEN with the name of the managed secret that stores the API token.

An external tool’s config accepts the following fields:

FieldRequiredDescription
urlYesThe request URL. Supports {{param}} placeholders and must be a valid URL after substitution.
methodNoGET, POST, PUT, or DELETE. The default is POST.
headersNoRequest headers. Values support {{param}} and {{secret:NAME}}.
bodyNoA JSON template for the request body. Values support {{param}} and {{secret:NAME}} placeholders.

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:

External tool that accepts a handle
{
name: 'upload_image',
description: 'Upload an image to the asset library',
toolType: 'external',
parametersSchema: {
type: 'object',
properties: {
filename: { type: 'string' },
image: {
type: 'string',
contentEncoding: 'base64',
description: 'Image bytes, or a runtype-asset:// handle from another tool'
}
},
required: ['filename', 'image']
},
config: {
url: 'https://api.example.com/images',
method: 'POST',
headers: {
'Authorization': 'Bearer {{secret:API_TOKEN}}'
},
body: {
filename: '{{filename}}',
data: '{{image}}'
}
}
}

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:

Custom tool
{
name: 'calculate_tax',
description: 'Calculate sales tax',
toolType: 'custom',
parametersSchema: {
type: 'object',
properties: {
amount: { type: 'number' },
rate: { type: 'number' }
},
required: ['amount', 'rate']
},
config: {
code: `
const tax = amount * (rate / 100);
return {
subtotal: amount,
tax: tax.toFixed(2),
total: (amount + tax).toFixed(2)
};
`,
timeout: 5000
}
}

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.

Custom tool that calls an API with a managed secret
{
name: 'github_user',
description: 'Look up the authenticated GitHub user',
toolType: 'custom',
parametersSchema: { type: 'object', properties: {} },
config: {
code: `
const res = await fetch('https://api.github.com/user', {
headers: { Authorization: \`Bearer \${secrets.GITHUB_TOKEN}\` }
});
return await res.json();
`,
networkAccess: { allowedHostnames: ['api.github.com'] },
secrets: [
{ key: 'GITHUB_TOKEN', hostnamePattern: 'api.github.com', injectionMode: 'bearer_header' }
]
}
}

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.

Flow tool
{
name: 'analyze_sentiment',
description: 'Run sentiment analysis on text',
toolType: 'flow',
parametersSchema: {
type: 'object',
properties: {
text: { type: 'string' }
},
required: ['text']
},
config: {
flowId: 'YOUR_FLOW_ID',
parameterMapping: { text: 'input_text' },
outputMapping: 'sentiment'
}
}

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 with toolId instead.
  • flow: an inline flow definition with a non-empty steps array. Only an agent dispatch through POST /v1/dispatch accepts it. A flow dispatch and POST /v1/agents/{id}/execute reject it, so reference a saved flow with flowId there.
  • 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.

Subagent tool
{
name: 'research_topic',
description: 'Research a topic and return a summary',
toolType: 'subagent',
parametersSchema: {
type: 'object',
properties: {
task: { type: 'string', description: 'What to research' }
},
required: ['task']
},
config: {
// Set exactly one of `agentId` or `agent`.
agentId: 'YOUR_AGENT_ID',
allowedTools: ['builtin:exa', 'mcp:linear:*'],
maxTurns: 5,
timeoutMs: 300000,
outputFormat: 'text'
}
}

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 agentId or agent.
  • An inline agent requires allowedTools. Set it to [] to grant the child no tools.
  • maxTurns must be a positive integer.
  • outputFormat is text, json, or last_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:

Dynamic subagent configuration
tools: {
toolIds: ['builtin:exa', 'mcp:linear:create_issue'],
subagentConfig: {
toolPool: ['builtin:exa', 'mcp:linear:*'], // subset of parent's tools
maxSpawnsPerRun: 5, // default 5
maxTurnsLimit: 10, // hard cap, default 10
allowNesting: false // default false
}
}

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:

Local tool
{
name: 'ask_user',
description: 'Ask the end user a question',
toolType: 'local',
parametersSchema: {
type: 'object',
properties: {
question: { type: 'string' }
},
required: ['question']
}
}

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:

Managed secret reference
headers: {
Authorization: 'Bearer {{secret:API_TOKEN}}'
}

Replace API_TOKEN with the name of the managed secret that stores the API token.

Secret references follow these rules:

  • Only external tools can reference a secret. A {{secret:NAME}} reference in any other tool type returns HTTP 400.
  • The API key that sends the request needs the SECRETS:READ or SECRETS:* permission. Without it, the request returns HTTP 403.

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:

VariableSource
{{paramName}}Tool call argument
{{secret:NAME}}Managed secret store
{{hiddenName}}Execution variable listed in hiddenParameterNames
{{_tenant.id}}, {{_endUser.id}}The execution’s tenant and end user
{{_identity.token}}Runtype-signed identity assertion, minted per call

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:

Template variables
{
hiddenParameterNames: ['_tenant'],
config: {
url: 'https://api.example.com/orders/{{id}}',
headers: {
'Authorization': 'Bearer {{secret:API_TOKEN}}',
'X-Tenant-Id': '{{_tenant.id}}'
}
}
}

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:

Combined tools
tools: {
// Saved tools by ID
toolIds: [
'YOUR_TOOL_ID',
'mcp:notion:create_page'
],
// Runtime tools
runtimeTools: [
{
name: 'dynamic_tool',
description: 'Run a dynamic tool',
toolType: 'external',
parametersSchema: { type: 'object', properties: {} },
config: { url: 'https://api.example.com', method: 'GET' }
}
],
maxToolCalls: 10
}

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:

TypeScript SDK
import { createExternalTool } from '@runtypelabs/sdk'
const weatherTool = createExternalTool({
name: 'get_weather',
description: 'Get weather for a city',
parametersSchema: {
type: 'object',
properties: {
city: { type: 'string' },
},
},
url: 'https://api.example.com/current?city={{city}}',
method: 'GET',
headers: {
Authorization: 'Bearer {{secret:WEATHER_API_KEY}}',
},
})
// Use in flow
tools: {
runtimeTools: [weatherTool]
}

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/dispatch and /v1/agents/:id/execute), Runtype accepts WebMCP tools by default. Include a tool in clientTools to 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:

{ "name": "create_flow", "catalog": "runtype-mcp" }

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:

{
"clientTools": [
{ "name": "create_flow", "catalog": "runtype-mcp" },
{
"name": "highlight_row",
"description": "Highlight a row in the on-page table.",
"parametersSchema": { "type": "object", "properties": { "id": { "type": "string" } } },
"origin": "webmcp"
}
]
}

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:

TypeScript SDK
import { RuntypeClient } from '@runtypelabs/sdk'
const client = new RuntypeClient({ apiKey: process.env.RUNTYPE_API_KEY })
// scope: 'turn' snapshots your local tools into the dispatch envelope's
// clientTools[] and drives the dispatch + resume loop for you, executing
// each tool call locally.
await client.runWithLocalTools(
{
flow: { id: 'YOUR_FLOW_ID' },
messages: [{ role: 'user', content: 'What time is it?' }],
},
{
get_time: {
description: 'Return the current ISO time',
parametersSchema: { type: 'object', properties: {} },
execute: async () => new Date().toISOString(),
},
},
{ scope: 'turn' }
)

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:

cURL
curl -X POST https://api.runtype.com/v1/dispatch \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent": { "agentId": "YOUR_AGENT_ID" },
"messages": [{ "role": "user", "content": "Search the catalog" }],
"clientTools": [
{
"name": "search",
"description": "Search the catalog",
"parametersSchema": { "type": "object", "properties": {} },
"origin": "webmcp"
}
]
}'

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:

Client tool policy
{
"agent": { "agentId": "YOUR_AGENT_ID" },
"clientTools": [
{
"name": "search_products",
"description": "Search products",
"parametersSchema": { "type": "object" },
"origin": "webmcp"
},
{
"name": "delete_account",
"description": "Delete the account",
"parametersSchema": { "type": "object" },
"origin": "webmcp"
}
],
"clientToolsPolicy": { "allowlist": ["search_*"] }
}

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:

Agent config
{
"tools": {
"approval": {
"require": ["place_pickup_order"],
"approver": "end-user"
}
}
}

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:

Persona widget config
initAgentWidget({
target: '#chat',
config: {
clientToken: 'YOUR_CLIENT_TOKEN',
webmcp: { enabled: true },
},
})

Replace YOUR_CLIENT_TOKEN with the client token for the chat surface.

Set the WebMCP policy on the chat surface as follows:

Chat surface policy
{
"type": "chat",
"behavior": {
"webmcp": {
"enabled": true,
"allowlist": [
{
"origin": "https://store.example.com",
"tools": ["search_*", "get_cart"]
},
{ "origin": "*", "tools": ["read_page"] }
]
}
}
}

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[] with origin: 'webmcp'.
  • Runtype applies a server-owned webmcp: prefix before the model sees the tool.
  • behavior.webmcp.enabled must be true, and any allowlist rules must match the validated request origin and the bare tool name.
  • The client token’s allowedOrigins remains 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_question with features.askUserQuestion.expose: true or suggest_replies with suggestions.followUps.expose: true (features.suggestReplies.expose is 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 await with an awaitReason and an elicitation payload. An MCP server uses mcp_elicitation. An external A2A agent uses a2a_input_required or a2a_auth_required.

Use the fields in the await event to run the tool and resume the execution:

await
{
"type": "await",
"executionId": "YOUR_EXECUTION_ID",
"seq": 12,
"toolId": "YOUR_TOOL_ID",
"toolCallId": "YOUR_TOOL_CALL_ID",
"toolName": "search_products",
"parameters": { "query": "waterproof trail shoe" },
"origin": "webmcp",
"pageOrigin": "https://store.example.com",
"awaitedAt": "2026-06-16T00:00:00Z"
}

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:

approval_start (subagent)
{
"type": "approval_start",
"executionId": "YOUR_EXECUTION_ID",
"seq": 12,
"approvalId": "YOUR_APPROVAL_ID",
"toolCallId": "YOUR_TOOL_CALL_ID",
"toolName": "send_email",
"toolType": "custom",
"parameters": { "to": "dana@example.com" },
"timeout": 300000,
"startedAt": "2026-06-17T00:00:00Z",
"iteration": 1,
"subagent": { "toolName": "delegate_email", "agentName": "Email Subagent" }
}

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:

cURL
curl -X POST https://api.runtype.com/v1/dispatch/resume \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"executionId": "YOUR_EXECUTION_ID",
"toolOutputs": {
"YOUR_TOOL_CALL_ID": { "result": "YOUR_TOOL_RESULT" }
},
"streamResponse": true
}'

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 the await event. It identifies the paused turn.
  • toolOutputs: an object keyed by the per-call toolCallId from 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 latest await event or JSON paused response from the winning continuation and answer only its outstanding calls.
  • HTTP 404 means 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:

await (external A2A agent asked a question)
{
"type": "await",
"executionId": "YOUR_EXECUTION_ID",
"seq": 8,
"toolId": "YOUR_TASK_ID",
"toolName": "external_agent",
"awaitReason": "a2a_input_required",
"elicitation": {
"mode": "form",
"message": "Which travel dates do you want to book?",
"serverName": "Travel Agent"
},
"externalAgent": {
"contextId": "YOUR_CONTEXT_ID",
"taskId": "YOUR_TASK_ID"
},
"awaitedAt": "2026-08-11T00:00:00Z"
}

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.message is peer-controlled, display-only text. Show it to the person, but don’t branch your logic on its content. Treat awaitReason as UX context, not a control signal.
  • elicitation.mode is form (answer with text) or url (send the human to elicitation.url to authenticate or consent, then continue). A2A pauses use form. MCP servers can use url.
  • externalAgent carries 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:

cURL
curl -X POST https://api.runtype.com/v1/dispatch/resume \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"executionId": "YOUR_EXECUTION_ID",
"toolOutputs": {
"YOUR_TOOL_CALL_ID": {
"kind": "mcpElicitation",
"response": {
"action": "accept",
"content": { "YOUR_FIELD": "YOUR_VALUE" }
},
"pauseCount": 1
}
},
"streamResponse": true
}'

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:

  • kind must be the literal string mcpElicitation. 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.action is accept, decline, or cancel. Send content only with accept, and match its properties to elicitation.requestedSchema from the await event. A decline or cancel response carries no content.
  • pauseCount echoes elicitation.pauseCount from the await event. 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:

cURL
curl -X POST https://api.runtype.com/v1/dispatch \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent": { "agentId": "YOUR_AGENT_ID" },
"messages": [{ "role": "user", "content": "June 3 to June 10." }],
"resumeFrom": { "executionId": "YOUR_EXECUTION_ID", "iteration": 0 },
"streamResponse": true
}'

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:

Non-streaming paused response (approval gate)
{
"success": true,
"result": "",
"status": "paused",
"stopReason": "paused",
"agentExecutionId": "YOUR_AGENT_EXECUTION_ID",
"pausedReason": {
"type": "local_action",
"executionId": "YOUR_EXECUTION_ID",
"toolId": "YOUR_TOOL_ID",
"toolName": "complete_order",
"parameters": { "orderId": "YOUR_ORDER_ID" },
"awaitReason": "approval_required",
"approvalId": "YOUR_APPROVAL_ID"
}
}

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", an approvalId is present): submit the decision to POST /v1/agents/:id/approve with the executionId and approvalId from pausedReason:

    cURL
    curl -X POST https://api.runtype.com/v1/agents/YOUR_AGENT_ID/approve \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
    "executionId": "YOUR_EXECUTION_ID",
    "approvalId": "YOUR_APPROVAL_ID",
    "decision": "approved",
    "streamResponse": false
    }'

    Replace YOUR_AGENT_ID, YOUR_API_KEY, YOUR_EXECUTION_ID, and YOUR_APPROVAL_ID with the values for the paused run.

  • Local/client tool call (no awaitReason / approvalId): run the tool yourself. Then resume with the result through POST /v1/dispatch/resume, using the executionId from pausedReason and the tool call:

    cURL
    curl -X POST https://api.runtype.com/v1/dispatch/resume \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
    "executionId": "YOUR_EXECUTION_ID",
    "toolOutputs": { "YOUR_TOOL_CALL_ID": { "result": "YOUR_TOOL_RESULT" } },
    "streamResponse": false
    }'

    Replace YOUR_API_KEY, YOUR_EXECUTION_ID, YOUR_TOOL_CALL_ID, and YOUR_TOOL_RESULT with 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.elicitation carries the peer’s question. For an external agent, the envelope also carries externalAgent with the conversation handles at the top level, alongside status, not inside pausedReason:

    Non-streaming paused response (external agent asked a question)
    {
    "success": true,
    "status": "paused",
    "stopReason": "paused",
    "pausedReason": {
    "type": "local_action",
    "executionId": "YOUR_EXECUTION_ID",
    "toolId": "YOUR_TASK_ID",
    "toolName": "external_agent",
    "awaitReason": "a2a_input_required",
    "elicitation": {
    "mode": "form",
    "message": "Which travel dates do you want to book?",
    "serverName": "Travel Agent"
    }
    },
    "externalAgent": {
    "contextId": "YOUR_CONTEXT_ID",
    "taskId": "YOUR_TASK_ID"
    }
    }

    Replace the YOUR_* values with identifiers from the paused response.

    Resume an external agent (a2a_input_required or a2a_auth_required) by re-dispatching with the person’s answer and resumeFrom, as described in Resume an A2A elicitation. Resume an MCP server (mcp_elicitation) with an mcpElicitation entry on POST /v1/dispatch/resume instead, 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:

cURL
curl -X POST https://api.runtype.com/v1/client/resume \
-H "Content-Type: application/json" \
-d '{
"sessionId": "YOUR_SESSION_ID",
"executionId": "YOUR_EXECUTION_ID",
"toolOutputs": {
"YOUR_TOOL_CALL_ID": { "result": "YOUR_TOOL_RESULT" }
},
"streamResponse": true
}'

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 matching Origin.
  • executionId: the value from the await event. 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-call toolCallId from the await event. 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 clientTools with the page’s complete registry and include clientToolsFingerprint for 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 returns 409 { "error": "client_tools_resend_required" }. Retry with the full clientTools array and the fingerprint.
cURL
curl -X POST https://api.runtype.com/v1/client/resume \
-H "Content-Type: application/json" \
-d '{
"sessionId": "YOUR_SESSION_ID",
"executionId": "YOUR_EXECUTION_ID",
"toolOutputs": {
"YOUR_TOOL_CALL_ID": { "url": "https://store.example.com/checkout" }
},
"clientTools": [
{
"name": "submit_checkout",
"description": "Submit the checkout form on this page",
"parametersSchema": { "type": "object", "properties": {} },
"origin": "webmcp"
}
],
"clientToolsFingerprint": "YOUR_CLIENT_TOOLS_FINGERPRINT",
"streamResponse": true
}'

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:

Detached frame
{
"type": "await",
"awaitReason": "detached",
"executionId": "YOUR_EXECUTION_ID"
}

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:

cURL
curl -N https://api.runtype.com/v1/agents/YOUR_AGENT_ID/executions/YOUR_EXECUTION_ID/events?after=YOUR_LAST_EVENT_ID \
-H "Authorization: Bearer YOUR_API_KEY"

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:

TypeScript SDK
import { RuntypeClient, withDetachedReconnect } from '@runtypelabs/sdk'
const client = new RuntypeClient({ apiKey: process.env.RUNTYPE_API_KEY })
// Reconnect is on by default.
const stream = await client.agents.executeStream('YOUR_AGENT_ID', {
messages: [{ role: 'user', content: 'Research this and write it up' }],
})
// Opt out to own the reconnect yourself, then drive it with withDetachedReconnect.
const raw = await client.agents.executeStream(
'YOUR_AGENT_ID',
{ messages: [{ role: 'user', content: 'Research this and write it up' }] },
{ autoReconnect: false }
)

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:

Request body
{
"messages": [{ "role": "user", "content": "Research this and write it up" }],
"durability": {
"watchLeaseMs": 1800000
}
}

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:

Read-shaped tool, safe to re-dispatch
{
name: 'fetch_user',
description: 'Fetch user details by ID',
toolType: 'external',
idempotent: true,
parametersSchema: {
type: 'object',
properties: { userId: { type: 'string' } },
required: ['userId']
},
config: {
url: 'https://api.example.com/users/{{userId}}',
method: 'GET'
}
}

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:

Agent that requires a committed intent
{
"tools": {
"intent": "mandatory",
"toolIds": ["builtin:runtype_record_upsert"]
}
}

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:

TOOL_INTENT_NOT_COMMITTED: the tool call was not started because its intent could not be recorded durably. It had no effect; it is safe to call it again.

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_ONLY when 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 idempotent keep the best-effort intent, because calling them again is safe.
  • A dispatch request can set tools.intent to mandatory for a saved Agent. A request can’t lower a saved Agent’s mandatory to best-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.

cURL
curl -X POST https://api.runtype.com/v1/agents/YOUR_AGENT_ID/approve \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"executionId": "YOUR_EXECUTION_ID",
"approvalId": "YOUR_APPROVAL_ID",
"decision": "approved",
"remember": true
}'

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.

TypeScript SDK
import { RuntypeClient } from '@runtypelabs/sdk'
const client = new RuntypeClient({ apiKey: process.env.RUNTYPE_API_KEY })
const grants = await client.toolApprovalGrants.list('YOUR_AGENT_ID')

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:

cURL
curl https://api.runtype.com/v1/tool-approval-grants?agentId=YOUR_AGENT_ID \
-H "Authorization: Bearer YOUR_API_KEY"

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:

FieldDescription
idGrant ID, used to revoke
agentIdThe agent the grant applies to
endUserRefThe approving end user, represented by eu_YOUR_END_USER_ID for agents with a Tenancy Strategy or by the raw endUser.id, or null for an account-level grant.
toolTypeTool namespace (builtin, mcp, custom)
toolNameThe tool the grant remembers
decisionThe remembered decision (allow)
createdAtWhen the grant was recorded
expiresAtExpiry time, or null if the grant doesn’t expire

Revoke a grant

Revoke a grant to prompt for approval again on future dispatches. The API returns { "revoked": true }.

TypeScript SDK
await client.toolApprovalGrants.revoke('YOUR_GRANT_ID')

Replace YOUR_GRANT_ID with the ID of the grant to revoke.

cURL
curl -X DELETE https://api.runtype.com/v1/tool-approval-grants/YOUR_GRANT_ID \
-H "Authorization: Bearer YOUR_API_KEY"

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 to true for 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. Set choices.alwaysAllow: true to show Always allow for this dispatch.

Configure an approval override as follows:

Approval override
{
"agent": {
"agentId": "YOUR_AGENT_ID",
"tools": {
"approval": {
"require": ["send_email"],
"choices": { "alwaysAllow": true }
}
}
},
"messages": [{ "role": "user", "content": "Email the summary to the team" }]
}

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:

Hidden parameter
{
name: 'list_orders',
description: 'List recent orders for the current account',
toolType: 'external',
parametersSchema: {
type: 'object',
properties: { status: { type: 'string' } },
required: ['status']
},
hiddenParameterNames: ['_tenant'],
config: {
url: 'https://api.example.com/v1/orders?status={{status}}',
method: 'GET',
headers: {
Authorization: 'Bearer {{secret:ORDERS_API_KEY}}',
'X-Tenant-Id': '{{_tenant.id}}'
}
}
}

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:

LimitValue
Serialized runtimeTools per request1 MB across all steps
Runtime custom tool timeout30 seconds
MCP servers per step5
Client tools payload256 KB
Client tool name64 characters, starting with a letter
Client tool description2 KB
Each string in a client tool’s schema2 KB
Tools for a local in-browser model50

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:

ErrorStatusFix
Runtime tool missing required fields400Set name, description, toolType, and parametersSchema. Set config for every type except local.
parametersSchema must be a JSON Schema object400Set type: "object" at the top level of parametersSchema.
collides with the reserved tool name400Rename the tool. Names that normalize to runtype_set_state are reserved, and the runtype_ prefix is reserved for platform tools.
cannot reference {{secret:KEY}}400Move the secret reference to an external tool.
does not have SECRETS:READ permission403Grant SECRETS:READ or SECRETS:* to the API key.
missing URL or has invalid URL format400Set config.url to a URL that is valid after placeholder substitution.
timeout exceeds 30s limit400Set a custom tool timeout of 30000 or less.

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:

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.

Use runtime tools to test configurations, then save working tools for production use.

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: