SWML IVR — Prompt, Gather, and Branching
For classic IVR menus (press 1, press 2, speak your option), SWML uses the prompt verb to play audio or TTS and capture DTMF or speech, plus cond to branch on the result.
Basic DTMF menu
version: 1.0.0
sections:
main:
- answer: {}
- prompt:
play: "say:Press 1 for sales. Press 2 for support. Press 3 for billing."
max_digits: 1
terminators: "#"
timeout: 5
- cond:
if: "%{prompt_value} == '1'"
then:
- goto: { section: sales }
elif: "%{prompt_value} == '2'"
then:
- goto: { section: support }
elif: "%{prompt_value} == '3'"
then:
- goto: { section: billing }
else:
- play:
url: "say:Invalid choice. Goodbye."
- hangup: {}
sales:
- connect: { to: "+15555550101" }
support:
- connect: { to: "+15555550102" }
billing:
- connect: { to: "+15555550103" }
Prompt parameters
| Parameter | Notes |
|---|---|
| play | TTS via say: prefix, or hosted audio URL. |
| max_digits | Max digits to capture. Captures stop at this length. |
| terminators | DTMF keys that end input early (typically "#"). |
| timeout | Seconds of silence before giving up. Default 5. |
| digit_timeout | Seconds between digits. Default 5. |
| speech | Object — enables ASR. See below. |
The captured value lands in %{prompt_value}.
Speech input (instead of DTMF)
- prompt:
play: "say:Are you calling about sales, support, or billing?"
speech:
lang: en-US
end_silence_timeout: 1.2
speech_timeout: 8
hints:
- sales
- support
- billing
- account
hints is critical — ASR uses them to bias matching. Without hints, "billing" might come through as "Billy" or "build".
Speech result also lands in %{prompt_value}. Use case-insensitive matching in cond:
- cond:
if: "%{prompt_value.toLowerCase()} == 'sales'"
then:
- goto: { section: sales }
Mixed input — accept DTMF or speech
- prompt:
play: "say:Press or say 1 for sales, 2 for support."
max_digits: 1
speech:
lang: en-US
hints: [sales, support, one, two]
The first input source (DTMF or speech) that completes wins. prompt_value will be "1" or "sales".
No-input handling
prompt returns to the next verb on timeout with prompt_value empty ("").
- prompt:
play: "say:How can I help you today?"
timeout: 6
speech:
lang: en-US
- cond:
if: "%{prompt_value} == ''"
then:
- play: { url: "say:I didn't hear anything. Goodbye." }
- hangup: {}
else:
- goto: { section: route }
Loop guard — re-prompt up to N times
version: 1.0.0
sections:
main:
- answer: {}
- goto: { section: menu }
menu:
- prompt:
play: "say:Press 1 for sales or 2 for support."
max_digits: 1
timeout: 6
- cond:
if: "%{prompt_value} == '1'"
then: [ { goto: { section: sales } } ]
elif: "%{prompt_value} == '2'"
then: [ { goto: { section: support } } ]
else:
- cond:
if: "%{vars.attempts} >= 2"
then:
- play: { url: "say:Too many invalid attempts. Goodbye." }
- hangup: {}
else:
- set: { vars.attempts: "%{vars.attempts || 0} + 1" }
- goto: { section: menu }
sales:
- connect: { to: "+15555550101" }
support:
- connect: { to: "+15555550102" }
Multi-step IVR (sub-menu)
Use sections aggressively. Don't try to do everything inside main.
version: 1.0.0
sections:
main:
- answer: {}
- goto: { section: main_menu }
main_menu:
- prompt:
play: "say:Press 1 for billing, 2 for technical support."
max_digits: 1
- cond:
if: "%{prompt_value} == '1'"
then: [ { goto: { section: billing_menu } } ]
elif: "%{prompt_value} == '2'"
then: [ { goto: { section: support_menu } } ]
billing_menu:
- prompt:
play: "say:Press 1 for payments, 2 for refunds."
max_digits: 1
- cond:
if: "%{prompt_value} == '1'"
then: [ { connect: { to: "+15555550111" } } ]
elif: "%{prompt_value} == '2'"
then: [ { connect: { to: "+15555550112" } } ]
support_menu:
- prompt:
play: "say:Press 1 for new ticket, 2 for existing ticket."
max_digits: 1
- cond:
if: "%{prompt_value} == '1'"
then: [ { connect: { to: "+15555550121" } } ]
elif: "%{prompt_value} == '2'"
then: [ { connect: { to: "+15555550122" } } ]
When to use AI instead
Hand-rolled IVR works well when:
- Inputs are strictly enumerable (press 1-9).
- You need DTMF-only routing for legacy phones.
- The flow is small and stable.
Switch to the ai verb when:
- Callers describe needs in natural language.
- The menu would have 5+ branches.
- You want to capture caller intent for analytics.
- You need to fall back to a human only when AI can't resolve.
For visual IVR construction without writing SWML, use the Call Flow Builder's Gather Input node.
Anti-patterns
- Setting
timeoutto 0 — input is never gathered, flow falls through immediately. - Forgetting
terminatorson long inputs — caller has to wait fordigit_timeoutto expire. - ASR without
hints— recognition accuracy drops sharply on domain-specific terms. - Looping a no-input menu without a counter — caller stuck forever.
- Stuffing branches into
mainwith deep nestedcond— split into sections.