- the work is logic, not a request: a calculation, or a lookup in data you already have
- you are using a library already included in the generated project
- the request needs a signature, a custom header scheme, or a retry rule
local: handler is a typed function. The signature is the tool’s
input and the Pydantic return model is its output, so the tool file has no
input: or output:. Livekit and pipecat compile typed handlers. Twilio runs
the same typed handlers (the Twilio target). On an slng target,
a typed handler is wrapped into an SLNG code tool and published by
unmute deploy (details). SLNG refuses a handler with no
:function.
On this page:
- Quickstart - add one callable
- The block - the one key it takes
- The rules the function follows - four rules
- When the handler is slow - cover the wait
- Where it ends up - inside the generated project
- On the slng target - the sample file, and what deploy publishes
- Share a helper between tools - one source file, two callables
- Credentials, when a real handler needs one - names, never values
- Advanced - modules, dependencies, data, and SLNG limits
- Troubleshooting - the two worth reading
Quickstart
These files extend an existing package that declarescustomer_id. Add
cancel_appointment to the package tool list and the agent
tool list, then validate the package.
tools/cancel_appointment.yaml
tools/cancel_appointment.py
examples/salon-concierge is a local handler, and all of them share the one
tools/salon.py.
The block
string
required
file.py:function. The path is a Python file inside the package, and the name after
the colon is the function to call. Its signature is the tool’s input and its return
type is the output. A tool file with input: or output: next to a local handler
is refused.examples/salon-concierge, six tool files point at functions in tools/salon.py.
The rules the function follows
The handler above takes
customer_id and the model is never asked for it,
because customer_id comes from with:. The model never sees that value and cannot
invent one. Put a description on a parameter with
Annotated[str, Field(description="...")].
When the handler is slow
A local handler that reads a calendar or a database keeps the caller waiting the same way a webhook does.start: say: on the tool’s entry in agent.yaml gives the agent one fixed sentence
to speak as the handler starts. It does not wait for that sentence to finish, so the
result arrives no later than it would in silence:
tools/record_complaint.yaml
agent.yaml
examples/salon-concierge. Keep the line shorter than the
gap it covers: a long one runs into the answer and breaks its own promise of a
wait. The fixtures above carry no start:, because they return instantly and a
tool that speaks before doing nothing slow is just noise. Full rules in
the behavior fields.
Two more things decide whether a tool should carry one at all. The line is spoken
when the tool is called, not when it succeeds, so a tool that can refuse the
call will sometimes promise something it then does not do. The salon’s booking
tool refuses a save that arrives unconfirmed, and the caller heard “putting that
through now” followed by a question asking their permission. And only one line
belongs in a turn: a flow’s own say: and a tool’s start: say: fire together when
the flow’s first step calls that tool, which the caller hears as two promises to
go and look.
Where it ends up
unmute compile copies the file into the generated project. The generated code
hands the function to unmute-core, which reads its types and calls it as a plain
function. A function may be async def. The same handler file runs unchanged on
LiveKit and Pipecat.
On the slng target
Alocal: tool works on the slng target. unmute compile wraps the
handler into SLNG’s code tool file and writes build/slng/tools/<name>.json
and build/slng/samples/<name>.json.
Put a sample beside the tool file, tools/<name>.sample.json. It is one JSON
object of arguments:
tools/book_slot.sample.json
unmute deploy --target slng creates or updates the code tool, runs it once on
this sample, and publishes it. Then it attaches the tool by id and version.
SLNG publishes a code tool only after one run, so a local tool with no sample is
refused. A handler with no :function and an async def handler are refused too. SLNG
code has no internet access.
Share a helper between tools
Keep several tool functions and their helper in one authored file. Each tool file names its own function withfile.py:function, so you maintain the helper
once. This works on LiveKit and Pipecat.
Create these three complete files in an existing package:
tools/text_helpers.py
tools/count_words.yaml
tools/first_word.yaml
agent.yaml
Terminal
Import a helper from another file in the package only after you check that the
compiled project carries it. The compiler reads the file named by
local.handler.
Keep reusable functions in that one file, or move the operation behind a
webhook or MCP server.Credentials, when a real handler needs one
agent.yaml under
secrets:; keep the value in your environment or secret store.
Advanced
Dependencies, files, and regeneration
Handlers run from the generated project root,/app in the emitted images.
The runtime requirement lives in generated pyproject.toml, and the compiler
selects dependencies from the chosen providers. Python’s standard library is
available. Check that generated dependency list before importing another library.
A local.dependencies declaration is refused on both code targets. LiveKit’s
target pins only override recognized packages;
there is no supported package field for adding arbitrary third-party dependencies.
A library installed on your laptop does not become part of the deployed image.
Adjacent JSON files, templates, certificates, and other module trees are not
copied with a handler. Knowledge documents have their
own declared inclusion path; that is not a general-purpose file packaging hook.
For a small constant, keep it in the handler source. Otherwise use a remote service
until the package supports the files and dependencies your operation needs.
Edit the authored file and recompile to update its generated copies. Compilation
replaces added modules and changes to the generated Dockerfile or dependency
manifest. Unmute has no authored lifecycle-hook or custom build-file API.
What the SLNG sandbox expects
A tool SLNG hosts runs under these rules. That includes alocal: tool that
deploy publishes, and a tool you write by hand and reference with
slng:.
SLNG calls the handler itself, synchronously. A plain def that runs its
own event loop internally, with asyncio.run(...), is fine: only the entry
point has to be synchronous.
SLNG derives the schemas by introspection. A hosted code tool’s module
defines an Input model, an Output model and a handler() taking one
Input, and the platform reads the parameter and result schemas off those
classes. unmute compile writes the same shape for a local tool: it wraps your typed
handler in an adapter like this one. Livekit and pipecat do not use it:
handler() is what SLNG calls. A return value that does not fit Output fails
there rather than reaching the model unnoticed.
Troubleshooting
unmute validate warns that a credential is not declared
unmute validate reads the handler, finds the names it looks up, and warns when
agent.yaml forgot to declare one:
secrets: in agent.yaml. Declaring it removes the
warning and keeps the explicit inventory complete.
A hosted handler fails the moment it reaches the network
Code on SLNG has no internet access at all. A handler that importsrequests, httpx, urllib, urllib3, aiohttp, http.client or socket
and reaches the network fails at connection time inside the sandbox.
Fix: move that work into an API-request tool, which SLNG runs itself,
outside the sandbox.
Where to go next
MCP servers
Offer a whole server’s tools at once.
Secrets
Every seam a credential travels through.