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

  1. dial@v2 tries to reach your team. $dial.ok is false when nobody answered.
  2. record@v1 plays a beep and records the caller until silence, a key press, or maxLength. Its result is a temporary file.
  3. saveRecordingFile@v1 turns that file into a Recording of your account (an MP3 URL, listed in the Recordings API), with an optional transcript.
  4. events.ready of saveRecordingFile@v1 runs 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.ready runs in the background once the recording is ready (usually after the call), with the actions of the hangup branch: no call action there. Use sms@v1 instead of email@v1 to text the team, or fetch@v3 to 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: $recording

record@v1 Parameters

ParameterDefaultDescription
maxLength30Maximum length of the message, in seconds (5 to 300).
timeout5Seconds of silence that end the recording (0 to 10).
beeptruePlay a beep before recording.
finishOnAnyKeytrueAny key press ends the recording.

Smart Voicemail with voicebot@v2

A 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 smartClassic (record@v1)Smart (voicebot@v2)
You getAn MP3 and its transcriptName, reason, callback number, urgency
Caller experienceBeep, talk, hang upA short conversation
CostRecording and transcriptionSpeech-to-text and text-to-speech, plus LLM tokens on your OpenAI key
Audio keptYesNo: 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. Keep timeout short (5 seconds is a good start) and tune yesHints/noHints with the words your callees actually use.

amd@v3 Result and Parameters

result:
  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
ParameterDefaultDescription
timeout6Seconds to listen (1 to 10).
minWords4A 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@v1 check on call.startedAt.
  • Keep a log of every attempt: push $dial.status and $amd.detected to your server with a fetch@v3 placed after the dial@v2 of the api-initiated branch (it runs once the call is over, answered or not; the hangup branch 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, send chat_text to openai@v3 before emailing it. See Record and Transcribe Calls.

Did this page help you?