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

Use CaseDescription
TestingTry tool configurations before saving
Dynamic configsTool URLs/params vary per request
User-providedUsers supply their own tool definitions
One-off tasksTools needed for a single execution

Basic Example

TypeScript SDK
1import { FlowBuilder, RuntypeClient } from '@runtypelabs/sdk'
2
3const client = new RuntypeClient({
4 apiKey: process.env.RUNTYPE_API_KEY,
5})
6
7const result = await new FlowBuilder()
8 .createFlow({ name: 'Weather Agent' })
9 .prompt({
10 name: 'Agent',
11 model: 'gpt-5.4',
12 userPrompt: 'What is the weather in Tokyo?',
13 tools: {
14 runtimeTools: [
15 {
16 name: 'get_weather',
17 description: 'Get current weather for a city',
18 toolType: 'external',
19 parametersSchema: {
20 type: 'object',
21 properties: {
22 city: { type: 'string', description: 'City name' },
23 },
24 required: ['city'],
25 },
26 config: {
27 url: 'https://api.weather.com/v1/current?city={{city}}',
28 method: 'GET',
29 headers: {
30 Authorization: 'Bearer {{secrets.weather_key}}',
31 },
32 },
33 },
34 ],
35 },
36 })
37 .run(client, { streamResponse: true })
Python SDK
1runtime_tool = {
2 "name": "get_weather",
3 "description": "Get current weather for a city",
4 "toolType": "external",
5 "parametersSchema": {
6 "type": "object",
7 "properties": {
8 "city": {"type": "string", "description": "City name"}
9 },
10 "required": ["city"]
11 },
12 "config": {
13 "url": "https://api.weather.com/v1/current?city={{city}}",
14 "method": "GET",
15 "headers": {
16 "authorization": "Bearer {{secrets.weather_key}}"
17 }
18 }
19}
20
21for event in client.dispatch({
22 "flow": {
23 "steps": [{
24 "type": "prompt",
25 "config": {
26 "model": "gpt-5.4",
27 "userPrompt": "What is the weather in Tokyo?",
28 "tools": {
29 "runtimeTools": [runtime_tool]
30 }
31 }
32 }]
33 },
34 "secrets": {
35 "weather_key": os.environ["WEATHER_API_KEY"]
36 }
37}):
38 print(event)
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": "gpt-5.4",
> "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.weather.com/v1/current?city={{city}}",
> "method": "GET"
> }
> }]
> }
> }
> }]
> }
> }'

Tool Types

Runtime tools support the same types as saved tools:

External Tools

Call any HTTP API:

1{
2 name: 'fetch_user',
3 description: 'Fetch user details by ID',
4 toolType: 'external',
5 parametersSchema: {
6 type: 'object',
7 properties: {
8 userId: { type: 'string' }
9 },
10 required: ['userId']
11 },
12 config: {
13 url: 'https://api.example.com/users/{{userId}}',
14 method: 'GET',
15 headers: {
16 'Authorization': 'Bearer {{secrets.api_token}}'
17 }
18 }
19}

Custom Tools (Code)

Execute JavaScript in a secure sandbox:

1{
2 name: 'calculate_tax',
3 description: 'Calculate sales tax',
4 toolType: 'custom',
5 parametersSchema: {
6 type: 'object',
7 properties: {
8 amount: { type: 'number' },
9 rate: { type: 'number' }
10 },
11 required: ['amount', 'rate']
12 },
13 config: {
14 code: `
15 const tax = amount * (rate / 100);
16 return {
17 subtotal: amount,
18 tax: tax.toFixed(2),
19 total: (amount + tax).toFixed(2)
20 };
21 `,
22 timeout: 5000
23 }
24}

Flow Tools

Execute another Runtype flow as a tool:

1{
2 name: 'analyze_sentiment',
3 description: 'Run sentiment analysis on text',
4 toolType: 'flow',
5 parametersSchema: {
6 type: 'object',
7 properties: {
8 text: { type: 'string' }
9 },
10 required: ['text']
11 },
12 config: {
13 flowId: 'flow_abc123',
14 parameterMapping: { 'input_text': 'text' },
15 outputMapping: 'sentiment'
16 }
17}

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.

1{
2 name: 'research_topic',
3 description: 'Research a topic and return a summary',
4 toolType: 'subagent',
5 parametersSchema: {
6 type: 'object',
7 properties: {
8 task: { type: 'string', description: 'What to research' }
9 },
10 required: ['task']
11 },
12 config: {
13 // Either point at a saved agent...
14 agentId: 'agent_abc123',
15 // ...or define the child inline with `agent: { ... }` (set exactly one).
16 allowedTools: ['builtin:exa', 'mcp:linear:*'],
17 maxTurns: 5, // default 5
18 timeoutMs: 300000, // default 5 minutes
19 outputFormat: 'text' // 'text' | 'json' | 'last_message'
20 }
21}

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:

1tools: {
2 toolIds: ['builtin:exa', 'mcp:linear:create_issue'],
3 subagentConfig: {
4 toolPool: ['builtin:exa', 'mcp:linear:*'], // subset of parent's tools
5 maxSpawnsPerRun: 5, // default 5
6 maxTurnsLimit: 10, // hard cap, default 10
7 allowNesting: false // default false
8 }
9}

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:

