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. description and input are everything the model knows about this tool. Write the description as an instruction rather than a label, and let the schema do real work: the enum above means the model cannot ask for a service the salon does not offer. The schema 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:
They are listed here so you know the names mean nothing yet, and so a refusal you meet reads as a decision rather than a bug. Both blocks have no fields, but YAML still needs an explicit body: write client: {} or provider_hosted: {}.

How each target treats a tool

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: and webhook: no longer reach SLNG. The platform owns a tool’s code, version and gate pipeline, and unmute creates no tool there. A tool your SLNG organisation already has is reached with slng:; a brand new one starts in the SLNG dashboard. Both blocks work exactly as before on LiveKit and Pipecat. slng: needs nothing local, and works everywhere because the definition travels when it has to. SLNG resolves a hosted reference by name at deploy time: no mirror, no hash, no unmute pull. LiveKit and Pipecat build and run the tool themselves, so they need a real copy; unmute pull is what fetches one, and for a code tool its module, into your package, with no network needed again after that. One limit is not in this table: a hosted tool that declares Python dependencies is refused on LiveKit and Pipecat, which build one dependency list for the whole project. See Hosted tools. 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. The SLNG target writes one agent with one prompt, so it has no tasks to scope anything to.

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 input 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; it refuses authored local and 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 local and webhook contracts: a JSON Schema with type: object describing model arguments. 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 assignments and success checks; it is not a general runtime result validator or a model prompt. Refused on builtin, MCP, knowledge, and hosted slng tools.
list of one-key pairs
Hidden argument/value pairs. Values are scalars or strings with {{variable}} placeholders. Omit to inject 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.
string
A fixed spoken sentence with no {{variables}}. Omit for no announcement. 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, inject 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 announce, and you should write both.

The three behavior fields

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.
string
A fixed spoken sentence with no {{variables}}. Omit for no announcement. Legal on local, webhook, knowledge, and hosted slng tools.
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 announce:

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 announce:. 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, optional output and inject, and one execution block. A local handler lives beside it in tools/<handler>.py. Do not put any of those fields in agent.yaml. Every tools: entry in agent.yaml is a string name:
  • the top-level list loads tools/<name>.yaml,
  • agents.<name>.tools grants an agent access, and
  • agents.<name>.tasks[].tools grants one nested task access.
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 assign: 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 the package level list does not name is not loaded at all, and nothing complains. Fix: check the top-level tools: list in agent.yaml first, then the agent’s or task’s own list.

output: is refused on an slng target

It used to be allowed there, for a reason. The compiler turned it into a pydantic Output class inside the code it uploaded, and SLNG read the tool’s result shape off that class. unmute uploads no code now, so the field reaches nothing and is refused rather than dropped. Fix: delete the field from the tool file. A tool SLNG hosts 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.