Plugin guides
Voice call TTS and inbound calls
Telephony text-to-speech, inbound policy and per-number routing, the spoken output contract, conversation startup, and the stale call reaper. Part of the Voice call plugin guide.
TTS for calls
Voice Call uses the core tts configuration for streaming speech on
calls. You can override it under the plugin config with the same shape —
it deep-merges with tts.
{ tts: { provider: "elevenlabs", providers: { elevenlabs: { speakerVoiceId: "pMsXgVXv3BLzUgSXRplE", modelId: "eleven_multilingual_v2", }, }, },}Behavior notes:
- Legacy
tts.<provider>keys inside plugin config (openai,elevenlabs,microsoft,edge) are repaired byopenclaw doctor --fix; committed config should usetts.providers.<provider>. - Core TTS is used when Twilio media streaming is enabled; otherwise calls fall back to provider-native voices.
- If a Twilio media stream is already active, Voice Call does not fall back to TwiML
<Say>. If telephony TTS is unavailable in that state, the playback request fails instead of mixing two playback paths. - When telephony TTS falls back to a secondary provider, Voice Call logs a warning with the provider chain (
from,to,attempts) for debugging. - When Twilio barge-in or stream teardown clears the pending TTS queue, queued playback requests settle instead of hanging callers awaiting playback completion.
- Resumed caller speech discards older automatic replies that are still being generated. Twilio streaming reacts to speech-start and partial transcripts; carrier webhook calls react when a new speech event arrives. Explicit speech requests remain available, and already accepted agent work can finish without speaking an obsolete reply.
TTS examples
Core TTS only
{tts: {provider: "openai",providers: { openai: { speakerVoice: "alloy" },},},}Override to ElevenLabs (calls only)
{plugins: {entries: { "voice-call": { config: { tts: { provider: "elevenlabs", providers: { elevenlabs: { apiKey: "elevenlabs_key", speakerVoiceId: "pMsXgVXv3BLzUgSXRplE", modelId: "eleven_multilingual_v2", }, }, }, }, },},},}OpenAI model override (deep-merge)
{plugins: {entries: { "voice-call": { config: { tts: { providers: { openai: { model: "gpt-4o-mini-tts", speakerVoice: "marin", }, }, }, }, },},},}Inbound calls
Inbound policy defaults to disabled. To enable inbound calls, set:
{ inboundPolicy: "allowlist", allowFrom: ["+15550001234"], inboundGreeting: "Hello! How can I help?",}Auto-responses use the agent system. Tune with responseModel,
responseSystemPrompt, and responseTimeoutMs.
Per-number routing
Use numbers when one Voice Call plugin receives calls for multiple phone
numbers and each number should behave like a different line. For example,
one number can use a casual personal assistant while another uses a business
persona, a different response agent, and a different TTS voice.
Routes are selected from the provider-supplied dialed To number. Keys must
be E.164 numbers. When a call arrives, Voice Call resolves the matching
route once, stores the matched route on the call record, and reuses that
effective config for the greeting, classic auto-response path, realtime
consult path, and TTS playback. If no route matches, the global Voice Call
config is used. Outbound calls do not use numbers; pass the outbound
target, message, and session explicitly when initiating the call.
Route overrides currently support:
inboundGreetingttsagentIdresponseModelresponseSystemPromptresponseTimeoutMs
The tts route value deep-merges over the global Voice Call tts config, so
you can usually override only the provider voice:
{ inboundGreeting: "Hello from the main line.", responseSystemPrompt: "You are the default voice assistant.", tts: { provider: "openai", providers: { openai: { speakerVoice: "coral" }, }, }, numbers: { "+15550001111": { inboundGreeting: "Silver Fox Cards, how can I help?", responseSystemPrompt: "You are a concise baseball card specialist.", tts: { providers: { openai: { speakerVoice: "alloy" }, }, }, }, },}Spoken output contract
For auto-responses, Voice Call appends a strict spoken-output contract to
the system prompt requiring a {"spoken":"..."} JSON reply. Voice Call
extracts speech text defensively:
- Ignores payloads marked as reasoning/error content.
- Parses direct JSON, fenced JSON, or inline
"spoken"keys. - Falls back to plain text and removes likely planning/meta lead-in paragraphs.
This keeps spoken playback focused on caller-facing text and avoids leaking planning text into audio.
Conversation startup behavior
For outbound conversation calls, first-message handling is tied to live
playback state:
- Barge-in queue clear and auto-response are suppressed only while the initial greeting is actively speaking.
- If initial playback fails, the call returns to
listeningand the initial message remains queued for retry. - Initial playback for Twilio streaming starts on stream connect without extra delay.
- Barge-in aborts active playback and clears queued-but-not-yet-playing Twilio TTS entries. Cleared entries resolve as skipped, so follow-up response logic can continue without waiting on audio that will never play.
- Realtime voice conversations use the realtime stream's own opening turn. Voice Call does not post a legacy
<Say>TwiML update for that initial message, so outbound<Connect><Stream>sessions stay attached.
Twilio stream disconnect grace
When a Twilio classic streaming or realtime media stream disconnects, Voice Call waits 2000 ms before auto-ending the call:
- If the stream reconnects during that window, auto-end is canceled.
- If no stream re-registers after the grace period, the call is ended to prevent stuck active calls.
- Realtime bridge/session resources, queued audio, transcript ownership, and in-flight consult work close immediately. Only call/provider finalization waits for reconnect.
Stale call reaper
Use staleCallReaperSeconds (default 120) to end calls that are never
answered and never reach a live conversation state, for example notify-mode
calls where the provider never delivers a terminal webhook. Set it to 0 to
disable.
The reaper runs every 30 seconds and only ends calls that have no
answeredAt timestamp and are not already in a terminal or live
(speaking/listening) state, so answered conversations are never reaped
by this timer; maxDurationSeconds (default 300) is the separate cap that
ends answered calls that run too long.
For notify-style flows where carriers can be slow to deliver ring/answer
webhooks, raise staleCallReaperSeconds past the default so slow-but-normal
calls are not reaped early; 120-300 seconds is a reasonable production
range.
{ plugins: { entries: { "voice-call": { config: { maxDurationSeconds: 300, staleCallReaperSeconds: 120, }, }, }, },}