Inline tool format
The canonical CallConfigSnapshot.tools[] shape — used when you pass an inline transient agent on POST /api/v1/call. Differs from the saved-tool format on every typed tool.
Every agent config's tools[] array — on a saved agent (POST /agents) or a transient inline agent (POST /api/v1/call) — can mix two kinds of entries:
| Entry | What it does |
|---|---|
UUID string — e.g. "abc12345-..." | A reference to a saved tool. The runner fetches it from the DB and registers it. |
Inline tool dict — { type, name, description, config } | A one-off tool defined inline. Validated against the canonical schema below. |
This page documents the inline dict shape. For the saved-tool (POST /tools) shape, see Tool types.
The inline shape is NOT the saved-tool shape
The two formats differ on function (nested server block vs. flat fields), on transferCall (no messageType/audioRecordingId), and on query (knowledgeBases instead of groups). Don't copy a definition.config from GET /tools/{tool_uuid} and paste it as an inline tool — it will fail validation.
Common envelope
Every inline tool dict has these fields:
| Field | Type | Required | Notes |
|---|---|---|---|
type | enum | ✓ | One of function, endCall, transferCall, query, testTool. (No calculator — that's a saved-tool only category today.) |
name | string | ✓ for function/query/transferCall/testTool | 1..64 chars, [A-Za-z0-9_-]+. The function name the LLM sees. For endCall, defaults to "end_call". Names must be unique after sanitization; end_call / retrieve_from_knowledge_base / safe_calculator are reserved (endCall tools may use end_call). |
description | string | — | Function description shown to the LLM. |
config | object | ✓ | Type-specific config — schemas below. |
Unknown fields are rejected (extra="forbid" on every config). Send only what's documented.
function — HTTP API
Call an external HTTP API. The LLM produces an arguments object matching parameters; the runner makes the HTTP request.
{
"type": "function",
"name": "lookup_order",
"description": "Look up the status of an order by id.",
"config": {
"server": {
"url": "https://api.acme.com/orders/{{orderId}}",
"method": "GET",
"headers": { "X-Tenant": "acme" },
"timeout_ms": 5000
},
"parameters": {
"type": "object",
"properties": {
"orderId": { "type": "string", "description": "Order ID like ORD-42." }
},
"required": ["orderId"]
},
"async": false,
"messages": ["Looking that up...", "One moment..."]
}
}config fields
| Field | Type | Required | Notes |
|---|---|---|---|
server.url | string | ✓ | Must start with http:// or https://. Supports {{contextVariable}} substitution. Private/loopback/link-local IPs and a blocked-host list are rejected. |
server.method | enum | — | GET, POST (default), PUT, PATCH, DELETE. |
server.headers | object | — | Static headers merged into every request. Supports {{contextVariable}} in values. |
server.timeout_ms | int | — | Request timeout in ms. 1000..30000. Default 10000. |
parameters | object | null | — | A JSON Schema object ({type, properties, required, ...}) describing the arguments the LLM may produce. Pass null (or omit) for a no-arg tool. |
async | bool | — | When true, the runner fires the request and returns control to the LLM immediately without waiting for a response. Default false. |
messages | string[] | — | Filler phrases the agent speaks before the request runs (e.g. "Let me check that..."). ONE per invocation, picked per messagesOrder. Max 5; [] (default) is silent. Sent via TTSSpeakFrame. |
messagesOrder | "random" / "in-order" | "random" | random = uniform pick among all variants; in-order = invocation K speaks entry K (wrapping around) within the call. |
`parameters` is JSON Schema, not a list
The saved-tool format uses parameters: [{name, type, description, required}, ...]. The inline format takes a raw JSON Schema dict — same shape OpenAI/Anthropic use for function tools. The conversion happens in the saved-tool path; inline tools skip the conversion and need real JSON Schema.
endCall — let the LLM hang up
Lets the LLM explicitly end the call. Optional goodbye message.
{
"type": "endCall",
"name": "end_call",
"description": "End the call when the caller says goodbye.",
"config": {
"messageType": "custom",
"customMessages": ["Thanks for calling. Goodbye!", "Take care — bye!"]
}
}config fields
| Field | Type | Required | Notes |
|---|---|---|---|
messageType | "none" / "custom" / "audio" | — | Default "custom" (speaks one of customMessages, defaulting to ["Goodbye!"]). "none" still speaks a built-in farewell — there is no silent-hangup option. |
customMessages | string[] | ≥1 non-blank entry required when messageType="custom" | Farewell variants — ONE is chosen at random each time the farewell plays. Max 5 entries. |
audioRecordingId | string | required when messageType="audio" | UUID of a pre-recorded audio file. |
No `endCallReason` field
The end reason is captured automatically (agent_hangup, user_hangup, max_duration, silence_timeout, voicemail, disconnect, error) on CallModel.end_reason — no LLM tokens needed and nothing to configure on the tool.
transferCall — transfer to a phone or SIP endpoint
{
"type": "transferCall",
"name": "transfer_to_billing",
"description": "Transfer the call to billing when the caller asks for refunds.",
"config": {
"destination": "+14155550199",
"customMessage": "Transferring you to billing now.",
"timeout": 30,
"recordTransfer": true
}
}config fields
| Field | Type | Required | Notes |
|---|---|---|---|
destination | string | ✓ | E.164 number (+1...) or SIP endpoint (PJSIP/123, SIP/123). |
customMessage | string | null | — | Optional phrase spoken right before transfer. |
timeout | int (5..300) | — | Seconds to wait for the destination to answer. Default 30. |
recordTransfer | bool | — | Default true. Carrier-side recording of the transfer leg (caller ↔ destination), attached to the call record as a transfer recording. Ignored when full-call carrier recording (recordInProviderConsole) is on. |
Inline `transferCall` has no `messageType` / `audioRecordingId`
The saved-tool format accepts messageType and audioRecordingId. The inline format does not — the schema rejects them. If you need an audio pre-transfer message, save a transferCall tool via POST /tools and reference it by UUID instead.
Every telephony provider zoxaAI supports accepts transferCall — Twilio, Vobiz, Asterisk ARI, Plivo, Vonage, Telnyx, Cloudonix, and Tata Smartflo. Web calls (WebRTC / WebSocket) cannot transfer. If a transfer can't proceed — a web call, or a provider whose transfer credentials aren't configured — the runner rejects it with reason: "provider_not_supported"; the LLM is told and can verbalize the failure to the caller rather than going silent.
query — knowledge-base retrieval
Retrieve from one or more named KB buckets during the call.
{
"type": "query",
"name": "knowledge_base",
"description": "Search the knowledge base for product / billing answers.",
"config": {
"knowledgeBases": [
{
"name": "billing",
"description": "Pricing, refunds, plan changes.",
"documentIds": ["doc-uuid-1", "doc-uuid-2"]
},
{
"name": "support",
"description": "Troubleshooting and how-to.",
"documentIds": ["doc-uuid-3"]
}
],
"messages": ["One moment, checking that..."],
"timeout": 5
}
}config fields
| Field | Type | Required | Notes |
|---|---|---|---|
knowledgeBases | array | ✓ | One or more named buckets (min_length=1). |
knowledgeBases[].name | string | ✓ | Short bucket label — the LLM sees this as the function-argument value. |
knowledgeBases[].description | string | ✓ | Tells the LLM when to query this bucket. |
knowledgeBases[].documentIds | string[] | ✓ | Document UUIDs from GET /files. Must be non-empty. |
messages | string[] | — | Filler variants spoken during retrieval — ONE per lookup, picked per messagesOrder. Max 5; [] is silent. |
messagesOrder | "random" / "in-order" | "random" | random = uniform pick among all variants; in-order = sequential within a call, wrapping around. |
timeout | int (1..30) | — | Per-query timeout in seconds. Default 5. |
`knowledgeBases`, not `groups`
The saved-tool format uses groups[]. The inline format uses knowledgeBases[]. Same fields inside each entry (name, description, documentIds) — only the outer key differs.
testTool — simulated tool for rehearsal
Exercises the full tool-calling flow — invocation, dead air, success/failure handling — without a real backend. The description drives when the LLM calls it; execution is a configurable delay followed by the configured outcome.
{
"type": "testTool",
"name": "simulate_crm_lookup",
"description": "Look up the caller's CRM record when they ask about their account.",
"config": {
"delaySeconds": 6.0,
"outcome": "success",
"responseText": "Account active, premium tier.",
"messages": ["Let me pull up your account.", "One moment please."]
}
}config fields
| Field | Type | Required | Notes |
|---|---|---|---|
delaySeconds | number (0.5..30) | — | Simulated execution time. Default 6. Realism task sound plays over it like any real tool. |
outcome | "success" / "failure" | — | Default "success". |
responseText | string | — | Returned as the result payload (success) or error detail (failure). Blank = a built-in steering message the agent relays naturally. |
messages | string[] | — | Filler variants spoken as the simulated execution starts — ONE per invocation, picked per messagesOrder, same as every other tool. Max 5; [] (default) is silent. |
messagesOrder | "random" / "in-order" | "random" | random = uniform pick among all variants; in-order = sequential within a call, wrapping around. |
Complete inline example
A transient agent on POST /api/v1/call with one of each inline tool:
{
"type": "outbound",
"callConfig": {
"provider": "twilio",
"auth": { "accountSid": "AC...", "authToken": "..." },
"phoneNumber": "+14155550100"
},
"toNumber": "+14155550199",
"agent": {
"name": "Aria from Acme",
"systemPrompt": "You are Aria from Acme. Be concise.",
"greeting": { "firstMessages": ["Hi, this is Aria from Acme — how can I help?"] },
"llm": { "provider": "openai", "model": "gpt-5.4-mini" },
"tts": { "provider": "cartesia" },
"stt": { "provider": "soniox" },
"tools": [
{
"type": "function",
"name": "lookup_order",
"description": "Look up the status of an order by id.",
"config": {
"server": {
"url": "https://api.acme.com/orders/{{orderId}}",
"method": "GET",
"headers": { "X-Tenant": "acme" },
"timeout_ms": 5000
},
"parameters": {
"type": "object",
"properties": { "orderId": { "type": "string" } },
"required": ["orderId"]
},
"messages": ["Let me look that up..."]
}
},
{
"type": "endCall",
"name": "end_call",
"description": "End when the caller says goodbye.",
"config": { "messageType": "custom", "customMessages": ["Thanks for calling Acme. Goodbye!"] }
},
{
"type": "query",
"name": "knowledge_base",
"description": "Search the KB for billing / product answers.",
"config": {
"knowledgeBases": [
{ "name": "billing", "description": "Pricing, refunds.", "documentIds": ["doc-uuid-1"] }
]
}
},
"abc12345-6789-0123-4567-89abcdef0123"
]
}
}The last entry — a UUID string — references a saved tool, mixed freely with the inline dicts.
See also
- Tool types — saved-tool format (POST /tools)
POST /api/v1/call— transient agent payload- Agent config — full nested agent schema
- Context variables —
{{variable}}substitution
Tool types
Full schema reference for every tool definition shape — function (HTTP), endCall, transferCall, calculator, query (knowledge base). Covers the saved-tool format (POST /tools) and the inline transient-agent format separately.
Knowledge Base overview
How documents are ingested, chunked, embedded, and retrieved.