Docs · Quickstart
Test your voice agent on a real call
callerlab dials your agent through your own Twilio account, plays a realistic caller, has a judge model give a verdict and can time the call from an optional recording. Everything starts as a dry run that dials nothing.
Install
Run callerlab from a clone; it isn’t on npm yet. Needs Node.js 22.6 or newer.
git clone https://github.com/RobStrayer/callerlab.git cd callerlab npm ci npm link # puts `callerlab` on your PATH pipx install edge-tts # free default voice, online (Microsoft Edge); needs ffmpeg npm run dash # local demo dashboard with sample data # what works now, what a real call is missing callerlab doctor
Step 1: Dry run
First set CALLERLAB_DIAL_TO to a number you own or may test (E.164). The dry run plans the calls to it and prints the projected spend and its bound. Free, and dials nothing.
# first: export CALLERLAB_DIAL_TO=<a number you own, E.164> # prints the plan, the projected spend and its bound; # dials nothing callerlab explore --pack examples/packs/home-services.json \ --to "$CALLERLAB_DIAL_TO"
explore plans and, with --yes, places calls; a model holds the conversation, then the judge scores the transcript. Options:
--temperamenteasygoing·impatient·pushy·argumentative·relentless·heated--bednoise bed, e.g.car-cabin--snrsignal-to-noise in dB, e.g.0--repeatN--max-usdmodeled run-cost cap in US dollars; no default--recordsave a local two-channel WAV on the stream transport
Step 2: First real call
What it needs, then the same command with --yes.
| Need | Set with |
|---|---|
| Twilio account and credentials | TWILIO_ACCOUNT_SID + TWILIO_AUTH_TOKEN (validates callback signatures) |
| Public HTTPS tunnel to 127.0.0.1:8787 | cloudflared tunnel --url http://127.0.0.1:8787 → CALLERLAB_PUBLIC_URL |
| A destination you may call | CALLERLAB_DIAL_TO or CALLERLAB_ALLOWED_DESTINATIONS |
| Caller ID and harness secret | CALLERLAB_DIAL_FROM, CALLERLAB_HARNESS_SECRET |
| Speech-to-text endpoint (for the default streaming transport) | CALLERLAB_STT_BASE_URL, CALLERLAB_STT_MODEL, CALLERLAB_STT_API_KEY. Without them a run falls back to deprecated Gather |
| Caller model (for explore) | CALLERLAB_MODEL_BASE_URL, CALLERLAB_MODEL_API_KEY |
# places real, paid calls through your Twilio account # no default spend cap: add --max-usd <dollars> callerlab explore --pack examples/packs/home-services.json \ --to "$CALLERLAB_DIAL_TO" --yes
--yes enables paid calls. Confirm permission and consent first.To measure reply gaps and conversation timing, configure a speech-to-text endpoint and add --transport stream --record. Recording is off by default; without it those timing measures are not available.
There is no default spend cap. --max-usd caps modeled run cost, not every provider invoice.
Step 3: Score it
Both run offline on the report a real call wrote, so they cost nothing.
callerlab scorecard --report .cache/reports/<stamp> callerlab ci --report .cache/reports/<stamp> --fail-on fail=1
callerlab scorecard
callerlab ci
Agent quickstart
Coding agents can install callerlab, dry-run a pack and read results. The skills carry callerlab’s calling rules.
Agents should dry-run the exact plan first and wait for your go-ahead before placing paid calls.
/plugin marketplace add RobStrayer/callerlab /plugin install callerlab@callerlab
npx skills add RobStrayer/callerlab
# read-only stdio server: list/read runs, compare, # test a --fail-on gate, price a call callerlab mcp
Voice-agent testing uses callerlab (run `callerlab help`).
- Run this without --yes first, and show the dry run to the human:
callerlab explore --pack examples/packs/home-services.json \
--to "$CALLERLAB_DIAL_TO"
- Add --yes only after the human says go, for this exact plan.
- Call only numbers the human owns or has written permission to test.
- Never read or print .env or any key.
- Report calls, conditions and judge model. One call is not a rate.Prompts that work
Gate a build
callerlab scorecard --report .cache/reports/<stamp> \ --require "task.pass.low>=0.7,speed.p95<=3500" callerlab ci --report .cache/reports/<stamp> \ --fail-on fail=1,flaky>=1
Both write summary.json and junit.xml beside the report. A judged fail does not set the exit code on its own; --fail-on decides that. A call that fails as a call, or is unjudged in a gated run, can still exit 1 or 2. For a workflow to copy, see the GitHub Actions example in docs/ci.md.
Exit codes
| Exit | Meaning | Agent should |
|---|---|---|
0 | No gate tripped; may be ungated or not evaluated | Read the verdict |
1 | A gate or call failed, or the run ended early | Read the output |
2 | No verdict, including configuration or evaluation problems | Read the cause and fix it |
3 | explore watchdog ended a stalled step | Read which step |
4 | Inconclusive: a call hit its time limit without a defect | Consider a longer call |
75 | live or explore: only the phone line failed, or line outcomes stopped the run | Try later, not immediately |
Environment
Every flag and default is in docs/CLI.md. CALLERLAB_EDGE_PYTHON can only come from your shell environment, not a project’s .env.
| Variable | What it sets | Needed for |
|---|---|---|
TWILIO_ACCOUNT_SID | Twilio account | Live dials |
TWILIO_AUTH_TOKEN | Validates X-Twilio-Signature; enables caller keypad | Live dials |
CALLERLAB_PUBLIC_URL | Your HTTPS tunnel URL | Live dials |
CALLERLAB_DIAL_FROM | A Twilio number you own | Live dials |
CALLERLAB_DIAL_TO / CALLERLAB_ALLOWED_DESTINATIONS | Numbers you may call (E.164, comma-separated) | Live dials |
CALLERLAB_HARNESS_SECRET | Signs step tokens | Live dials |
CALLERLAB_MODEL_BASE_URL / _API_KEY | Caller model (and judge by default) | explore, judge |
CALLERLAB_JUDGE_BASE_URL / _API_KEY | Separate judge endpoint | Optional |
CALLERLAB_TTS_PROVIDER | edge (the free voice, used when no other voice key is set); see docs/CLI.md for other providers | Optional |
CALLERLAB_STT_BASE_URL / _MODEL / _API_KEY | Speech to text; all three or none | stream, transcribe |
CALLERLAB_FAIL_ON | Default --fail-on gate | CI |
CALLERLAB_ALERT_WEBHOOK | Canary state changes (https only) | Canaries |