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:
- The eight execution blocks - the ways a tool runs
- How each target treats a tool - what each one supports
- Choosing a kind - stop at the first fit
- Fields inside each execution block - every key, per block
- Which fields the block allows - the shared contract fields
- The three behavior fields - announce, effect, interruption
- Define once, attach by name - one definition, two lists
- Troubleshooting - the refusals you will meet
tools/find_slots.yaml
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:
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, andauth. - Python:
handleranddependencies. - MCP:
server,url_env,transport,auth, andtools. - Prebuilt:
idandinstructions. - Knowledge:
base. - Hosted:
slngnames 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.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.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.
Writing the line
Writing the line
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.
The mistake worth avoiding
The mistake worth avoiding
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.The rules
The rules
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 intools/<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>.toolsgrants an agent access, andagents.<name>.tasks[].toolsgrants one nested task access.
agent.yaml
agent.yaml
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:A tool you wrote is never offered
A file intools/ 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.