nomoVM
Asterisk

Add AI Asterisk answering machine detection to any dialplan with EAGI

Asterisk answering machine detection usually means one line: AMD(). This developer guide replaces that line with AI detection in any Asterisk 13+ dialplan, from custom predictive dialers to FreePBX and Issabel outbound campaigns. You will install one EAGI script, keep AMD() as a fallback, route on AMDSTATUS and AMDCAUSE, then test and log the result.

An Asterisk dialplan snippet where the AMD() line is replaced by an EAGI call to nomovm.agi.py with a fallback branch

The problem with AMD() in a custom dialer

If you build or maintain an outbound dialer on Asterisk, your answering machine detection is almost certainly AMD(). It is built in, it costs nothing per call, and it does one thing: it measures silences and bursts of sound and applies timing rules from amd.conf. A short "Hello?" followed by silence becomes HUMAN; long speech becomes MACHINE.

That works on textbook answers and fails on the rest. A person who says "Hello? ... Hello?" can be dropped as a machine, while a short personal greeting, a call screening assistant or a recorded "Hello?" robot reaches your agents. You can tune the nine settings, but each change moves errors from one side to the other. (We walk through every setting in amd.conf settings explained, and compare both approaches in Asterisk AMD vs AI AMD.)

This guide is for developers who want to swap that one line for AI detection, without changing carriers, adding servers or rewriting call flow.

How the EAGI integration works

nomoVM is a hosted AI answering machine detection service. The integration on Asterisk is a single script, nomovm.agi.py, run with EAGI() right where AMD() used to be:

  1. EAGI hands the script the call's incoming audio on file descriptor 3.
  2. The script opens an encrypted WebSocket to wss://amd.nomovm.com/ with your key and streams the audio in 100 ms frames from the moment of answer.
  3. nomoVM returns one verdict. Most calls are decided about 2 seconds after answer; only unclear calls listen up to 3.2 seconds.
  4. The script sets AMDSTATUS and AMDCAUSE and returns to the dialplan.

The variables follow AMD()'s naming, so code downstream doesn't care which detector ran:

AMDSTATUSAMDCAUSE valuesMeaning
HUMANHUMANA live person. Connect an agent.
MACHINEMACHINE, SCREENING, SILENCE, TONE, BUSY, UNKNOWNNot a confirmed person. The cause says what was heard.
FALLBACKAUTH, SUSPENDED, QUOTA, NOSERVERnomoVM didn't judge the call. Run AMD() instead.

AMDSTATUS is HUMAN only when AMDCAUSE is HUMAN. There is no NOTSURE: an unclear call gets more listening time rather than a shrug.

