What it needs
Two things. Thevoiceai CLI, on your PATH. SLNG hosts the agent, and voiceai is the
tool that owns your account and the push. Unmute opens no connection to the SLNG
agents API itself, at compile time or any other time.
SLNG_API_KEY is read first, then VOICEAI_API_KEY, then whatever
profile voiceai login stored. One SLNG key serves every SLNG role, so this is
the same key a generated livekit or pipecat project reads at run time.
Usage
slng: reference needs no unmute pull first, no mirror, and no hash:
unmute deploy resolves it directly against your organisation, checks it,
and attaches the version it checked. This needs a voiceai release that
supports that checked, resolved push; an older one is refused with upgrade
guidance before anything is written. See Hosted tools.
A clean run reads:
validate’s own row, and the sentence after it is there
because a clean local result is narrower than a clean deploy: it says which
checks this command has not made.
Each attached line names a reference this run resolved and checked against
your organisation, never one it created. Every tool it names already existed,
published, before this run started. v<n> is the version this run checked and
attached, which is always the latest published one. An MCP selection carries no
version, because a server tool has none; what was checked for it is the schema
hash SLNG’s own snapshot recorded. The requirements satisfied count is the
same account read, folded in with every Vault entry, MCP server and builtin the
package needs.
Only a slng target is deployed. A livekit or pipecat target compiles to a
project that somebody else’s platform runs, so it is unmute compile plus that
platform’s own deploy step. Deploying a package with no slng target names the
block that would add one.
The organisation is printed on every run because an exported key and a stored
profile can belong to different ones, and nothing else on screen would tell you
which you just wrote to.
Four stages
Validate. The same checks asunmute validate,
against the slng target only. The slng target refuses what SLNG will not run,
and hearing it here costs no network call.
Preflight. Your organisation is asked what it already has, and the answer is
compared with what the package needs. A real run can refresh an unusable MCP
snapshot or fill a missing Vault entry with consent before later checks finish.
A dry run does neither.
Compile. The same output as unmute compile --target slng,
written to build/slng/. Compiling as part of deploying is deliberate: it means
you cannot push an artifact that is older than the package.
Push. Checked references are staged with their resolved IDs and versions,
then passed to voiceai agents push with --require-resolved and --expect-org.
A direct push of build/slng skips Unmute’s binding checks and resolved staging;
use unmute deploy for this workflow.
What the preflight checks
Four kinds of read, plus one per MCP server your package names and one perslng: reference, to check the published version it would attach. The cost
does not grow with how many secrets you declare.
Unmute creates no tools or MCP servers. SLNG owns tool creation entirely, and
an authored
local: or webhook: block is refused before this step is
reached. So there is no first-deploy grace period where an absent tool is
expected. Every name this preflight checks already exists, published, or the
run stops.
A control is never checked either. It reaches SLNG as a curated capability you
attach to the agent in the dashboard, and the compiled body carries no reference
to it, so there is nothing for a check to resolve.
When a check cannot be made
Two different things can go wrong here, and they are not treated the same. An oldvoiceai stops the run before any account read, because it cannot
make the checked, resolved push this command promises. It is refused with
upgrade guidance rather than falling back to a push that resolves and attaches
whatever is newest, unchecked. See Deploy to SLNG.
A read that fails once the run is under way splits by what the rest of the
run still covers, and it splits per requirement rather than per listing. Each
one says what covers it:
- The push checks a name and its kind. It never reads whether the entry holds a value, so neither run can tell you that.
- The push reads the names in your package: the
{{$NAME}}tokens in your prompts and the credentials on your own tool bodies. A credential discovered from a hosted tool’s published contract, or from a hosted MCP server’s connection, is in neither, so it reaches no later check and an unreadable vault blocks it here.
unmute deploy
attaches the published version and the MCP schema hash it resolved, under a
mode that tells the push to check neither itself, so a read that failed here
has no later step to catch it:
Creating missing secrets
Secrets are the only thing unmute can write, so they are the only gap it offers to close:.env is piped to voiceai secret create on standard input. If there is none, the terminal is handed to
voiceai secret create, which prompts with the input masked.
Either way the value never reaches a command line, a file unmute writes, or your
screen. There is no --value flag anywhere in this path, deliberately: an
argument lands in shell history and is visible in ps.
A run with no terminal, such as CI, prompts for nothing. It prints the command
that would fix each entry and exits non-zero.
An entry that exists under the other kind, a variable where you need a secret,
is reported as a mismatch and is not offered a fill: the name is taken, so
creating it again would be refused.
After a successful push
The run reports attached numbers from your organisation’s SIP trunks. It does not verify the carrier’s routing or place an inbound test call:PATCH on the agent, so it disturbs nothing else, and it
runs after the push, so re-running a deploy offers it again rather than
leaving you with a silent number.
Anything other than a listed number leaves it unattached, and a run with no
terminal never asks and never attaches: a deploy that quietly claimed a phone
number would be somebody’s phone bill.
Unmute buys no numbers and configures no carrier routing. Set up the connection
in SLNG and route the carrier number to its SIP destination. Attachment alone
does not redirect a number from Twilio Dev Phone or another webhook. Follow
Receive phone calls, then confirm a real
call appears in SLNG. Required injected inputs need valid defaults for inbound
calls, since the carrier supplies no web-session arguments.
--call places one outbound call from the agent you just deployed, which is how
you hear a phone agent without waiting for someone to ring it:
A push replaces
Updating an agent replaces it with what the package declares. A tool reference the package no longer names is detached, and a field that differs from the live agent is overwritten.--dry-run names both, and changes nothing:
attached, because nothing was:
v<n>, from v<m> is a reference the agent already has at an older version,
and v<n>, new is one it does not have at all. A first deployment shows no
previous version rather than inventing one.
The indented lines are what a replacement would alter on that attachment. They
exist for the settings only the dashboard could have added: a description typed
there, an invocation switched to system, a call_start trigger, a system
argument, or an argument override the package no longer supplies. Each is named
against the attachment it belongs to rather than left for the push to discard
silently.
A setting the package can declare is compared, not listed. The sentence
spoken before a tool runs is announce: in the tool file, so a package whose
announcement already matches the agent’s shows no line for it, and one that
differs shows the sentence being replaced. Only the parts of that setting a
package has no key for, such as making the agent wait for the sentence, are
reported as things a replacement clears.
A detached reference is named by the identifier the live agent carries for it,
because a tool the package no longer mentions may be knowable by nothing else.
The published-description and published-parameter lines say the two versions’
contracts differ. They say nothing about whether the tool behaves the same: a
schema comparison cannot see a change that kept the same signature, and this
output does not pretend otherwise.
An agent’s name comes from name: in agent.yaml joined to the target, so a
run that resolves to update when you expected create means an agent of that
name already exists. unmute deploy warns and names it; change name:, or let
--agent-id pick a different one.
When it refuses
A refusal blocks the agent push. A real deploy may already have completed MCP refresh or consented Vault writes; inspectdeploy-report.json for those
changes. A dry run changes no remote state.
The problems you will meet most:
These problems stop the agent push.
no mirror of it is committed and does not match the hash are not on this list. unmute deploy compiles only the
slng target, which reads no mirror. Those two refusals belong to a livekit or
pipecat compile of the same package. See Hosted tools.
Trying a hosted tool
No sample is needed to deploy references to published tools. To exercise one separately, use the same account and supply arguments matching its contract:check_order with an order_number
parameter; change the name and input for your own tool.
Where to go next
Test the deployed agent
Test in the browser, configure phone routing, and read call results.