Skip to main content
A tool is two things: a contract the model sees, and something that runs when the model calls it. Both live in one file, tools/<name>.yaml. The file stem is the tool name. It must be lower snake case and cannot start with an underscore. On this page:
tools/find_slots.yaml
The top of the file is the contract. For a local tool the function’s typed signature in tools/salon.py is the input, and its Pydantic return model is the output, so the file has no input: or output:. A webhook tool writes both as JSON Schema instead. Write the description as an instruction rather than a label, and let the types do real work: a Literal["haircut", "hair-color", "blowout"] on the service argument means the model cannot ask for a service the salon does not offer. The signature is also the whole argument list. Generated Pipecat tools turn an extra argument into a normal corrective tool result, so the model can retry with only the declared fields. A handler failure before any result is returned the same way, without exposing the private exception to the model or leaving a call stuck in progress. Keep workflow prerequisites in the prompt; do not make their names look like extra tool inputs in the description. The one block near the bottom is the execution: it says how the tool runs. Every tool file has exactly one.

The eight execution blocks

Exactly one, and the compiler holds you to it. A file carrying two blocks, and a file carrying none, are both refused with their line: see Troubleshooting.

The two gated blocks

client: and provider_hosted: exist in the schema and no target emits them. The capability table denies both on every provider, so writing one fails with the target named:
These blocks are unsupported. Use a supported tool execution type instead. Both blocks have no fields, but YAML still needs an explicit body: write client: {} or provider_hosted: {}.

How each target treats a tool

Twilio runs typed handlers, and the only builtin it takes is end_call. The target runs one agent with no tasks, and it refuses start: say and interruption: cancel. The Twilio target lists every refusal. On livekit and pipecat, the table is narrower: they compile only local:, webhook:, knowledge: and the end_call builtin: they refuse mcp:, slng: and every other builtin, so those are SLNG-only for now. The same tool file compiles to three different runtimes, and they do not all support the same things. This table is the capability table, which is what the compiler actually reads, so a no here is a refusal you will meet at compile time rather than a surprise on a call. Three of those rows are worth a sentence. local: reaches SLNG as a code tool, and webhook: does not. On an slng target, unmute deploy publishes a typed local: tool to SLNG. It needs a tools/<name>.sample.json beside it (details). A webhook: block is refused. An API request tool is created in SLNG and reached with slng:. Both blocks work as before on LiveKit and Pipecat. mcp: and slng: are SLNG-only for now. LiveKit and Pipecat do not compile them, and the compiler refuses them. Builtins other than end_call are refused there too. SLNG resolves a hosted reference by name at deploy time: no mirror, no hash, no unmute pull. See Hosted tools and MCP servers. knowledge: needs a runtime of ours to live in. LiveKit and Pipecat compile to a Python project, so the documents ride in the image and the search runs in the process. The SLNG target writes a deployment body and SLNG runs the agent, so there is no image to carry a folder and no process of ours to index it in. Put the facts in the agent’s instructions, or compile to a code target. client: and provider_hosted: are names with nothing behind them. They exist in the schema and no target emits them. They are documented so a refusal reads as a decision rather than a bug.

Attaching a tool to a task

A tool listed on a task rather than on an agent is a narrower thing, and two kinds are not available everywhere. Pipecat’s reason is the same for both: a task tool there is a flows handler holding a FlowManager, not a decorated function holding FunctionCallParams. SLNG takes no knowledge: tool anywhere. On SLNG a tool that only a task lists is attached hidden from the main agent.

The behavior fields

LiveKit runs a tool to completion, so a per-tool interruption value has nothing to act on and it says so. SLNG owns its own turn taking and has no per-tool setting at all.
Every refusal above names the target and tells you what to do instead. If you get one you do not understand, unmute validate prints the same message with the tool name attached.

Choosing a kind

Work down this list and stop at the first one that fits. Prefer fewer tools. Every tool is text in the model’s context on every turn, and two tools with overlapping descriptions is the most common reason a model calls the wrong one. A tool the agent list does not name is not offered at all, which is the cheapest way to narrow a choice. Write the description as an instruction, not a label. description and the input types are the whole of what the model knows. Say when to call it, and say when not to.

Fields inside each execution block

Each tool file declares exactly one execution block. Its page lists the fields, types, required conditions, and defaults:
  • Webhook: url_env, base_url, path, and auth.
  • Python: handler and dependencies.
  • MCP: server, url_env, transport, auth, and tools.
  • Prebuilt: id and instructions.
  • Knowledge: base.
  • Hosted: slng names a published tool.
client and provider_hosted take an empty object and are gated on every target. SLNG accepts published tool references and typed local: handlers, which deploy publishes as code tools. It refuses authored webhook bodies.

Which fields the block allows