1const result = await client.dispatch({
2 flow: { ... },
3 secrets: {
4 api_token: process.env.EXTERNAL_API_KEY,
5 db_password: process.env.DB_PASSWORD
6 }
7})

Reference in tool config: {{secrets.api_token}}

Secrets are never logged, stored, or returned in API responses.

Variable Substitution

Tool configurations support template variables:

VariableSource
{{paramName}}Tool call parameter
{{secrets.keyName}}Dispatch secrets field
{{_record.field}}Current record data
{{_flow.id}}Flow metadata

Example:

1config: {
2 url: 'https://api.example.com/{{_record.type}}/{{id}}',
3 headers: {
4 'Authorization': 'Bearer {{secrets.token}}',
5 'X-User-Id': '{{_user.id}}'
6 }
7}

Combining with Saved Tools

Mix runtime tools with saved tools:

1tools: {
2 // Saved tools by ID
3 toolIds: [
4 'tool_abc123',
5 'mcp:notion:create_page'
6 ],
7 // Runtime tools
8 runtimeTools: [
9 { name: 'dynamic_tool', ... }
10 ],
11 maxToolCalls: 10
12}

SDK Helper Functions

The TypeScript SDK provides helper functions:

1import { createExternalTool } from '@runtypelabs/sdk'
2
3const weatherTool = createExternalTool({
4 name: 'get_weather',
5 description: 'Get weather for a city',
6 parametersSchema: {
7 type: 'object',
8 properties: {
9 city: { type: 'string' },
10 },
11 },
12 url: 'https://api.weather.com/current?city={{city}}',
13 method: 'GET',
14 headers: {
15 Authorization: 'Bearer {{secrets.key}}',
16 },
17})
18
19// Use in flow
20tools: {
21 runtimeTools: [weatherTool]
22}

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/dispatch and /v1/agents/:id/execute) the caller holds a secret key and controls the page, so webmcp tools are admitted by default — their presence in clientTools[] 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.

TypeScript SDK
1import { RuntypeClient } from '@runtypelabs/sdk'
2
3const client = new RuntypeClient({ apiKey: process.env.RUNTYPE_API_KEY })
4
5// scope: 'turn' snapshots your local tools into the dispatch envelope's
6// clientTools[] and drives the dispatch + resume loop for you, executing
7// each tool call locally.
8await client.runWithLocalTools(
9 {
10 flow: { id: 'flow_abc123' },
11 messages: [{ role: 'user', content: 'What time is it?' }],
12 },
13 {
14 get_time: {
15 description: 'Return the current ISO time',
16 parametersSchema: { type: 'object', properties: {} },
17 execute: async () => new Date().toISOString(),
18 },
19 },
20 { scope: 'turn' }
21)
cURL
$curl -X POST https://api.runtype.com/v1/dispatch \
> -H "Authorization: Bearer YOUR_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "agent": { "id": "agent_abc123" },
> "messages": [{ "role": "user", "content": "Search the catalog" }],
> "clientTools": [
> {
> "name": "search",
> "description": "Search the catalog",
> "parametersSchema": { "type": "object", "properties": {} },
> "origin": "webmcp"
> }
> ]
> }'

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.

1{
2 "agent": { "id": "agent_abc123" },
3 "clientTools": [
4 {
5 "name": "search_products",
6 "description": "...",
7 "parametersSchema": { "type": "object" },
8 "origin": "webmcp"
9 },
10 {
11 "name": "delete_account",
12 "description": "...",
13 "parametersSchema": { "type": "object" },
14 "origin": "webmcp"
15 }
16 ],
17 "clientToolsPolicy": { "allowlist": ["search_*"] }
18}

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.

Persona widget config
1initAgentWidget({
2 target: '#chat',
3 config: {
4 clientToken: 'YOUR_CLIENT_TOKEN',
5 webmcp: { enabled: true },
6 },
7})
1{
2 "type": "chat",
3 "behavior": {
4 "webmcp": {
5 "enabled": true,
6 "allowlist": [
7 { "origin": "https://store.example.com", "tools": ["search_*", "get_cart"] },
8 { "origin": "*", "tools": ["read_page"] }
9 ]
10 }
11 }
12}

On a Persona surface:

  • The embedding page registers tools with document.modelContext.registerTool(...).
  • 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 features.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:

await
1{
2 "type": "await",
3 "executionId": "exec_...",
4 "seq": 12,
5 "toolId": "toolu_...",
6 "toolCallId": "call_...",
7 "toolName": "search_products",
8 "parameters": { "query": "waterproof trail shoe" },
9 "origin": "webmcp",
10 "pageOrigin": "https://store.example.com",
11 "awaitedAt": "2026-06-16T00:00:00Z"
12}

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:

