Appearance
VoiceAgent
WARNING
This recipe needs a voice-provisioned org, so it isn't available in Developer Edition orgs or Trailhead Playgrounds. Its connection telephony block references a telephony connection that only exists in an org where Agentforce Voice is provisioned, such as a voice-enabled demo org. Developer Edition orgs and Trailhead Playgrounds don't include the Agentforce Voice Setup experience, so that connection isn't available there.
Overview
This recipe demonstrates how to make an agent voice-capable directly in the script - both by writing its instructions for a spoken medium and by declaring the voice wiring (connection, voice model, persona, and language) in the .agent file itself. The agent is a Socratic "rubber duck" debugging buddy: instead of handing developers a fix, it helps them find the bug themselves by asking one question at a time. Its instructions branch on @system_variables.current_modality, so the same agent speaks plainly on a call and uses inline code in a chat - no variables, actions, or flows required - keeping the recipe focused on what changes when an agent speaks.
Agent Flow
mermaid
%%{init: {'theme':'neutral'}}%%
graph TD
A[Agent Starts] --> B[Load Config Block]
B --> C[Initialize System Block]
C --> D[Apply Language + Voice Config]
D --> E[Display Welcome Message]
E --> F[start_agent: agent_router]
F --> G[Transition to debugging Subagent]
G --> H{current_modality is voice?}
H -->|Yes| I[Apply Spoken Instructions]
H -->|No| J[Apply Text Instructions]
I --> K[Developer describes the bug]
J --> K
K --> L[Ask ONE focused question]
L --> M{Bug found?}
M -->|No| K
M -->|Yes| N[Encourage and confirm]
N --> O[End]Key Concepts
- Persona-driven behavior: Behavior comes from instructions alone - no actions or state
- System vs. reasoning instructions: The global persona lives in
system.instructions; the turn-by-turn behavior lives in the subagent'sreasoning.instructions - Behavioral constraints: Instructing the agent to withhold the answer and ask questions instead - a genuine instruction-design discipline
- Modality-aware instructions: Branching on
@system_variables.current_modalityso spoken and typed conversations get different delivery rules - ASR-noise repair: Instructing the agent, on voice only, to expect and reinterpret speech-to-text mistranscriptions of technical vocabulary
- Voice configuration in script: Declaring a
connection telephonyandmodality voiceblock so the voice wiring, voice model, and persona travel with the recipe - Voice model selection: Choosing a voice model (
model.id) - here the lower-latency Englisheleven_flash_v2- instead of the language's default - Two
languageblocks: One top-level (text modality + linter) and one nested inmodality voice(voice modality + persona resolution)
How It Works
The Persona, in Two Places
The rubber-duck personality is established in two complementary spots.
First, globally, in the system block - this applies to every subagent, whether the conversation is spoken or typed:
agentscript
system:
instructions: "You are a friendly rubber-duck debugging buddy for software developers. You help them find bugs themselves by asking thoughtful, Socratic questions rather than handing over fixes."Then, specifically, in the debugging subagent's reasoning instructions - this governs what the agent does on each turn. The interesting part is that the instructions tell the agent what not to do (don't hand over the fix), which is what makes it a rubber duck rather than a generic Q&A bot.
The Behavioral Constraint
Most "useful" agents are told to answer. This one is deliberately told to hold back and lead with questions - help the developer discover the bug on their own instead of handing over the fix. This is the whole lesson: the value of the agent comes from the instructions, not from any code.
Making the Agent Speak
An agent that talks differs from a text agent on two levels: how it speaks (instructions) and that it speaks at all (voice configuration in the script).
1. Instructions that adapt to the modality. Replies that are read aloud follow different rules than replies in a chat window:
| Concern | Text | Spoken |
|---|---|---|
| Response length | A few sentences is fine | Short - easy to follow by ear |
| Formatting | Inline code is fine | No code or markdown read aloud |
| Symbols/IDs | Can reference i++ | Say them in words: "index plus plus" |
Rather than pick one set of rules, the subagent checks the current_modality system variable and adds only the rules that fit. It's "voice" on a telephony connection, "text" on a messaging connection, and None when neither is bound. Testing for "voice" and putting the text rules in else covers both the text and unbound cases:
agentscript
if @system_variables.current_modality == "voice":
| Keep replies short and easy to follow by ear: speak in full sentences,
never read code or markdown aloud, and say symbols in words (for example
"index plus plus" for "i++").
else:
| Keep replies to a few sentences. You can refer to code directly and
format short snippets or symbols as inline code (for example `i++`),
but don't paste a corrected version of their code.The voice branch also adds one thing a text agent never needs: repairing speech-to-text noise. When a developer talks, their words reach the agent as an imperfect transcription, and technical vocabulary is the first thing to get mangled - "Agent Script" becomes "agent for script", "returns undefined" becomes "returns on the even", "async" becomes "a sink". The agent reasons over that garbled text, not your audio, so the instructions tell it to expect the noise and quietly repair obvious mishears in a debugging context. A typed message has no transcription step, so this rule lives only in the voice branch:
agentscript
| Their words reach you as speech-to-text, so technical vocabulary is
often garbled - "agent for script" means "Agent Script", "returns on
the even" means "returns undefined", "a sink" means "async". Read every
message charitably in a software-debugging context and quietly repair
obvious mishears.2. Voice configuration in the script. The recipe carries three blocks a text agent doesn't:
agentscript
# Pin a voice-supported default language for the TEXT modality (also required by
# the linter whenever a modality voice block is present)
language:
default_locale: "en_US"
# Wire the agent to a telephony (voice) connection
connection telephony:
adaptive_response_allowed: True
# Choose the voice model + persona and set how the agent listens
modality voice:
language:
default_locale: "en_US"
outbound:
persona_id: "74752e92d40e"
model:
id: "eleven_flash_v2"
inbound:
filler_words_detection: Truelanguage(top-level) pins a voice-supported locale for the text modality (the language the LLM replies in). It's also required whenever amodality voiceblock is present - without it the linter flagsvoice-language-missing-language-block, the agent inherits the org's default, and if that isn't voice-supported Agent Builder warns and falls back to English (US). (For multi-locale config, see the LanguageSettings recipe.)connection telephonydeclares that the agent is wired to a voice connection - this is what makes it a voice agent, not just a text agent with terse instructions.modality voiceconfigures the spoken modality:language(nested) sets the voice modality's locale, and is required for persona/model resolution. Apersona_idisn't a global voice - it belongs to a specific (model, locale) pair in the voice catalog, so the modality needs a locale to resolve it against. Omit this nested block and thepersona_idis ignored - the voice falls back to the locale's default persona. It's a separate setting from the top-levellanguage, so you need both.outbound.persona_idpicks the voice from the catalog;outbound.model.idpicks the voice model - hereeleven_flash_v2, a lower-latency English Flash model. Other models:eleven_v3(default, most languages),eleven_flash_v2_5(non-English), andkotoba(Japanese). Omitmodelto use the language's default. Agent Builder's Voice Settings write thepersona_idback into the script when you pick a voice.inbound.filler_words_detectionhelps the agent handle "um"/"uh" fillers in the incoming speech-to-text stream.
NOTE
The older flat modality voice format (voice_id with outbound_speed/outbound_stability/outbound_similarity) still works, but it cannot select a voice model - use the outbound.model.id structure above to choose one.
Key Code Snippets
The Subagent's Reasoning
agentscript
subagent debugging:
description: "Guides the developer to find their own bug through Socratic questioning"
reasoning:
instructions:->
| You are the developer's rubber duck. Help them discover the bug on
their own instead of handing over the fix. Guide with simple questions
like "What did you expect to happen?" or "What did you change last?",
and stay warm and playful.
if @system_variables.current_modality == "voice":
| Their words reach you as speech-to-text, so technical vocabulary is
often garbled - "agent for script" means "Agent Script", "returns on
the even" means "returns undefined", "a sink" means "async". Read every
message charitably in a software-debugging context and quietly repair
obvious mishears.
| Keep replies short and easy to follow by ear: speak in full sentences,
never read code or markdown aloud, and say symbols in words (for example
"index plus plus" for "i++").
else:
| Keep replies to a few sentences. You can refer to code directly and
format short snippets or symbols as inline code (for example `i++`),
but don't paste a corrected version of their code.The Voice Configuration
Together, these three blocks are what make it an actual voice agent:
agentscript
language:
default_locale: "en_US"
connection telephony:
adaptive_response_allowed: True
modality voice:
language:
default_locale: "en_US"
outbound:
persona_id: "74752e92d40e"
model:
id: "eleven_flash_v2"
inbound:
filler_words_detection: TrueTry It Out
Deploy the recipe to a voice-provisioned org, then open Agentforce Studio and start a conversation - it's the quickest way to try the agent, with no phone setup. Describe a bug and notice that the agent asks you a question rather than solving it.
Which instructions the agent follows depends on current_modality: the voice branch applies only when the conversation arrives over the telephony connection, and anything else gets the text branch. To exercise the voice branch end to end, call the agent over its telephony connection (see Notes for the setup).
Example Interaction
Over voice, each turn is short enough to listen to, asks a single question, and speaks any symbols out loud.
text
Agent: Quack! I'm your rubber duck. Tell me about the bug you're chasing, and I'll help you think it through.
User: My loop skips the last item in the array.
Agent: What condition are you using to stop the loop?
User: I go while index is less than length minus one.
Agent: Quack - say that comparison out loud again. Should the last valid index really be length minus one, or just less than length?
User: ...ah. It should be less than length. Off by one.Over text, the same question can point straight at the code.
text
User: My loop skips the last item in the array.
Agent: What condition are you using to stop the loop? Paste that line if it's handy.
User: for (let i = 0; i < arr.length - 1; i++)
Agent: Walk me through it: if `arr` has three items, what's the last value of `i` that runs?
User: ...1. Oh, it should be `i < arr.length`. Off by one.What's Next
- HelloWorld: The minimal agent this recipe is modeled on - start there for the bare structure
- TemplateExpressions: More on
if/elseconditionals inside instructions - SystemInstructionOverrides: Customize the persona per subagent for finer control
- LanguageSettings: Configure multiple locales for a multi-language voice or text agent
- VariableManagement: Track debugging state (e.g., which questions have been asked) across turns
Notes
- The
persona_idis model- and catalog-specific. Themodality voiceblock'soutbound.persona_id(here74752e92d40e) is a voice hash from the voice catalog, and the same voice has a different hash per model - so apersona_idonly resolves against themodel.idit was listed under. Pick from the catalog page for your chosen model. If you deploy to a different org and the voice isn't available, pick one in Voice Settings and let the script round-trip. - You need both
languageblocks. The top-levellanguagesets the text modality (and satisfies the linter); thelanguagenested insidemodality voicesets the voice modality and is what makespersona_id/modelresolution work - drop it and the voice silently reverts to the locale's default persona (no deploy error). Both pindefault_locale: "en_US". Voice mode only supports certain locales, so if the agent's default language isn't one of them, Agent Builder warns and falls back to English (US). (See the LanguageSettings recipe for multi-locale config.) - Don't confuse dictation with the voice connection. The microphone/waveform icon in the Live Test input box is speech-to-text dictation (it types for you) and appears for every agent, voice-enabled or not. The
connection telephony+modality voiceblocks are what actually make the agent speak its replies aloud. - Full telephony setup is out of scope here. The script declares the connection, but standing up the underlying voice channel (browser voice preview, or full telephony via Amazon Connect / a partner provider with a Contact Center and phone number) is org configuration. See Salesforce's Agentforce Voice / Service Cloud Voice setup documentation for the current, org-specific steps.
- Naming rules: Agent and subagent names use letters, numbers, and underscores only, must start with a letter, and cannot end with or contain consecutive underscores.
- Indentation: Agent Script uses 3 spaces per level.