Tools and functions
A knowledge base lets the agent know something. A function lets it do something — look up this caller's order, book the slot, hand the call to a person. The model decides when; you decide what is possible.
What is available
/v1/functionscurl -s https://voice.sphoro.com/v1/functions \
-H "Authorization: Bearer $SPHORO_API_KEY"This lists what your account can actually use, which is the only list worth trusting — naming a function on an agent that this endpoint does not return is refused.
The three built in
| Tool | Does | Given to the agent by |
|---|---|---|
search_knowledge_base | Searches the documents attached to this agent, mid-turn, before it answers. | Attaching at least one base in
knowledge_base_ids. Naming it in tools does nothing. |
transfer_call | Hands the caller to a person. | Setting
transfer_number on the agent. That is the only number it can ever
dial — the model chooses when, never where. |
end_call | Ends the call, with a reason in the model's own words that is written onto the call record. | Available to an agentic agent; switchable under
autonomy: "custom". |
"tools": ["transfer_call"] and no transfer_number has no transfer tool,
and the model never mentions transferring because it cannot see one.Functions that call your own API
The registry also runs tools backed by your REST endpoints: the model calls
get_order_status, we validate the arguments, call your URL, and hand the response
back to the model to speak from.
A tool like that is described by four things:
{
"name": "get_order_status",
"description": "Look up the status of an order by its reference. Ask the caller to read the reference back before calling this.",
"parameters": {
"type": "object",
"properties": {
"order_id": { "type": "string", "pattern": "^AC-[0-9]{5}$" }
},
"required": ["order_id"],
"additionalProperties": false
},
"url": "https://api.acme.example/orders/{{order_id}}",
"method": "GET"
}tool_configs, and the endpoint above lists what the
deployment registered — the shared, code-defined ones. Most agents have both.Tools an agent carries itself
tool_configs on an agent is a list of tools defined by whoever built the agent,
each carrying the configuration that makes it mean something: which calendar, which person,
which endpoint. It replaces the whole list on every write, so a tool left out of it is removed.
There are four kinds.
kind | Does | Configured by |
|---|---|---|
calendar_availability | Reads the open slots, so the agent offers times that are actually free. | calendar: a Cal.com
api_key, an event_type_id and a timezone. |
book_appointment | Writes the agreed time in, and reads the reference back. | The same calendar block. Give the agent both: one to offer times, one
to take one. |
transfer_call | Hands the call to a person, at its own destination. | transfer: a destination in E.164 or a
sip: URI, and an optional webhook_url told before the call
moves. |
custom_function | Calls any endpoint of yours. | request:
a method, a url, headers, and a table of
parameters. |
{
"tool_configs": [
{
"kind": "custom_function",
"name": "get_order_status",
"description": "Look up an order by its reference. Ask the caller to read the reference back before calling this — a wrong reference returns somebody else's order.",
"pre_tool_message": { "en-IN": "Let me pull that order up." },
"request": {
"method": "GET",
"url": "https://api.acme.example/orders/{{order_id}}",
"headers": { "Accept": "application/json" },
"secret_header_values": { "Authorization": "Bearer sk_live_…" },
"parameters": [
{ "name": "order_id", "type": "string", "in": "path", "required": true,
"description": "The order reference the caller read out, like AC-10293." }
],
"timeout_seconds": 8
}
},
{
"kind": "transfer_call",
"name": "escalate_to_billing",
"description": "Put the caller through to billing when they dispute a charge.",
"transfer": { "destination": "+15559998888" }
}
]
}Credentials go one way
A calendar key travels as {"api_key": {"value": "cal_live_…"}} and comes back as
{"api_key": {"hint": "••••1234"}}; sealed headers go out under
secret_header_values and come back as secret_header_names alone. Leaving
either out on an update keeps what is stored — which is what lets you change a description
without resending the key.
The pre-tool message
A tool can take seconds, and silence on a phone reads as a dropped line.
pre_tool_message is what the agent says while it runs, keyed by language code, and
it is per tool because the wait is not the same wait: "let me check the diary" and "let me put
you through" describe different things happening. An agent's own filler_phrase
covers any tool that has none.
Where else the work could go
| Instead of a live tool | Use | Difference |
|---|---|---|
| Work that can happen after the call | A
workflow's trigger_webhook step | Runs once the conversation is over. The caller never waits for it. |
| Work between calls in a sequence | A journey's
api node | Calls your endpoint between calls and branches on the answer. |
Most things people reach for a live tool to do turn out not to need to be live. "Write the outcome to our CRM" does not have to happen while the caller is on the line; "tell the caller their balance" does.
Writing the description
The description is a prompt, not documentation. It is the only thing the model reads when deciding whether to call this tool, and rewriting it is the highest-leverage change you can make to a tool that fires at the wrong times.
| Instead of | Write |
|---|---|
| "Books an appointment." | "Reserve a slot on the clinic calendar. Confirm the date and time with the caller first, and never call this twice for one caller." |
| "Gets order status." | "Look up an order by its reference. Ask the caller to read the reference back before calling this — a wrong reference returns somebody else's order." |
Say when to call it, what to do first, and when not to. Those three sentences do more than any schema change.
Arguments are validated before your endpoint sees them
A model will, reliably and forever, invent argument names, omit required fields and pass strings where numbers belong. Every call is checked against the schema first, and every problem is reported at once.
| Supported | |
|---|---|
| Types | object, array, string,
number, integer, boolean, null |
| Presence | required, additionalProperties |
| Strings | minLength, maxLength, pattern,
enum |
| Numbers | minimum, maximum,
exclusiveMinimum, exclusiveMaximum |
| Arrays | items, minItems, maxItems |
| Objects | properties, nested to any depth |
| Combinators | allOf, anyOf, oneOf |
| Formats | email, date-time, uri,
phone |
"additionalProperties": false.
Without it, a model that invents an extra argument has it silently accepted and passed to your
handler. With it, the invention is caught and the model is told, on the same turn, so it
corrects itself.Coercion is deliberately narrow
Models routinely emit "3" where a number belongs. Only the unambiguous cases are
converted; anything else is left alone so validation still catches it.
"3" ──▶ 3 (integer)
"0.7" ──▶ 0.7 (number)
"true" ──▶ true (boolean)
"maybe" ──▶ rejectedA validation failure is text, not an error
When arguments do not validate, the model is handed the failure as the tool result rather than the call being aborted. It then corrects itself on the next turn, usually within the same breath. That feedback loop is most of what makes tool calling reliable on a live call, and it is why a badly-specified schema shows up as a slow call rather than a broken one.
What the caller hears while it runs
A tool call is a real network round-trip inside a live conversation. Two things to set:
| Setting | Does |
|---|---|
filler_phrase on the agent | Something said while the tool runs — "let me check that for you". Makes a three-second lookup sound like thinking rather than a dropped line. |
max_tool_rounds in capabilities | How many times one turn may call a tool and generate again. 1–10, default 3. Every round is a full model call plus your API, so a bigger number is a longer silence rather than a more thorough answer. |
Watching tool calls
call.tool.called fires on every invocation, carrying the tool name — on
webhooks and on the per-call event stream. It is the first thing to
look at when a call has an unexplained pause in the middle: if the pause lines up with a tool
call, your endpoint is the latency, not the model.
/v1/calls/{id}/eventscurl -sN https://voice.sphoro.com/v1/calls/$CALL_ID/events \
-H "Authorization: Bearer $SPHORO_API_KEY"Restricting tools to part of a conversation
On a flow, each node can narrow or force the tools available while
the call is on it — tools restricts what the model can see, and tool
forces one to be called. A transfer node that must transfer, a booking node that must book. That
is usually a better answer than writing "only book once you have confirmed" into a prompt and
hoping.