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.

json5
{  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 by openclaw doctor --fix; committed config should use tts.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

json5
{tts: {provider: "openai",providers: {  openai: { speakerVoice: "alloy" },},},}

Override to ElevenLabs (calls only)

json5
{plugins: {entries: {  "voice-call": {    config: {      tts: {        provider: "elevenlabs",        providers: {          elevenlabs: {            apiKey: "elevenlabs_key",            speakerVoiceId: "pMsXgVXv3BLzUgSXRplE",            modelId: "eleven_multilingual_v2",          },        },      },    },  },},},}

OpenAI model override (deep-merge)

json5
{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:

json5
{  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:

  • inboundGreeting
  • tts
  • agentId
  • responseModel
  • responseSystemPrompt
  • responseTimeoutMs

The tts route value deep-merges over the global Voice Call tts config, so you can usually override only the provider voice:

json5
{  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 listening and 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.

json5
{  plugins: {    entries: {      "voice-call": {        config: {          maxDurationSeconds: 300,          staleCallReaperSeconds: 120,        },      },    },  },}
Was this useful?
On this page

On this page