Three architectural approaches for building voice agents that handle multiple languages, with provider recommendations and common pitfalls.
| Single Static Agent | Two Agents with Handoff | Self-Handoff with Overrides | |
|---|---|---|---|
| Complexity | Low | Medium | High |
| STT Accuracy | Lower (multi-language tradeoff) | Highest (dedicated per-language config) | High (dedicated config via overrides) |
| Latency | None (no handoff) | ~50–200ms audio gap on handoff | Same as two-agent |
| Tool messages | Must use contents[] or single language |
Each assistant has its own language | Same tools, contents[] with language variable |
| Backend objects | 1 assistant | 2 assistants | 1 assistant |
| Best for | Simple bilingual, code-switching OK | Distinct language experiences | Dynamic language config without duplicating assistants |
Use one assistant with a multilingual transcriber and voice.
| Provider | Config | How it works |
|---|---|---|
| Deepgram (recommended) | language: "multi" on nova-3 |
Auto-detects per-utterance across supported languages. Supports keyterm for vocabulary boosting (since Nov 2025). |
| Gladia | languageBehaviour: "automatic multiple languages" |
V2 solaria model with native code-switching |
| Soniox | model: stt-rt-v4, languages: [en, es] |
Single universal model handles 60+ languages — no per-language model swap. Supports customVocabulary: [...] for vocabulary boosting and maxEndpointDelayMs for turn-taking tuning. |
| Speechmatics | language: "en_es" |
Bilingual mode for Spanish+English |
| AssemblyAI | language: "multi" |
Universal streaming multilingual model |
| Provider | Config | Notes |
|---|---|---|
ElevenLabs eleven_multilingual_v2 |
No language config needed | Auto-detects from text. Best quality. |
| OpenAI TTS | No config needed | Language inferred from text automatically |
Cartesia sonic-3 |
language: "es" or "en" |
accentLocalization: 1 adapts accent to transcript language |
| Azure | voiceId: "en-US-EmmaMultilingualNeural" |
MultilingualNeural voices handle multiple languages |
You are a bilingual support agent fluent in English and Spanish.
Always detect the language the customer is speaking and respond
in that same language. If the customer switches languages
mid-conversation, follow their lead immediately.
Cultural guidelines:
- English: Direct, solution-focused, professional
- Spanish: Warm, use "usted" initially, build personal connection
- STT accuracy is lower with
"multi"vs a dedicated language setting - Tool messages, idle messages, and
endCallMessageare static (single language) unless usingcontents[] - No way to change voice mid-call without a handoff
Each assistant is fully configured for one language — dedicated transcriber, voice, system prompt, and tool messages.
[English Assistant] ←── handoff ──→ [Spanish Assistant]
transcriber: en transcriber: es
voice: en voice voice: es voice
tools: English messages tools: Spanish messages
prompt: English prompt: Spanish
type: handoff
destinations:
- type: assistant
assistantId: other-language-assistant-id
description: "Hand off when the caller switches to [other language]"
contextEngineeringPlan:
type: all| Type | Behavior | Best for |
|---|---|---|
all (default) |
Full conversation preserved, system message replaced | Continuing same conversation in new language |
none |
Clean slate | Starting fresh in new language |
lastNMessages |
Last N messages kept | Partial context preservation |
userAndAssistantMessages |
User/bot turns only (tool calls stripped) | Clean handoff without tool noise |
The destination assistant's firstMessage fires after handoff. Options:
- Set
firstMessage: ""to suppress it - Set it to a language-appropriate greeting:
"En que puedo ayudarle?" - Use
firstMessageMode: "assistant-speaks-first-with-model-generated-message"for an LLM-generated greeting
- Highest STT accuracy: dedicated language per assistant
- Full control: each assistant has its own tool messages, voice, persona
- Tool messages naturally in the right language
- Two assistants to maintain
- ~50–200ms audio gap on handoff
- LLM must correctly detect language switches
Single assistant hands off to itself with assistantOverrides to change language configuration at runtime.
type: handoff
destinations:
- type: assistant
assistantId: this-same-assistant-id
description: "Switch to Spanish when the caller speaks Spanish"
assistantOverrides:
transcriber:
provider: deepgram
model: nova-2
language: es
voice:
provider: eleven-labs
voiceId: your-spanish-voice-id
model: eleven_multilingual_v2
variableValues:
language: Spanish
greeting: "En que puedo ayudarle?"
contextEngineeringPlan:
type: allNearly any assistant property: transcriber, voice, model.messages, firstMessage, variableValues, analysisPlan, tools:append, and more.
To fully replace (not merge) a nested object like transcriber, provide the complete object — partial objects are deep-merged with the existing config.
Fields that support {{ variableName }} substitution from variableValues:
firstMessage,voicemailMessage,endCallMessagemessagePlan.idleMessages,endCallPhrasesmodel.messages[*].content(system prompt)- Tool function names, descriptions, parameters, server URLs, headers, and messages
analysisPlan.*Promptfields
NOT supported by templates: transcriber.language, voice.voiceId, and other non-string config fields. These must be set via assistantOverrides directly.
- No infinite loop protection: if the LLM keeps triggering the handoff, it loops until
maxDurationTimeout. Add clear prompt instructions: "Do not trigger language switch if already in the correct language." firstMessagefires on each swap (unless empty)- Full pipeline teardown/rebuild on every self-handoff
Every tool message (request-start, request-complete, request-failed, request-response-delayed) supports per-language variants:
# Single language (simple)
content: "Please hold while I look that up"
# Per-language variants (multilingual)
contents:
- type: text
text: "Please hold while I look that up"
language: en
- type: text
text: "Un momento mientras busco eso"
language: esVapi checks the active language and selects the matching variant. If no match is found, the first entry may be auto-translated.
Caveat: The active language is set once at call start from the transcriber config. With Deepgram language: "multi", it defaults to "en". This means contents[] may always select English unless the language is explicitly set via a handoff.
| Field | Workaround |
|---|---|
firstMessage |
Use {{ greeting }} via variableValues, or firstMessageMode: "assistant-speaks-first-with-model-generated-message" |
endCallMessage |
Use {{ endMessage }} via variableValues |
voicemailMessage |
No template support |
messagePlan.idleMessages |
LiquidJS supported, but consider using hooks instead |
transcriber:
provider: deepgram
model: nova-3
language: multi
keyterm: # Nova-3 multilingual supports keyterm since Nov 2025
- your-brand-name
- domain-specific-term
voice:
provider: cartesia
model: sonic-3
voiceId: your-voice-id
pronunciationDictId: pdict_xxx # IPA works across all languages
model:
provider: openai
model: gpt-4.1Alternative voice: ElevenLabs eleven_multilingual_v2 if you need ElevenLabs-specific features (alias rules work across all languages, but IPA phoneme rules are English-only).
# English Assistant
transcriber: { provider: deepgram, model: nova-3, language: en }
voice: { provider: eleven-labs, voiceId: your-english-voice }
# Spanish Assistant
transcriber: { provider: deepgram, model: nova-3, language: es }
voice: { provider: eleven-labs, voiceId: your-spanish-voice }| Pitfall | Root Cause | Solution |
|---|---|---|
| Agent understands Spanish but speaks English | TTS voice is English-only | Use multilingual TTS (Cartesia sonic-3, ElevenLabs multilingual_v2, OpenAI) |
| Tool messages always in English | Active language defaults to "en" with "multi" STT |
Use contents[] with explicit language variants |
| Spanish STT accuracy worse than dedicated | Multi-language models trade accuracy for flexibility | Use dedicated per-language assistants (Approach 2) |
| Self-handoff infinite loop | LLM re-triggers handoff after seeing same conversation | Clear prompt: "Do not trigger language switch if already in correct language" |
firstMessage not in detected language |
firstMessage is a static string |
Use LLM-generated first message or LiquidJS templates |
| Idle messages don't adapt | messagePlan.idleMessages has no contents[] |
Use LiquidJS templates or hooks |
What you might expect: keyterm is a vocabulary boost — terms get recognized more reliably regardless of which language the customer is speaking.
What actually happens: With model: nova-3 and language: multi, the language ID step uses partial transcripts as a signal. A keyterm array dominated by English brand names, English product terms, or English acronyms tilts that signal toward English, especially on short utterances or code-switched turns. The result is Deepgram routing non-English speech through the English pipeline, producing low-confidence transcripts that may get filtered out entirely (see confidenceThreshold in assistants.md).
This is most visible when a Spanish-only customer is misrecognized as English on their first utterance, which then cascades — the assistant responds in English, the customer gets confused, and the loop continues.
Recommendation for code-switching customers: Use Gladia Solaria (provider: gladia, languageBehaviour: automatic multiple languages) or Soniox (provider: soniox, model: stt-rt-v4, languages: [en, es]) instead of Deepgram language: multi. Both are built around code-switching as a first-class case and aren't biased by vocabulary-boost content the same way. Soniox is the strongest pick when you need code-switching detection AND vocabulary boosting in the same call — it supports customVocabulary: [...] (the equivalent of Deepgram's keyterm) on the same single universal model that handles all 60+ languages. See Approach 1 for the full transcriber comparison.
If you must stay on Deepgram multi: Keep keyterm short (under 20 entries), include the customer's expected non-English equivalents, and avoid English-only acronyms that have no foreign-language form.
transcriber:
provider: soniox
model: stt-rt-v4
language: en # primary / default language (ISO 639-1)
languages: [en, es] # hint set for code-switching; omit for single-language
customVocabulary:
- your-brand-name
- domain-specific-term
- non-English-equivalent
confidenceThreshold: 0.3
# maxEndpointDelayMs: 800 # optional turn-taking tuning