The contract fields are shared, with one exception that matters:
string
Required for local, webhook, and knowledge tools: explains when the model should call the tool. Builtins use their registry description if omitted; hosted slng tools inherit their published description. Refused on MCP sources.
object
Required for authored webhook contracts: a JSON Schema with type: object describing model arguments. A local tool leaves it out, because its handler signature is the input. Refused on builtin, MCP, knowledge, and hosted slng tools, which own their schemas.
object
An author-side JSON Schema with type: object. Omit for no declared result schema. Used by save: rows and outcome matches; it is not a general runtime result validator or a model prompt. A local tool leaves it out, because its return model is the output. Refused on builtin, MCP, knowledge, and hosted slng tools.
list of one-key pairs
Hidden argument/value pairs. A value is state.variable, a scalar, or a string with {{state.variable}} placeholders. Omit to pass nothing. Legal on local, webhook, and hosted slng tools; builtin send_sms requires its literal from_number setting.
string
Accepts provider_default, continue, or cancel. Omitted means provider_default. Refused on MCP sources. Target support is listed above.
string
Accepts returns_data or ends_conversation. Omitted means returns_data, except builtins whose effect comes from the registry. Refused on MCP and knowledge tools.
list of strings
Written on the tool’s entry in agent.yaml, not in this file. A fixed spoken sentence with no {{variables}}. Omit for no line. Legal on local, webhook, knowledge, and hosted slng tools.
An mcp: file carries none of them, because the server owns each tool’s contract. A builtin: file takes no input or output. The registry supplies its contract and default description, and your description is added on top if you write one. A knowledge: file takes no input, output, with or effect either: the tool asks for one string and returns passages, so there is nothing to describe and nothing to merge into. It does take description and a start: say: line on its entry in agent.yaml, and you should write both.

The three behavior fields

tools/check_calendar.yaml
agent.yaml
string
Accepts provider_default, continue, or cancel. Omitted means provider_default. Refused on MCP sources. Target support is listed above.
string
Accepts returns_data or ends_conversation. Omitted means returns_data, except builtins whose effect comes from the registry. Refused on MCP and knowledge tools.
The start: say: line is the one of the three written on the tool’s entry in agent.yaml, as in the file listing above. All three are optional, and each is honored differently per target. Pipecat maps interruption onto its own cancel-on-interruption setting, while LiveKit runs tools to completion, so a non-default value warns there. effect is fixed by the registry on a builtin: tool, and a conflicting value fails.

Using start: say:

The line is spoken once, as the tool starts, before the tool’s own work. Nothing waits for it to finish playing, so the caller hears the tool’s answer no later than they would without the line. It covers the wait, it does not add one. Reach for it when the tool is slow enough that the silence reads as a dropped call. A request to a service you do not control, a handler that queries a database or a calendar. If the tool answers instantly, the announcement is just a sentence in the way. Do not put one on every tool. Two tools firing back to back means the caller hears two announcements in a row, which is worse than the pause you were trying to cover.
The sentence is fixed, so it is spoken word for word every time that tool runs. Everything else follows from that.Pick something that still sounds fine the third time. If the model calls a tool repeatedly within one conversation, that is an argument for not announcing it at all.
If your instructions already tell the agent to say it is checking something, delete that line when you add start: say:. Otherwise the model speaks its own version, the tool speaks the fixed one, and the caller hears both.
It adds no new interruption rule: if the caller speaks over the line, the tool’s own interruption: value decides what happens. LiveKit emits the line for a tool listed on an agent or on a task. A target whose driver has no lowering for the field fails validation with that driver’s own reason, rather than dropping the line quietly.

Define once, attach by name

Define each tool once. The full definition exists only in tools/<name>.yaml: description, input and optional output on a webhook tool, optional with, and one execution block. A local handler lives beside it in a Python file, and the block names its function as tools/<file>.py:<function>. Do not put any of those fields in agent.yaml. Every file in tools/ is a tool, so there is no list that loads them. Every tools: entry in an agent’s or task’s list in agent.yaml is a string name:
  • agents[].tools grants an agent access, and
  • agents[].tasks[].tools grants one nested task access.
A top-level tools: list in agent.yaml is only for a tool that needs behaviour on every agent. Each entry is a block with save:, outcomes:, hidden: or start:. A bare name there is refused.
agent.yaml
For a task-scoped tool, attach the same loaded name to the task instead, where the task is nested inside its agent:
agent.yaml
The agent and task lists are visibility scopes. Attach a tool only where it is called; do not grant it to both unless both really call it. Never replace a name with an inline mapping of description, input, output, local, or webhook. A tool’s optional output: remains its own JSON Schema. A task does not copy that schema. Its save: list derives finish fields from destination variables and saves only the values needed later.

Troubleshooting

A tool file has two execution blocks, or none

Every tool file runs exactly one way, so both shapes are refused. Two blocks:
None at all, where the message is also the list:
Fix: keep the block the tool really needs and delete the other, or add one of the blocks the second message lists.

A tool you wrote is never offered

A file in tools/ that no agent or task lists is loaded but nobody may call it. Fix: check the agent’s or task’s own tools: list.

output: is refused on an slng target

A local: tool’s Output comes from the return model of its Python function. A tool SLNG hosts carries its own Output model. So the field has nothing to set and is refused rather than dropped. Fix: delete the field from the tool file. A hosted tool carries its own Output model, written where the tool was written. Full story in Python tools and Hosted tools.

Where to go next

Webhook tools

The everyday case: call your own API.

Python tools

When the call needs code of your own.

MCP servers

Offer a whole server’s tools at once.

Prebuilt tools

The ones the runtime already has.

Knowledge bases

Answer from a folder of your own documents.