
Retell CLI: Ship Voice Agent Updates Without Breaking Prod
The Retell CLI (@retell-ai/retell-cli) can run your whole voice agent release from a terminal, as long as you treat versions the way Retell does: publishing locks a version for good, so every change starts with create-agent-version, not update-agent. I ran version 0.47.0 against throwaway agents today, broke it on purpose a few ways, and ended up with a 10-line deploy script that works. Below are the exact errors you'll hit and the one shell habit that quietly corrupts bulk updates.
What Is the Retell CLI?
It's Retell's official command-line client, announced in their August 3, 2026 changelog. Install it with npm install --global @retell-ai/retell-cli (Node 22 or later), then run retell auth login. The official docs page is about 40 lines long and stops at retell list-agents, which is why I'm writing this.
The first thing worth knowing: it isn't hand-written.Every command is generated from Retell's OpenAPI catalog. Version 0.47.0 ships 110 commands, one per API operation, named after the endpoint: create-agent, update-phone-number, list-calls, create-batch-test and so on. The package has had 66 releases since July 22, and 0.47.0 landed on November 1. Pin the version in CI. It moves weekly.
The second thing: it's thin. There are no retries, no auto-pagination (you pass --pagination-key yourself), a 60-second request timeout, and every failure exits with code 1 and prints Retell API request failed (422): {...}to stderr. That's fine for scripts. It's just not a framework.
How Does Retell CLI Authentication Work in CI?
Set RETELL_API_KEYand you're done. I read the credential code to confirm the order: the environment variable wins, then the OS keychain (where auth login stores the key), then ~/.config/retell/credentials.json with 0600 permissions as a fallback. retell auth whoami tells you which one it picked (mine said Source: env).
On headless Linux runners with no keychain, the login falls back to the file on its own. If you want to skip the keychain entirely, RETELL_DISABLE_KEYRING=1 does that, and RETELL_CONFIG_DIR moves the file. In CI, use the env var and nothing else, so a stale login on a self-hosted runner can never shadow the key you meant.
Why Does update-agent Fail After You Publish?
This is the one everybody hits. Here's the exact sequence I ran on a throwaway agent, in order:
| # | Command | What came back |
|---|---|---|
| 1 | create-agent | v0, is_published false |
| 2 | publish-agent-version --version 0 | prints null, exit 0 |
| 3 | update-agent (any field) | 422: Cannot update published agent other than version title |
| 4 | create-agent-version --base-version 0 | v1 draft, LLM bumped to v1 |
| 5 | update-agent | 200, edits v1 |
| 6 | get-agent (no --version) | returns v1, the draft |
| 7 | publish-agent-version --version 1 | v1 and LLM v1 published |
| 8 | delete-agent-version --version 0 | 400: Cannot delete agent version 0. |
The help text says update-agentupdates "the latest draft version." Once you publish, there is no draft, so you get 422 Cannot update published agent other than version title. Passing --version 0 doesn't help either. That matches Retell's versioning docs: publishing makes a version read-only, and you keep working in a new draft created from it.
Three details from that run that the docs don't spell out:
- The LLM follows the agent. Creating the v1 draft also moved the attached Retell LLM from version 0 to version 1, and publishing the agent published that LLM version. One publish, not two.
get-agentreturns the draft. Without--versionit gave me v1 while v1 was still unpublished. Any script that readsget-agentto report "what's live" is wrong the moment someone opens a draft.- Publishing prints
null.Exit code 0, empty body. Don't parse it withjqexpecting an object; confirm withlist-agent-versions.
Version 0 can't be deleted at all (Cannot delete agent version 0.). To clean up a test agent, delete-agent removes every version, then delete-retell-llmremoves the response engine, which isn't deleted with it.
How Do You Deploy a Retell Agent Change From the CLI?
Keep the agent config in git as JSON, and let a script turn it into a new published version. This is the script I ran twice against a test agent today. It went v0 to v1 to v2, and get-agent --version 1 and --version 2 came back with the voice speeds each config set:
#!/usr/bin/env bash
# deploy-agent.sh <agent_id> <config.json> "<version title>"
set -euo pipefail
AGENT="$1"; CONFIG="$2"; TITLE="$3"
exec </dev/null # the CLI reads stdin as a request body when it isn't a TTY
BASE=$(retell list-agent-versions "$AGENT" --output jsonl \
| jq -s '[.[] | select(.is_published)] | max_by(.version) | .version')
DRAFT=$(retell create-agent-version "$AGENT" --base-version "$BASE" | jq .version)
retell update-agent "$AGENT" --version "$DRAFT" --body "$CONFIG" > /dev/null
retell publish-agent-version "$AGENT" --version "$DRAFT" --version-title "$TITLE" > /dev/null
echo "published v$DRAFT (from v$BASE)"Two things make it work. --body config.json sends the whole file, and any flag you add on the same command overrides that field (I checked with --dry-run: a file with voice_speed 1.0 plus --voice-speed 1.15 sent 1.15). And the exec </dev/null line, which gets its own section below.
Publishing doesn't move traffic by itself. Phone numbers point at agents through inbound_agents and outbound_agents, each entry with a weight and an optional agent_version. The schema accepts a number, latest, latest_published or a tag name. I validated this with --dry-run, not against a live number:
retell update-phone-number +15555550100 \
--inbound-agents '[{"agent_id":"agent_abc","agent_version":3,"weight":1}]'The CLI can't move environment tags. Every agent gets prod and staging tags, and the dashboard lets you move prodto a new version so every number on that tag follows. There's no tag command in 0.47.0. So pick one: bind numbers to explicit versions and let CI update them, or keep numbers on prod and make the tag move a human click after the script publishes. I prefer the second for anything that takes real calls. The click doubles as a release approval, and rollback is moving the tag back.
Before that click, put a real test in between. The CLI exposes create-batch-test and the test-case commands, and a scripted test call beats finding out from a customer. I covered what to listen for in my post on agents that keep interrupting.
Why Does a Bulk Update Loop Stop After One Agent?
This is the trap I'd have shipped if I hadn't read the source. Any command that sends a JSON body reads stdin as that body whenever stdin isn't a terminal. In a normal while read loop, stdin is your input file. So the first update-agent swallows every remaining line:
# agents.txt has three agent IDs, one per line
while read id; do
retell update-agent "$id" --voice-speed 1.05
done < agents.txt
# iteration 1: Error: Unexpected token 'a', "agent_two
# "... is not valid JSON
# iterations 2 and 3 never runThat's the loud version. The quiet one is worse: if the leftover lines happen to be valid JSON, they get merged into the request. In a dry run, a loop whose input contained {"agent_name":"hijacked-by-stdin"} produced a body with that name next to the voice_speed I actually asked for. Imagine that on a loop over 40 client agents.
The fix is one redirect:
while read id; do
retell update-agent "$id" --voice-speed 1.05 < /dev/null
done < agents.txtWith it, the same loop ran all three iterations. Read-only commands with no body, like get-agent, aren't affected; they ran 3 of 3 without the redirect. I still add it everywhere, because I won't remember which commands have bodies at 11 PM.
Is --dry-run Worth Using?
Yes, more than I expected. It prints the method, URL, headers (with the key shown as Bearer [REDACTED]) and body, and it validates the body against the OpenAPI schema locally before anything goes out. That caught my first mistake in about 100 ms:
$ retell create-retell-llm --dry-run --start-speaker agent \
--general-prompt "You are..." --model gpt-5.6
Error: Enter a valid JSON value
$ retell create-retell-llm --dry-run --start-speaker agent \
--general-prompt "You are..." --model-config '{"primary":"gpt-5.6"}'
Error: OpenAPI validation failed: requestBody.model_config does not match
any allowed schema; requestBody.model_config.primary must be object
# what it wants:
--model-config '{"type":"cascading","primary":{"model":"gpt-5.4-mini"}}'The validator also lists every allowed model when you get the name wrong, which is faster than hunting through docs. Note that gpt-5.6 on its own isn't valid; the catalog has gpt-5.6-terra and gpt-5.6-luna.
What it can't catch is anything the server decides. An empty llm_id passed local validation and came back from the API as a 400. Dry runs check shape, not meaning.
Speed is a non-issue: a dry run took 0.10 s median over five runs on my Mac mini, and a real get-agentround trip 0.28 s. That's fast enough to call it in a loop over every agent in a workspace.
Retell CLI vs SDK vs Dashboard: Which One When?
My split: the dashboard for building and listening to calls, the CLI for anything repeatable (deploys, bulk changes, pulling call lists into a terminal), and the SDKfor code that runs inside a product. Retell's docs draw the same line. If you're coming from the dashboard, my Retell AI setup guide covers the build side, and the Web SDK v3 migration notes cover the browser side.
The bigger point is that voice agents are config, and config belongs in git. I made the same argument for Claude agents in agents as code with ant apply. Retell's draft-and-publish model maps cleanly onto it. Each merge becomes a published version with a title you can trace back to a commit, and rollback means pointing at the previous number.
Frequently Asked Questions
Why does Retell say 'Cannot update published agent other than version title'?
Because the newest version of that agent is published, and published versions are read-only. update-agent edits the latest draft, and there isn't one. Run retell create-agent-version <agent_id> --base-version <N> first, then update the new draft.
Does publishing a Retell agent also publish its LLM?
In my test, yes. Creating a draft from version 0 moved the attached Retell LLM to version 1 as well, and publishing the agent's version 1 flipped that LLM version to published. You don't need a separate LLM publish step for a single-prompt agent.
Does retell get-agent return the live version?
No. Without --version it returns the newest version, which can be an unpublished draft. If a script needs to know what's live, read list-agent-versions and filter on is_published, or ask for an explicit --version.
Can the Retell CLI move the prod and staging environment tags?
Not in version 0.47.0. There's no tag command in the CLI. A phone number can be bound to a tag by name through update-phone-number, but moving the tag itself is a dashboard action. In CI, bind numbers to explicit version numbers instead.
Should I use the Retell CLI or the SDK?
Retell's own docs say the CLI is for managing resources and the SDK is for application code. I agree: the CLI fits deploy scripts, CI jobs and quick checks, while anything that runs inside your product, like starting calls for users, belongs in the TypeScript or Python SDK.
How I Tested This
November 2, 2026, @retell-ai/retell-cli 0.47.0 (catalog v4) installed locally on a Mac mini in Allen, Texas, Node 26, authenticated with RETELL_API_KEY. Every write went to throwaway agents and Retell LLMs created for the test and deleted at the end (a final get-agent returned 404). No phone numbers or production agents were touched, and no calls were placed. The phone-number binding was checked with --dry-runonly. Auth order, timeouts and the stdin behavior come from reading the package's dist/src/runtime files and then reproducing each one.
Running More Than a Handful of Voice Agents?
I set up Retell agents with versioned configs, test calls before release, and a rollback that takes one step. Tell me how many agents you run and how you ship changes today.
Related Posts
Voice AI
Retell Voicemail Detection Not Working? 6 Real Causes
Retell voicemail detection is off on API-created agents until you set voicemail_option, deaf when the agent language doesn't match the greeting, and blind when the agent talks first. An audit of 185 of my outbound calls, plus the IVR, 3-minute window and ring_duration_ms traps.
Voice AI
Retell Web SDK 3.0: Migrate RetellWebClient Before Oct 18
retell-client-js-sdk 2.x and /v2/create-web-call are deprecated on October 18, 2026. I tested v2 vs v3 create-web-call side by side: upgrade the browser first, then the server, and rebuild the talking indicator because v3 calls don't emit agent_start_talking.
Voice AI
Retell SIP Endpoint Retired: Move to sip.retellai.com
Retell retired the legacy 5t4n6j0wnrl.sip.livekit.cloud SIP host on 30 September 2026. The fix is one hostname in your Twilio, Telnyx or PBX origination URI, but Retell never stores that URI, and the old host still answered SIP OPTIONS on retirement morning.