Skip to main content
Some tools need code rather than a request. Write the function in your package and name it from the tool file. Reach for a Python handler when:
  • 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
If it is just an authenticated HTTP call, use a webhook tool instead. It needs no code from you and no code review. A 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

These files extend an existing package that declares customer_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
Those two are a worked example rather than a quote: they are the smallest shape that shows a handler receiving a value from State. For real ones, every tool in 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.
The function does not need the tool’s name, and one file can hold many tools. In 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
That one is real, from 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

A local: 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 with file.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
Merge these names into the agent’s list; keep the other agent fields:
agent.yaml
From the package directory, compile the declared targets:
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

Only the name belongs in the file. Put that name in 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 a local: 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:
The literal lookup also makes the compiler include the name in generated environment instructions and the startup check even if the declaration is missing. Fix: add the name to 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 imports requests, 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.