Step by step: install and wire up the dialplan

  1. Create a key

    Sign up (the free plan needs no card) and create a WS key in the dashboard. One key per dialer server keeps your logs tidy.

  2. Install the script

    Copy nomovm.agi.py into Asterisk's AGI directory and make it executable. It needs Python 3.6 or newer and uses only the standard library.

    cp nomovm.agi.py /var/lib/asterisk/agi-bin/
    chmod 755 /var/lib/asterisk/agi-bin/nomovm.agi.py
    python3 --version
  3. Test the connection

    Before touching the dialplan, check that the server can reach the service:

    python3 /var/lib/asterisk/agi-bin/nomovm.agi.py --test wss://amd.nomovm.com/ YOUR_KEY

    It should print AMDSTATUS=MACHINE AMDCAUSE=SILENCE in about 2 seconds, because the test streams silence. FALLBACK with AUTH means the key is wrong; NOSERVER means the server can't reach the service, so check outbound HTTPS and DNS.

  4. Replace the AMD() line

    Find where your answered outbound calls run AMD() and replace that one line with the EAGI call and a fallback branch:

    ; before
    same => n,AMD()
    
    ; after
    same => n,EAGI(nomovm.agi.py,wss://amd.nomovm.com/,YOUR_KEY,4500)
    same => n,GotoIf($["${AMDSTATUS}" = "FALLBACK"]?native:decided)
    same => n(native),AMD()
    same => n(decided),NoOp(AMD says ${AMDSTATUS} / ${AMDCAUSE})

    The last argument, 4500, is the client timeout in milliseconds. Keep it there: a shorter timeout turns slow verdicts into dropped people. If your current line passes arguments, such as AMD(2000,2000,1000,5000,120,50,4,256), keep them on the native line so your fallback behaves exactly as today.

  5. Reload and check

    asterisk -rx "dialplan reload"
    asterisk -rx "dialplan show s@outbound-amd"

    Use your own context name in the second command.

The FALLBACK branch and routing on AMDSTATUS and AMDCAUSE

Why the fallback matters

A hosted service can be unreachable, a key can be revoked, a free plan can reach its monthly limit. nomoVM handles all of these the same way: it returns FALLBACK before any audio is used, so the call is still fresh and your dialplan can run Asterisk's AMD() on it. Fallback calls are never billed. A failure after streaming has started, such as a timeout, is different: the script sets MACHINE with cause UNKNOWN, so only a confirmed person reaches an agent.

A complete example context

Here is a full context for a custom dialer that originates calls into s@outbound-amd once they are answered. Adapt the targets to your own queues and agents.

[outbound-amd]
exten => s,1,NoOp(Answered outbound call ${UNIQUEID})
 same => n,EAGI(nomovm.agi.py,wss://amd.nomovm.com/,YOUR_KEY,4500)
 same => n,GotoIf($["${AMDSTATUS}" = "FALLBACK"]?native:decided)
 same => n(native),AMD()
 same => n(decided),Log(NOTICE,AMD ${UNIQUEID} ${AMDSTATUS} ${AMDCAUSE})
 same => n,Set(CDR(userfield)=${AMDSTATUS}:${AMDCAUSE})
 same => n,GotoIf($["${AMDSTATUS}" = "HUMAN"]?human)
 same => n,GotoIf($["${AMDCAUSE}" = "SCREENING"]?screening)
 same => n,GotoIf($["${AMDCAUSE}" = "MACHINE"]?voicemail)
 same => n,Hangup()

 same => n(human),Queue(sales)
 same => n,Hangup()

 same => n(screening),Playback(custom/company-intro)
 same => n,Hangup()

 same => n(voicemail),Playback(custom/voicemail-message)
 same => n,Hangup()

A few notes on that routing:

  • HUMAN goes to agents. Nothing else does.
  • SCREENING means a call screening assistant answered (Google Call Screen or iPhone screening). Its status is MACHINE, so if you don't branch on it, it is treated like any machine. Here it gets a short message that names the company. What is appropriate for your campaign is your call; see call screening detection for the options.
  • MACHINE with cause MACHINE is a voicemail greeting or recorded speech, so it is the right place for a voicemail drop if you use one.
  • SILENCE, TONE, BUSY, UNKNOWN fall through to a hangup. You could also flag BUSY numbers for a retry and TONE numbers (SIT, fax) for list cleaning.
  • NOTSURE and HANGUP can only come from the native AMD() branch. Here they fall through to the hangup. If your dialplan treated NOTSURE differently before, keep that behavior for fallback calls.

FreePBX and Issabel

Both run Asterisk, so the same context works. Put it in /etc/asterisk/extensions_custom.conf, which both include and don't overwrite, send your outbound campaign's answered calls to it, and reload with fwconsole reload on FreePBX or a dialplan reload. Your trunks, routes and queues stay as they are.

VICIdial and GoAutoDial

These run Asterisk too, but they already have an AMD extension (8369) that hands the result to VD_amd.agi. Rather than writing your own context, copy 8369 as 8370 with the EAGI line in front, and switch the campaign's Routing Extension. The VICIdial AMD setup guide has the exact extension.

Testing and logging

Test on real calls before you send a campaign through it:

  • Call your own phone and answer. Say "Hello?" and wait. You should land in the HUMAN branch.
  • Call your own phone and let it ring out to voicemail. You should get MACHINE with cause MACHINE.
  • Break the key on purpose. Put a wrong key in the EAGI line, place a call, and confirm you see FALLBACK / AUTH in the log and that AMD() ran. Put the right key back.
  • Watch the AGI exchange. From the Asterisk CLI, agi set debug on shows the variables the script sets. Turn it off afterwards.

To place a test call without your dialer, originate one from the CLI into the context, using your own trunk and number:

asterisk -rx "channel originate PJSIP/15551230000@your-trunk extension s@outbound-amd"

For logging, the example context writes every verdict to the Asterisk log with Log() and to the CDR userfield, so you can count verdicts per day and per campaign with the tools you already use. The nomoVM dashboard keeps its own call logs with verdicts, causes, usage and CSV export. If you turn recordings on, it keeps only the seconds the service heard, for 1 to 90 days, with playback, and you can delete them any time. Those few seconds are exactly what you need to check decisions, as described in how to test AMD accuracy.

Not on Asterisk? Use the WebSocket API

The Asterisk script is a thin client around a public WebSocket API, and any system that can stream audio can use it directly:

  • Connect to wss://amd.nomovm.com/ with Authorization: Bearer amd_live_... on the handshake.
  • Optionally send a text start message with your own call ID so it shows in your call logs.
  • Send the answered call's audio as binary messages: 8,000 Hz, 16-bit signed little-endian, mono, ideally in 100 ms frames.
  • Read one JSON verdict with amdstatus and amdcause, the same values as above.
  • Use a client timeout of 4,500 ms. On FALLBACK, run your own detection. On any error after streaming started, treat the call as MACHINE / UNKNOWN.

The full message format and an example client are in the API reference. FreeSWITCH users have a ready-made Lua script that uses mod_audio_stream; see FreeSWITCH AMD.

What you get, and what it costs

The change is one script on the dialer you already run. No new servers, no hardware, no carrier change: nomoVM listens to audio Asterisk already has, so it works with any SIP carrier. Expect about 10 minutes from key to first verdict. On 400 real outbound calls checked by a person, 94.9% of live people were sent to agents and 88% of calls sent to agents were live people; the method is on the accuracy page.

Pricing is per answered call that gets a verdict, with no per-server or per-agent fees and unlimited servers: free up to 30,000 calls a month, then $99 a month for 1,000,000 calls, with larger plans on the pricing page. More background is on the Asterisk AMD page. When you're ready, create a free key and replace your first AMD() line.

Frequently asked questions

Which Asterisk versions does nomovm.agi.py support?

Asterisk 13 or newer with EAGI, which is built in. The script needs Python 3.6 or newer and uses only the standard library, so there is nothing to install with pip.

Why EAGI instead of AGI?

EAGI gives the script the call's incoming audio on file descriptor 3, which plain AGI does not. The script streams that audio to nomoVM and sets AMDSTATUS and AMDCAUSE when the verdict arrives.

Do I have to change the rest of my dialplan?

Usually not. nomoVM sets the same AMDSTATUS and AMDCAUSE variables that AMD() sets, so lines that route on AMDSTATUS keep working. You only add the FALLBACK branch, and any new routes you want for causes such as SCREENING.

What should the dialplan do with NOTSURE or HANGUP?

nomoVM never returns them. They can only appear when the FALLBACK branch runs Asterisk's own AMD(), so handle them in the same way your dialplan handled them before you added nomoVM.

Can I use nomoVM without Asterisk?

Yes. Any system that can stream 8 kHz, 16-bit mono audio over a WebSocket can call wss://amd.nomovm.com/ directly with a Bearer key and receive one JSON verdict. FreeSWITCH has its own Lua script that uses mod_audio_stream.

Does it work with FreePBX and Issabel?

Yes, both run Asterisk. Put the custom context in extensions_custom.conf, point your outbound campaign's answered calls at it, and reload. The rest of the system and your carrier stay as they are.

Stop sending voicemail to your agents

30,000 calls a month free. No card, no setup fee. Your dialer falls back to Asterisk AMD() on its own if anything goes wrong.