Build voice agent
Agent Skills for the Patter SDK — give your AI agent a phone number. Works in Claude Code, Cursor, OpenClaw, Hermes Agent, Codex, Cline, Goose, Amp, Windsurf, and any harness that consumes the Agent Skills standard. One CLI: npx skills add patterai/skills
npx -y skills add PatterAI/skills --skill build-voice-agentAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Build a production voice agent with the Patter SDK — receive or place real phone calls answered by an AI agent. Use when the user wants to make a phone bot, IVR replacement, voice receptionist, outbound caller, survey bot, customer-support voice agent, voice-AI demo, AI cold-caller, or any flow where an LLM talks over the telephone. Covers the three Patter modes — OpenAI Realtime / Realtime2 (GA, lowest latency), ElevenLabs ConvAI (turn-taking), and the STT→LLM→TTS Pipeline (mix any providers) — in both Python and TypeScript. Trigger this skill even if the user only says "phone bot", "voice AI", "call my customers", or "answer the phone with AI" without naming Patter.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
14.0 KB, as published. Nobody here has run it
Build a voice agent with Patter
Patter is an open-source SDK that turns any AI agent into a phone agent. Pick one of three architectures (Realtime, ConvAI, Pipeline), describe the behaviour in a system prompt, and run a local server that connects to your Twilio or Telnyx number — no Patter Cloud, no managed service. The four-line pattern below is the entire surface.
Architecture in one diagram
┌────────────────────────────────────────────────────────────────────┐
│ Patter server (local) │
│ ┌────────────────┐ ┌───────────────────┐ ┌───────────────┐ │
│ │ Carrier WS │───▶│ Agent (engine / │───▶│ Carrier WS │ │
│ │ in (audio in) │ │ pipeline) loop │ │ out (TTS out) │ │
│ └────────────────┘ └───────────────────┘ └───────────────┘ │
│ ▲ │ │
│ │ Built-in tools: │ │
│ │ transfer_call · end_call │ │
└───────────┼──────────────────────────────────────────────┼──────────┘
│ │
│ WSS (mulaw 8kHz / pcm 16kHz) │
▼ ▼
┌──────────┐ ┌──────────┐
│ Twilio │ ──────── real phone call ───── │ User │
│ Telnyx │ └──────────┘
└──────────┘
You write: system prompt, optional tools, optional guardrails, choice of engine. Patter writes: WebSocket framing, audio transcoding, barge-in, VAD, cost tracking, tunnel, dashboard.
Decide the mode
| Mode | When to use | Latency | Cost |
|---|---|---|---|
OpenAIRealtime2 (GA, default) | Default for everything. Lowest latency, single key. | ~200 ms turn | $$$ |
OpenAIRealtime | Legacy gpt-realtime-mini; pick only if explicitly required. | ~200 ms turn | $$ |
ElevenLabsConvAI | Turn-taking conversations (slower but better at long pauses). | ~600 ms turn | $$ |
Pipeline | Custom STT/LLM/TTS combinations. Use when the user wants Anthropic Claude / Cerebras / Whisper / a specific TTS voice. | ~800 ms turn | $-$$$$ |
Default pick: OpenAIRealtime2. Switch only if the user explicitly asks for
custom voice/LLM, lower cost, or a non-OpenAI provider.
Quick start
Python
import asyncio
from getpatter import Patter, Twilio, OpenAIRealtime2
async def main():
phone = Patter(carrier=Twilio(), phone_number="+15550001234")
agent = phone.agent(
engine=OpenAIRealtime2(),
system_prompt=(
"You are Mia, the AI receptionist for Acme Plumbing. "
"Greet the caller warmly. Help them book a service visit. "
"Keep replies under two sentences."
),
first_message="Hi, this is Mia at Acme Plumbing — how can I help?",
)
await phone.serve(agent, tunnel=True)
asyncio.run(main())
TypeScript
import { Patter, Twilio, OpenAIRealtime2 } from "getpatter";
const phone = new Patter({
carrier: new Twilio(),
phoneNumber: "+15550001234",
});
const agent = phone.agent({
engine: new OpenAIRealtime2(),
systemPrompt:
"You are Mia, the AI receptionist for Acme Plumbing. " +
"Greet the caller warmly. Help them book a service visit. " +
"Keep replies under two sentences.",
firstMessage: "Hi, this is Mia at Acme Plumbing — how can I help?",
});
await phone.serve({ agent, tunnel: true });
Both expose a tunnel URL on startup (tunnel ready: https://abc.trycloudflare.com).
Point your Twilio number's voice webhook at that URL (see configure-telephony),
call the number, talk to Mia.
Pick the right mode
The default works for 80% of agents. Switch when:
| Want this | Pick this mode | Reference |
|---|---|---|
| Lowest possible latency, OpenAI voice | OpenAIRealtime2 (default) | references/realtime-mode.md |
| Better long-pause handling, ElevenLabs voice | ElevenLabsConvAI | references/convai-mode.md |
| Mix custom STT / LLM / TTS (Anthropic, Deepgram, Cartesia, …) | Pipeline | references/pipeline-mode.md |
| Save cost on high-volume outbound | Pipeline with Cerebras LLM + Deepgram STT + ElevenLabs Turbo | references/pipeline-mode.md |
| Noisy line / speakerphone (denoiser, high-pass, AGC, semantic turn detection) | Pipeline audio levers | references/pipeline-mode.md |
Read the corresponding reference only when the user picks that mode — do not preload all three.
Outbound calls
The same Patter instance places outbound calls — but phone.call() always
needs a running server to dial through (raises PatterConnectionError
otherwise), and you choose how the call's lifecycle is reported:
wait=True(completion-aware, since 0.6.3) —call()blocks until the callee hangs up and returns aCallResultwith theoutcome, duration, transcript, and cost. This is what you want for scripts and campaigns: oneawait, one resolved call.wait=False(default, fire-and-forget) —call()returns at the moment the carrier dials (not at hangup) and yieldsNone/void. The call then lives entirely inside the running server — so something has to keep that server alive (a long-runningserve(), or theasync with/await usingblock below) for the conversation to continue.
The completion-aware form is the recommended pattern. async with Patter(...)
(Python) / await using (TypeScript) boots the local server, keeps it alive
for the call's lifetime, and tears it down cleanly on exit — no dangling
process, no manual serve() task to cancel:
import asyncio
from getpatter import Patter, Twilio, OpenAIRealtime2
async def main():
# `async with` runs the local server for the duration of the block.
async with Patter(carrier=Twilio(), phone_number="+15550001234") as phone:
agent = phone.agent(
engine=OpenAIRealtime2(),
system_prompt="You are Mia from Acme. Keep replies under two sentences.",
first_message="Hi, this is Mia from Acme.",
)
result = await phone.call(
to="+14155551234",
agent=agent,
voicemail_message="Sorry we missed you. Call back at +1...",
wait=True, # block until the call ends → CallResult
)
# CallResult is frozen: call_id, outcome, status,
# duration_seconds, transcript, cost, metrics.
print(result.outcome) # answered | voicemail | no_answer | busy | failed
print(f"{result.duration_seconds:.0f}s · ${result.cost.total_usd:.4f}")
asyncio.run(main())
import { Patter, Twilio, OpenAIRealtime2 } from "getpatter";
// `await using` runs the local server, then disposes it when the block exits.
await using phone = new Patter({
carrier: new Twilio(),
phoneNumber: "+15550001234",
});
const agent = phone.agent({
engine: new OpenAIRealtime2(),
systemPrompt: "You are Mia from Acme. Keep replies under two sentences.",
firstMessage: "Hi, this is Mia from Acme.",
});
const result = await phone.call({
to: "+14155551234",
agent,
voicemailMessage: "Sorry we missed you. Call back at +1...",
wait: true, // block until the call ends → CallResult
});
// CallResult is readonly: callId, outcome, status,
// durationSeconds, transcript, cost, metrics.
console.log(result.outcome); // answered | voicemail | no_answer | busy | failed
console.log(`${result.durationSeconds}s · $${result.cost.totalUsd}`);
wait=True is timeout-bounded on ring_timeout (default 25 s), so a number
that never picks up resolves to outcome="no_answer" rather than hanging
forever. machine_detection is on by default since 0.6.3: Patter auto-detects
voicemail, plays the call's voicemail_message before hanging up, and the
CallResult comes back with outcome="voicemail". A non-empty
voicemail_message implicitly enables AMD even if you pass
machine_detection=False. Otherwise the live person hears first_message
and the agent starts the conversation. See configure-telephony for the
full set of outbound options.
System prompt — what matters for voice
Voice prompts are different from chat prompts. Patter doesn't add hidden instructions, so be explicit:
- Identity: "You are <name>, calling on behalf of <company>."
- Goal: "Help the caller book a service visit." (one line)
- Length rule: "Keep every reply under two sentences." (LLMs over-explain on voice.)
- Stop conditions: "If the caller wants a human, call
transfer_call." - Persona: "Warm, professional, never robotic. Say 'mm-hm' when listening."
The first_message is what the agent says immediately on pickup — keep it
to 1 sentence under 8 seconds of audio.
Gotchas
- Patter has no Patter Cloud as of 0.7.0 —
Patter(api_key=...)raisesNotImplementedError. Always usecarrier=...+phone_number=.... - Built-in tools are always present: every agent gets
transfer_callandend_callfor free. You don't have to register them. - Mulaw vs PCM audio: Twilio carrier = mulaw 8 kHz, Telnyx = PCM 16 kHz. Patter transcodes both transparently — you never touch raw audio unless you write a pipeline hook.
tunnel=Trueis dev-only. For production, use a static webhook URL (your own subdomain or paid ngrok). Cloudflare quick tunnels work but occasionally have a ~3 s WSS upgrade race on first call.first_messageis played before the LLM responds. If you omit it, there's an awkward 1–2 s pause while the LLM warms up.- Barge-in works by default since 0.6.3 (VAD activation 0.8, deactivation
0.65 — tuned for room noise). If your callers are interrupting at the
wrong moments, see
inspect-calls-and-metricsto read the VAD events from the call log. - Callers cut off mid-pause? In Pipeline mode add a semantic
turn_detector(smart-turn or NAMO) and, on noisy lines, thedenoiser/high_pass_hz/agclevers — see references/pipeline-mode.md. In Realtime mode useopenai_realtime_noise_reduction="far_field"andrealtime_turn_detection— see references/realtime-mode.md. prewarm_first_message=Falseis the default since 0.6.3 — it was briefly flipped toTruemid-release and reverted because it conflicted with barge-in.
Common errors
| Symptom | Fix |
|---|---|
RuntimeError: Patter Cloud is not implemented | Don't pass api_key=. Use carrier=Twilio() + phone_number="...". |
PatterConnectionError on call(..., wait=True) | No running server to dial through. Wrap the call in async with Patter(...) / await using, or place it while a serve() task is alive. |
| Agent says nothing on pickup | Missing first_message=. Add one. |
| LLM responses are 4 paragraphs long | Add "Keep replies under two sentences" to system prompt. |
| Caller hears garbled audio | Wrong audio rate. Check you're using the right carrier (mulaw 8 kHz Twilio, PCM 16 kHz Telnyx). Patter handles this; user code only breaks it via custom pipeline hooks. |
| Connection drops after 30 s | Either the tunnel died (use static URL) or barge-in hung. Check MetricsStore for the last event. |
Related skills
setup-patter— install + env vars (run this first if user hasn't).configure-telephony— set up the Twilio/Telnyx webhook.add-tools-and-handoffs— custom tools,transfer_call, guardrails.inspect-calls-and-metrics— live dashboard, cost per call, transcript.
References
- Patter overview: https://docs.getpatter.com/python-sdk/overview · https://docs.getpatter.com/typescript-sdk/overview
- Concepts: https://docs.getpatter.com/concepts
- Agent configuration: https://docs.getpatter.com/python-sdk/agents · https://docs.getpatter.com/typescript-sdk/agents
- Examples: https://docs.getpatter.com/examples