approval_start (subagent)
1{
2 "type": "approval_start",
3 "executionId": "exec_...",
4 "seq": 12,
5 "approvalId": "appr_...",
6 "toolCallId": "call_...",
7 "toolName": "send_email",
8 "toolType": "custom",
9 "parameters": { "to": "team@example.com" },
10 "timeout": 300000,
11 "startedAt": "2026-06-17T00:00:00Z",
12 "iteration": 1,
13 "subagent": { "toolName": "delegate_email", "agentName": "Email Subagent" }
14}

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

cURL
$curl -X POST https://api.runtype.com/v1/dispatch/resume \
> -H "Authorization: Bearer YOUR_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "executionId": "exec_...",
> "toolOutputs": {
> "call_...": { "result": "..." }
> },
> "streamResponse": true
> }'
  • executionId is the value from the await (or approval_start) event — it addresses the paused turn.
  • toolOutputs is keyed by the per-call toolCallId from 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_delta and finishes with execution_complete / execution_error (or another await if 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:

Non-streaming paused response (approval gate)
1{
2 "success": true,
3 "result": "",
4 "status": "paused",
5 "stopReason": "paused",
6 "agentExecutionId": "aex_...",
7 "pausedReason": {
8 "type": "local_action",
9 "executionId": "exec_...",
10 "toolId": "toolu_...",
11 "toolName": "complete_order",
12 "parameters": { "orderId": "ord_1" },
13 "awaitReason": "approval_required",
14 "approvalId": "appr_..."
15 }
16}

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", 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/AGENT_ID/approve \
    > -H "Authorization: Bearer YOUR_API_KEY" \
    > -H "Content-Type: application/json" \
    > -d '{
    > "executionId": "exec_...",
    > "approvalId": "appr_...",
    > "decision": "approved",
    > "streamResponse": false
    > }'
  • Local/client tool call (no awaitReason / approvalId): run the tool yourself, then resume with the result via POST /v1/dispatch/resume, keyed by the executionId from pausedReason and the tool call (as with the streaming path above):

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

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:

cURL
$curl -X POST https://api.runtype.com/v1/client/resume \
> -H "Content-Type: application/json" \
> -d '{
> "sessionId": "cs_...",
> "executionId": "exec_...",
> "toolOutputs": {
> "call_abc123": { "result": "..." }
> },
> "streamResponse": true
> }'
  • sessionId is the session from /v1/client/init; it authenticates the request (active session, active client token, matching Origin).
  • executionId comes 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.
  • toolOutputs is keyed by the per-call toolCallId from 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 a clientToolsFingerprint for 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 returns 409 { "error": "client_tools_resend_required" }; retry once with the full clientTools[] plus the fingerprint.
cURL
$curl -X POST https://api.runtype.com/v1/client/resume \
> -H "Content-Type: application/json" \
> -d '{
> "sessionId": "cs_...",
> "executionId": "exec_...",
> "toolOutputs": {
> "call_abc123": { "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": "a1b2c3...",
> "streamResponse": true
> }'

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.

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

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.

TypeScript SDK
1import { RuntypeClient } from '@runtypelabs/sdk'
2
3const client = new RuntypeClient({ apiKey: process.env.RUNTYPE_API_KEY })
4
5const grants = await client.toolApprovalGrants.list('agent_abc123')
cURL
$curl https://api.runtype.com/v1/tool-approval-grants?agentId=agent_abc123 \
> -H "Authorization: Bearer YOUR_API_KEY"

The response is { "data": [...] }. Each grant has these fields:

FieldDescription
idGrant ID, used to revoke
agentIdThe agent the grant applies to
endUserRefThe end user who approved, or null for an account-level (all users) 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 does not expire

Revoke a grant

Revoke a grant so the tool prompts for approval again on future dispatches. This returns { "revoked": true }.

TypeScript SDK
1await client.toolApprovalGrants.revoke('grant_abc123')
cURL
$curl -X DELETE https://api.runtype.com/v1/tool-approval-grants/grant_abc123 \
> -H "Authorization: Bearer YOUR_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, agentInput.tools.approval mirrors the agent’s saved approval config for that single run. It accepts:

  • require: which tools need approval (true for 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. Set choices.alwaysAllow: true to show the Always allow affordance for this dispatch, the same option you would otherwise turn on in the saved agent config.
1{
2 "agent": { "id": "agent_abc123" },
3 "messages": [{ "role": "user", "content": "Email the summary to the team" }],
4 "agentInput": {
5 "tools": {
6 "approval": {
7 "require": ["send_email"],
8 "choices": { "alwaysAllow": true }
9 }
10 }
11 }
12}

Limits

LimitValue
Total runtime tools per request50
Client tools per dispatch50
Client tools payload64 KB
MCP servers per step5
Custom tool timeout30 seconds

API Format

The API uses camelCase for all field names:

1{
2 "tools": {
3 "runtimeTools": [{
4 "name": "myTool",
5 "toolType": "external",
6 "parametersSchema": { ... },
7 "config": { ... }
8 }]
9 }
10}

Best Practices

If you use the same runtime tool repeatedly, save it to your account via the API or dashboard for cleaner code.

Use lowercase header names (e.g., authorization not Authorization) to avoid issues with automatic case conversion.

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

Ensure parametersSchema is valid JSON Schema. Invalid schemas cause tool calls to fail.

Next Steps