Voicemail & Answering Machine Detection
Take a message when nobody answers (record@v1 + saveRecordingFile@v1), notify your team by email or SMS, and detect answering machines on outbound calls with amd@v3.
Two sides of the same problem: inbound, nobody picks up and the caller wants to leave a message; outbound, your campaign reaches a voicemail and you want to know, leave a message, or hang up.
📥 Inbound: Take a Message
How It Works
dial@v2tries to reach your team.$dial.okisfalsewhen nobody answered.record@v1plays a beep and records the caller until silence, a key press, ormaxLength. Its result is a temporary file.saveRecordingFile@v1turns that file into a Recording of your account (an MP3 URL, listed in the Recordings API), with an optional transcript.events.readyofsaveRecordingFile@v1runs when the recording (and transcript) is ready: email it, text it, push it to your CRM.
Example: Forward, Then Voicemail
description: Forward the call, take a message if nobody answers, email it
compat:
version: "2025-09-01"
defaults:
language: en-US
variables:
$team: "+33199001020"
$notify: [email protected]
$dial: null
$record: null
$recording: null
branches:
inbound-call:
actions:
- action: dial@v2
params:
targets:
- number: ${{ $team }}
timeout: 25
result: $dial
# Nobody answered: take a message
- if: '!$dial.ok'
then:
- action: play@v3
params:
tts:
text: We are not available right now. Please leave a message after the beep, and press any key when you are done.
- action: record@v1
params:
maxLength: 120
timeout: 5 # stop after 5 seconds of silence
result: $record
- action: saveRecordingFile@v1
params:
file: ${{ $record }}
transcription:
enabled: true
language: en-US
events:
ready:
- action: email@v1
params:
to: ${{ $notify }}
subject: New voicemail from ${{ call.fromNumber || "an anonymous caller" }}
text: |
Caller: ${{ call.fromNumber || "anonymous" }}
Duration: ${{ recording.duration }} seconds
Listen: ${{ recording.url }}
Transcript:
${{ chat_text }}
result: $recording
- action: play@v3
params:
tts:
text: Thank you, goodbye.
- action: hangup@v1
events.readyruns in the background once the recording is ready (usually after the call), with the actions of thehangupbranch: no call action there. Usesms@v1instead ofemail@v1to text the team, orfetch@v3to create a ticket. See Record and Transcribe Calls for the variables available there.
Let the Caller Review the Message
playRecordingFile@v1 plays the temporary file back before you save it.
- label: record
action: record@v1
params:
maxLength: 120
result: $record
- action: play@v3
params:
tts:
text: Here is your message.
- action: playRecordingFile@v1
params:
file: ${{ $record }}
- action: gather@v4
params:
tts:
text: Press 1 to send it, or 2 to record it again.
maxDigits: 1
timeout: 10
result: $choice
- if: $choice.input === "2"
goto:
label: record
- action: saveRecordingFile@v1
params:
file: ${{ $record }}
result: $recordingrecord@v1 Parameters
record@v1 Parameters| Parameter | Default | Description |
|---|---|---|
maxLength | 30 | Maximum length of the message, in seconds (5 to 300). |
timeout | 5 | Seconds of silence that end the recording (0 to 10). |
beep | true | Play a beep before recording. |
finishOnAnyKey | true | Any key press ends the recording. |
Smart Voicemail with voicebot@v2
voicebot@v2A classic voicemail records whatever the caller says, and someone has to listen to it. With voicebot@v2, an AI agent answers instead: it asks who is calling, why, and how to call back, then hands you structured fields instead of an audio file. The bot saves each field with its built-in set_value tool and ends the conversation with the built-in hangup tool. $bot.values holds the fields, $bot.messages the whole conversation.
description: Forward the call; if nobody answers, an AI agent takes the message and emails it
compat:
version: "2025-09-01"
defaults:
language: en-US
variables:
$team: "+33199001020"
$notify: [email protected]
$openaiKey: <openai-key>
$dial: null
$bot: null
$message: null
branches:
inbound-call:
actions:
- action: dial@v2
params:
targets:
- number: ${{ $team }}
timeout: 25
result: $dial
# Nobody answered: the AI agent takes the message
- if: '!$dial.ok'
then:
- action: voicebot@v2
params:
initialMessages:
- role: system
content: >
You take phone messages for Acme while the team is unavailable.
Make short replies, ask one question at a time.
Collect: the caller's name, the reason for the call, a callback number
(the caller's number is ${{ call.fromNumber || "unknown" }}, confirm it or ask for another one),
and whether it is urgent.
Store each answer with the set_value function, with these names:
name, reason, callback_number, urgent (yes or no).
Then read a one-sentence summary back to the caller, say goodbye,
and call the hangup function.
Never promise a callback time.
# assistant initial messages are said by the bot
- role: assistant
content: Hello, the team cannot take your call right now. I can take a message. May I have your name?
engine:
name: OpenAI
model: gpt-6.1-terra
auth: ${{ $openaiKey }}
params:
temperature: 1
result: $bot
# Build the message from what the bot collected
- action: javascript@v1
params:
code: |
const v = $bot?.values || {}
$message = {
name: v.name || "unknown",
reason: v.reason || "not given",
callback: v.callback_number || call.fromNumber || "anonymous",
urgent: v.urgent === "yes",
conversation: ($bot?.messages || [])
.filter(m => m.role === "user" || m.role === "assistant")
.map(m => (m.role === "user" ? "Caller: " : "Bot: ") + m.content)
.join("\n")
}
- action: email@v1
params:
to: ${{ $notify }}
subject: '${{ $message.urgent ? "[URGENT] " : "" }}Message from ${{ $message.name }} (${{ $message.callback }})'
text: |
Name: ${{ $message.name }}
Call back: ${{ $message.callback }}
Urgent: ${{ $message.urgent ? "yes" : "no" }}
Reason: ${{ $message.reason }}
Conversation:
${{ $message.conversation }}
- action: hangup@v1| Classic vs smart | Classic (record@v1) | Smart (voicebot@v2) |
|---|---|---|
| You get | An MP3 and its transcript | Name, reason, callback number, urgency |
| Caller experience | Beep, talk, hang up | A short conversation |
| Cost | Recording and transcription | Speech-to-text and text-to-speech, plus LLM tokens on your OpenAI key |
| Audio kept | Yes | No: add startRecording@v2 before the bot to keep it |
📤 Outbound: Detect Answering Machines
How It Works
On a Broadcast Call, the called party answers, and your scenario continues in the answerOnBranch branch — whether a human or a voicemail picked up. amd@v3 listens for a few seconds and tells you which one it is:
- a human says a short greeting ("Hello?") and waits;
- a machine plays a long greeting ("Hi, you have reached…, please leave a message after the tone").
amd@v3 counts the words it hears: minWords words or more means a machine. yesHints and noHints short-circuit the decision when a known word is heard.
Example: Leave a Message on Voicemails Only
description: Call a list of people; talk to humans, leave a message on voicemails
compat:
version: "2025-09-01"
defaults:
language: en-US
variables:
$target: "+33639980010" # passed when starting the scenario
$dial: null
$amd: null
$silence: null
branches:
api-initiated:
actions:
- action: dial@v2
params:
callerId: "+33199001234"
targets:
- number: ${{ $target }}
timeout: 30
answerOnBranch: answered
result: $dial
answered:
actions:
- action: amd@v3
params:
timeout: 5
minWords: 4
yesHints: [message, voicemail, tone, beep]
noHints: [hello, yes]
result: $amd
# A voicemail: wait for the end of the greeting, then leave the message
- if: $amd.detected === "machine"
then:
- action: waitForSilence@v1
params:
silence: 2
timeout: 20
result: $silence
- action: play@v3
params:
tts:
text: Hello, this is Acme. Your order is ready for pickup. See you soon!
- action: hangup@v1
# A human (or no decision): talk to them
- action: play@v3
params:
tts:
text: Hello, this is Acme. Your order is ready for pickup.
- action: gather@v4
params:
tts:
text: Press 1 to confirm you will come today, or 2 for tomorrow.
maxDigits: 1
timeout: 10
result: $answer
- action: hangup@v1
Answering machine detection is a heuristic: it listens to the first seconds of the call. A human who answers with a long sentence can be mistaken for a machine, and a short voicemail greeting for a human. Keeptimeoutshort (5 seconds is a good start) and tuneyesHints/noHintswith the words your callees actually use.
amd@v3 Result and Parameters
amd@v3 Result and Parametersresult:
ok: true # false when no decision was made
detected: machine # machine | human | "" (no decision before the timeout)
recognized: message # the hint that matched, or the words heard when minWords was reached
timeout: false| Parameter | Default | Description |
|---|---|---|
timeout | 6 | Seconds to listen (1 to 10). |
minWords | 4 | A machine when at least this many words are heard (1 to 10). |
yesHints | — | Words that mean a machine, in the callee's language (up to 10). |
noHints | — | Words that mean a human (up to 10). |
language | — | Speech-to-text language. Defaults to defaults.language of the scenario. |
tts / audioFile | — | Optional prompt played while listening (set at most one). Key presses are ignored. |
💡 Going Further
- Business hours: send callers straight to voicemail outside opening hours, with a
javascript@v1check oncall.startedAt. - Keep a log of every attempt: push
$dial.statusand$amd.detectedto your server with afetch@v3placed after thedial@v2of theapi-initiatedbranch (it runs once the call is over, answered or not; thehangupbranch only runs for inbound calls), so your campaign can retry the voicemails later. See Push Data to Your Platform. - Summarize voicemails with AI: in
events.ready, sendchat_texttoopenai@v3before emailing it. See Record and Transcribe Calls.
Updated 4 days ago
