Master building production-grade interactive voice response (IVR) systems in ViciDial using Asterisk dialplans, call routing, and real-time menu logic
Prerequisites
Before starting this tutorial, ensure you have:
- ViciDial 2.14+ installed and running (tested on CentOS 7/8 or Ubuntu 18.04+)
- Asterisk 13+ compiled with res_agi, res_http_post, and res_voicemail enabled
- Root or sudo access to the ViciDial server
- A working inbound DID or test extension
- MySQL/MariaDB database access with vicidial user permissions
- Basic understanding of Asterisk dialplans and ViciDial agent flow
- Recorded audio prompts in WAV format (8-bit, 8000 Hz mono recommended)
- At least one active campaign in ViciDial with inbound call handling enabled
Understanding ViciDial IVR Architecture
ViciDial's IVR system sits at the intersection of Asterisk dialplans and the ViciDial database. When an inbound call arrives, it follows this path:
- DID routes to an Asterisk context (usually defined in
extensions-vicidial.conf) - IVR dialplan plays prompts and collects DTMF input
- Digits trigger logic that queries ViciDial database or executes AGI scripts
- Calls route to campaigns, queues, or agents based on menu selections
- Call metadata logs to vicidial_log table
The key difference from traditional Asterisk IVR: ViciDial maintains call state in its database, enabling features like call recording, agent integration, and reporting.
Section 1: Core IVR Dialplan Structure
Creating the Main IVR Context
Your primary IVR context lives in /etc/asterisk/extensions-vicidial.conf. Here's a production-tested template:
[from-vicidial-ivr]
; Main IVR entry point for inbound DID calls
exten => s,1,Answer()
exten => s,n,Set(CALLFILENAME=${UNIQUEID})
exten => s,n,Set(CHANNEL(language)=en)
exten => s,n,NoOp(=== ViciDial IVR Entry - CallID: ${UNIQUEID} ===)
exten => s,n,Set(vicidial_log_id=${SHELL(/usr/share/astguiclient/VICIDIAL_log_insert.agi ${CHANNEL(name)} ${EXTEN} ${CALLERID(num)} ${CALLERID(name)})})
exten => s,n,Goto(ivr_main_menu,s,1)
[ivr_main_menu]
; Main menu - play prompt and collect input
exten => s,1,NoOp(=== Main Menu ===)
exten => s,n,Set(TIMEOUT(digit)=5)
exten => s,n,Set(TIMEOUT(response)=10)
exten => s,n,Background(/var/spool/asterisk/monolithic/recordings/main_menu_prompt)
exten => s,n,WaitExten(2)
; Menu options - route based on DTMF input
exten => 1,1,Goto(sales_queue,s,1)
exten => 2,1,Goto(support_queue,s,1)
exten => 3,1,Goto(billing_queue,s,1)
exten => 0,1,Goto(operator_queue,s,1)
; Default handler - timeout or invalid input
exten => t,1,Playback(invalid_selection)
exten => t,n,Goto(ivr_main_menu,s,1)
exten => i,1,Playback(invalid_selection)
exten => i,n,Goto(ivr_main_menu,s,1)
; Repeat menu option
exten => *,1,Goto(ivr_main_menu,s,1)
[sales_queue]
exten => s,1,NoOp(=== Routing to Sales Queue ===)
exten => s,n,Set(CAMPAIGN_ID=sales_inbound)
exten => s,n,Queue(sales_queue,t,,,300)
exten => s,n,Playback(sorry_all_agents_busy)
exten => s,n,VoiceMail(u1000@vicidial)
exten => s,n,Hangup()
[support_queue]
exten => s,1,NoOp(=== Routing to Support Queue ===)
exten => s,n,Set(CAMPAIGN_ID=support_inbound)
exten => s,n,Queue(support_queue,t,,,300)
exten => s,n,Playback(sorry_all_agents_busy)
exten => s,n,VoiceMail(u1001@vicidial)
exten => s,n,Hangup()
[billing_queue]
exten => s,1,NoOp(=== Routing to Billing Queue ===)
exten => s,n,Set(CAMPAIGN_ID=billing_inbound)
exten => s,n,Queue(billing_queue,t,,,300)
exten => s,n,Playback(sorry_all_agents_busy)
exten => s,n,VoiceMail(u1002@vicidial)
exten => s,n,Hangup()
[operator_queue]
exten => s,1,NoOp(=== Routing to Operator Queue ===)
exten => s,n,Set(CAMPAIGN_ID=operator_inbound)
exten => s,n,Queue(operator_queue,t,,,300)
exten => s,n,Hangup()
Connecting DID to IVR Context
In your inbound route configuration (via Asterisk or ViciDial UI), point the DID to the from-vicidial-ivr context:
[from-trunk]
; Receives calls from SIP trunk
exten => 1234567890,1,Goto(from-vicidial-ivr,s,1)
Alternatively, configure this in /etc/asterisk/sip.conf:
[trunk-provider]
type=peer
host=your.sip.provider
context=from-trunk
insecure=port,invite
Section 2: Database-Driven Menu Logic
Storing Menu Configurations in ViciDial Database
Instead of hardcoding menus in dialplans, use the ViciDial database to define dynamic menus. Create a custom table:
CREATE TABLE vicidial_ivr_menus (
menu_id INT AUTO_INCREMENT PRIMARY KEY,
menu_name VARCHAR(50) UNIQUE NOT NULL,
campaign_id VARCHAR(20),
prompt_filename VARCHAR(255),
timeout_seconds INT DEFAULT 10,
max_attempts INT DEFAULT 3,
created_date DATETIME DEFAULT CURRENT_TIMESTAMP,
modified_date DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
active INT DEFAULT 1
) ENGINE=InnoDB;
CREATE TABLE vicidial_ivr_options (
option_id INT AUTO_INCREMENT PRIMARY KEY,
menu_id INT NOT NULL,
digit_pressed VARCHAR(2),
action_type ENUM('goto_menu', 'queue', 'voicemail', 'hangup', 'agi_script'),
action_target VARCHAR(100),
description VARCHAR(100),
sequence INT,
FOREIGN KEY (menu_id) REFERENCES vicidial_ivr_menus(menu_id),
UNIQUE KEY unique_menu_digit (menu_id, digit_pressed)
) ENGINE=InnoDB;
Insert sample menu configuration:
INSERT INTO vicidial_ivr_menus (menu_name, campaign_id, prompt_filename, timeout_seconds, max_attempts) VALUES
('main_menu', 'ivr_main', '/var/spool/asterisk/monolithic/recordings/main_menu', 10, 3),
('billing_submenu', 'billing_inbound', '/var/spool/asterisk/monolithic/recordings/billing_menu', 8, 2);
INSERT INTO vicidial_ivr_options (menu_id, digit_pressed, action_type, action_target, description, sequence) VALUES
(1, '1', 'queue', 'sales_queue', 'Sales Department', 1),
(1, '2', 'queue', 'support_queue', 'Support Department', 2),
(1, '3', 'goto_menu', 'billing_submenu', 'Billing', 3),
(2, '1', 'queue', 'billing_queue', 'Billing Support', 1),
(2, '2', 'voicemail', 'u1002@vicidial', 'Leave Message', 2);
AGI Script for Dynamic Menu Routing
Create /var/lib/asterisk/agi-bin/vicidial_ivr_router.agi:
#!/usr/bin/perl
# ViciDial IVR Router - Dynamic menu handling via database
use strict;
use Asterisk::AGI;
use DBI;
use Data::Dumper;
my $agi = new Asterisk::AGI;
# Database configuration
my $dbhost = 'localhost';
my $dbname = 'asterisk';
my $dbuser = 'vicidial';
my $dbpass = 'your_db_password';
my $dbh = DBI->connect("DBI:mysql:$dbname:$dbhost", $dbuser, $dbpass)
or die "Cannot connect: $DBI::errstr\n";
# Get variables from Asterisk
my $menu_name = $agi->get_variable('MENU_NAME');
my $digit_pressed = $agi->get_variable('DIGIT_PRESSED');
my $callid = $agi->get_variable('UNIQUEID');
$agi->verbose("IVR Router: Menu=$menu_name, Digit=$digit_pressed", 3);
# Fetch menu configuration
my $menu_query = $dbh->prepare(
"SELECT menu_id, prompt_filename, timeout_seconds FROM vicidial_ivr_menus WHERE menu_name = ? AND active = 1"
);
$menu_query->execute($menu_name) or die "Query failed: $DBI::errstr\n";
my $menu = $menu_query->fetchrow_hashref();
unless ($menu) {
$agi->exec('Playback', 'invalid_selection');
$agi->set_variable('MENU_ACTION', 'retry');
$dbh->disconnect();
exit(0);
}
# Fetch option for pressed digit
my $option_query = $dbh->prepare(
"SELECT action_type, action_target FROM vicidial_ivr_options WHERE menu_id = ? AND digit_pressed = ?"
);
$option_query->execute($menu->{menu_id}, $digit_pressed) or die "Query failed: $DBI::errstr\n";
my $option = $option_query->fetchrow_hashref();
if ($option) {
$agi->verbose("Executing action: $option->{action_type} -> $option->{action_target}", 3);
if ($option->{action_type} eq 'queue') {
$agi->set_variable('QUEUE_NAME', $option->{action_target});
$agi->set_variable('MENU_ACTION', 'queue');
} elsif ($option->{action_type} eq 'goto_menu') {
$agi->set_variable('MENU_NAME', $option->{action_target});
$agi->set_variable('MENU_ACTION', 'goto_menu');
} elsif ($option->{action_type} eq 'voicemail') {
$agi->set_variable('VOICEMAIL_BOX', $option->{action_target});
$agi->set_variable('MENU_ACTION', 'voicemail');
} elsif ($option->{action_type} eq 'hangup') {
$agi->set_variable('MENU_ACTION', 'hangup');
}
} else {
$agi->verbose("No action defined for digit: $digit_pressed", 2);
$agi->set_variable('MENU_ACTION', 'invalid');
}
$dbh->disconnect();
exit(0);
Make the script executable:
chmod 755 /var/lib/asterisk/agi-bin/vicidial_ivr_router.agi
chown asterisk:asterisk /var/lib/asterisk/agi-bin/vicidial_ivr_router.agi
Dialplan Integration with AGI
Update your main dialplan to use the AGI script:
[ivr_dynamic_menu]
exten => s,1,NoOp(=== Dynamic Menu Handler ===)
exten => s,n,Set(TIMEOUT(digit)=5)
exten => s,n,Set(TIMEOUT(response)=${timeout_seconds})
exten => s,n,Background(${prompt_filename})
exten => s,n,WaitExten(2)
exten => 1,1,Set(DIGIT_PRESSED=1)
exten => 1,n,AGI(vicidial_ivr_router.agi)
exten => 1,n,GotoIf($["${MENU_ACTION}"="queue"]?execute_queue,s,1)
exten => 1,n,GotoIf($["${MENU_ACTION}"="goto_menu"]?ivr_dynamic_menu,s,1)
exten => 1,n,GotoIf($["${MENU_ACTION}"="voicemail"]?execute_voicemail,s,1)
exten => 1,n,Hangup()
exten => 2,1,Set(DIGIT_PRESSED=2)
exten => 2,n,AGI(vicidial_ivr_router.agi)
exten => 2,n,GotoIf($["${MENU_ACTION}"="queue"]?execute_queue,s,1)
exten => 2,n,GotoIf($["${MENU_ACTION}"="goto_menu"]?ivr_dynamic_menu,s,1)
exten => 2,n,Hangup()
; Repeat for digits 3-9...
exten => t,1,Playback(invalid_selection)
exten => t,n,Goto(ivr_dynamic_menu,s,1)
exten => i,1,Playback(invalid_selection)
exten => i,n,Goto(ivr_dynamic_menu,s,1)
[execute_queue]
exten => s,1,Queue(${QUEUE_NAME},t,,,300)
exten => s,n,Playback(all_agents_busy)
exten => s,n,Hangup()
[execute_voicemail]
exten => s,1,VoiceMail(${VOICEMAIL_BOX})
exten => s,n,Hangup()
Section 3: Call Recording and ViciDial Integration
Enabling Call Recording in IVR
ViciDial requires explicit recording setup. Add this to your IVR entry context:
[from-vicidial-ivr]
exten => s,1,Answer()
exten => s,n,Set(CALLFILENAME=${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${UNIQUEID})
exten => s,n,Set(CHANNEL(language)=en)
exten => s,n,MixMonitor(/var/spool/asterisk/monolithic/recordings/ivr/${CALLFILENAME}.wav,b)
exten => s,n,Set(RECORDING_FILE=/var/spool/asterisk/monolithic/recordings/ivr/${CALLFILENAME}.wav)
exten => s,n,NoOp(=== ViciDial IVR Recording: ${RECORDING_FILE} ===)
exten => s,n,Goto(ivr_main_menu,s,1)
Ensure the recording directory exists and has proper permissions:
mkdir -p /var/spool/asterisk/monolithic/recordings/ivr
chown asterisk:asterisk /var/spool/asterisk/monolithic/recordings/ivr
chmod 755 /var/spool/asterisk/monolithic/recordings/ivr
Logging IVR Events to ViciDial Database
Create a dialplan macro to log IVR interactions:
[macro-ivr_log_event]
; Arguments: ${ARG1}=event_type, ${ARG2}=menu_name, ${ARG3}=digit_pressed
exten => s,1,NoOp(=== Logging IVR Event ===)
exten => s,n,AGI(agi://<ivr_logger_script>,${ARG1},${ARG2},${ARG3})
[from-vicidial-ivr-logged]
exten => s,1,Answer()
exten => s,n,Set(CALLFILENAME=${UNIQUEID})
exten => s,n,MixMonitor(/var/spool/asterisk/monolithic/recordings/ivr/${CALLFILENAME}.wav,b)
exten => s,n,Macro(ivr_log_event,ivr_entry,main_menu,)
exten => s,n,Goto(ivr_main_menu,s,1)
[ivr_main_menu]
exten => s,1,Set(TIMEOUT(digit)=5)
exten => s,n,Set(TIMEOUT(response)=10)
exten => s,n,Background(/var/spool/asterisk/monolithic/recordings/main_menu_prompt)
exten => s,n,WaitExten(2)
exten => 1,1,Macro(ivr_log_event,digit_pressed,main_menu,1)
exten => 1,n,Goto(sales_queue,s,1)
exten => 2,1,Macro(ivr_log_event,digit_pressed,main_menu,2)
exten => 2,n,Goto(support_queue,s,1)
exten => t,1,Macro(ivr_log_event,timeout,main_menu,)
exten => t,n,Goto(ivr_main_menu,s,1)
exten => i,1,Macro(ivr_log_event,invalid_input,main_menu,)
exten => i,n,Goto(ivr_main_menu,s,1)
Section 4: Advanced Features — Caller Authentication and Call Routing
Account Lookup with DTMF Entry
Allow customers to enter an account number for routing:
[ivr_account_lookup]
exten => s,1,NoOp(=== Account Lookup ===)
exten => s,n,Set(TIMEOUT(digit)=5)
exten => s,n,Playback(enter_account_number)
exten => s,n,Read(ACCOUNT_NUMBER,please_enter_account)
exten => s,n,AGI(agi://<account_validation>,${ACCOUNT_NUMBER})
exten => s,n,GotoIf($["${ACCOUNT_VALID}"="1"]?lookup_success,s,1)
exten => s,n,Playback(account_not_found)
exten => s,n,Goto(ivr_main_menu,s,1)
[lookup_success]
exten => s,1,NoOp(=== Account Found: ${ACCOUNT_NUMBER} ===)
exten => s,n,Set(CAMPAIGN_ID=${ACCOUNT_CAMPAIGN})
exten => s,n,Queue(${ACCOUNT_QUEUE},t,,,300)
exten => s,n,Playback(sorry_all_agents_busy)
exten => s,n,Hangup()
IVR Call Priority Based on Menu Selection
Route calls with different queue priorities:
[ivr_priority_menu]
exten => s,1,NoOp(=== Priority Menu ===)
exten => s,n,Set(TIMEOUT(digit)=5)
exten => s,n,Set(TIMEOUT(response)=10)
exten => s,n,Background(/var/spool/asterisk/monolithic/recordings/priority_menu)
exten => s,n,WaitExten(2)
; High priority (existing customers)
exten => 1,1,Set(QUEUE_PRIORITY=10)
exten => 1,n,Set(CALL_TYPE=existing_customer)
exten => 1,n,Goto(route_to_queue,s,1)
; Standard priority (new customers)
exten => 2,1,Set(QUEUE_PRIORITY=5)
exten => 2,n,Set(CALL_TYPE=new_customer)
exten => 2,n,Goto(route_to_queue,s,1)
; VIP priority
exten => 3,1,Set(QUEUE_PRIORITY=15)
exten => 3,n,Set(CALL_TYPE=vip_customer)
exten => 3,n,Goto(route_to_queue,s,1)
[route_to_queue]
exten => s,1,NoOp(=== Queue with Priority ${QUEUE_PRIORITY} ===)
exten => s,n,Set(QUEUE_PRIORITY=${QUEUE_PRIORITY})
exten => s,n,Queue(sales_queue,t,,,300)
exten => s,n,Hangup()
Section 5: Troubleshooting IVR Issues
Common Problems and Solutions
IVR Not Receiving DTMF Input
Problem: Menu plays but pressing buttons doesn't work.
Diagnosis:
asterisk -rx "sip set debug on"
asterisk -rx "core set verbose 3"
tail -f /var/log/asterisk/messages | grep -i dtmf
Solution: Check for DTMF mode mismatch. ViciDial typically uses RFC2833 (RTP-based):
[sip-vicidial]
dtmfmode=rfc2833
Restart Asterisk:
systemctl restart asterisk
Calls Dropping After Menu Selection
Problem: Call disconnects after pressing a digit.
Diagnosis: Check dialplan syntax:
asterisk -rx "dialplan reload"
asterisk -rx "dialplan show from-vicidial-ivr" | grep -A5 "exten => 1"
Solution: Verify all Goto() statements reference valid contexts and extensions:
exten => 1,1,Goto(sales_queue,s,1)
; Verify that 'sales_queue' context exists with 's' extension
Recording Files Not Found
Problem: Playback fails with "file not found" error.
Diagnosis:
ls -la /var/spool/asterisk/monolithic/recordings/
asterisk -rx "core set verbose 3" | grep -i playback
Solution: Ensure audio files exist and are in correct format:
# Check format
file /var/spool/asterisk/monolithic/recordings/main_menu_prompt.wav
# Convert if necessary
sox input.wav -b 8 -r 8000 output.wav remix -
# Test playback
asterisk -rx "core set verbose 3"
# In dialplan: Playback(/var/spool/asterisk/monolithic/recordings/test_prompt)
IVR Context Not Loading
Problem: "Unknown context" error when dialing DID.
Diagnosis:
asterisk -rx "dialplan show from-vicidial-ivr"
asterisk -rx "core show functions" | grep Load
Solution: Reload dialplan and verify syntax:
# Check syntax first
asterisk -vvvvvc -g 2>&1 | head -50
# Reload
asterisk -rx "dialplan reload"
# Verify
asterisk -rx "dialplan show from-vicidial-ivr" | head -20
AGI Script Errors
Problem: AGI script returns error, calls fail.
Diagnosis:
tail -f /var/log/asterisk/messages | grep agi
asterisk -rx "agi debug on"
Solution: Check script permissions and Perl dependencies:
chmod 755 /var/lib/asterisk/agi-bin/vicidial_ivr_router.agi
perl -c /var/lib/asterisk/agi-bin/vicidial_ivr_router.agi
cpan install Asterisk::AGI
Queue Timeout Not Working
Problem: Calls queue indefinitely instead of timing out.
Diagnosis:
asterisk -rx "queue show sales_queue"
Solution: Verify queue timeout is set in dialplan:
exten => s,1,Queue(sales_queue,t,,,300)
; ^^^ timeout in seconds
Also check queue definition in queues.conf:
[sales_queue]
member => SIP/agent1
member => SIP/agent2
timeout=20
Database Connection Failures in AGI
Problem: AGI script fails to connect to database.
Diagnosis:
mysql -h localhost -u vicidial -p asterisk -e "SELECT COUNT(*) FROM vicidial_ivr_menus;"
Solution: Verify credentials and permissions:
# From MySQL:
GRANT SELECT ON asterisk.vicidial_ivr_menus TO 'vicidial'@'localhost';
FLUSH PRIVILEGES;
# Test Perl DBI connection
perl -e 'use DBI; my $dbh = DBI->connect("DBI:mysql:asterisk:localhost", "vicidial", "password"); print "OK\n";'
Section 6: Monitoring and Testing Your IVR
Asterisk CLI Commands for IVR Debugging
Monitor active calls:
asterisk -rx "core show calls"
View queue statistics:
asterisk -rx "queue show sales_queue"
Monitor real-time verbose output:
asterisk -rx "core set verbose 3"
tail -f /var/log/asterisk/messages
Test dialplan routing:
asterisk -rx "dialplan eval 1234567890 from-vicidial-ivr s"
Check for syntax errors:
asterisk -rx "dialplan reload"
Manual IVR Testing
From the Asterisk CLI, originate a test call:
asterisk -rx "channel originate SIP/test-agent Extension 1000@from-vicidial-ivr"
Test IVR audio playback:
asterisk -rx "console dial 1000@from-vicidial-ivr"
Accessing ViciDial Logs
View call logs in ViciDial admin:
- Navigate to
/vicidial/admin.php - Reports → Call Log
- Filter by DID or time range
Query call logs directly:
SELECT * FROM vicidial_log WHERE called_number = '1234567890'
AND call_date >= NOW() - INTERVAL 1 HOUR
ORDER BY call_date DESC LIMIT 20;
Check agent state during IVR interaction:
SELECT user, status, phone_number, call_date FROM vicidial_agent_log
WHERE call_date >= NOW() - INTERVAL 1 HOUR
ORDER BY call_date DESC;
Summary
You now have a comprehensive production-grade ViciDial IVR setup with:
- Core IVR dialplans that route inbound calls through menu hierarchies
- Database-driven dynamic menus using custom tables for flexible configuration
- AGI script integration for intelligent call routing and account lookup
- Call recording and logging that integrates with ViciDial's reporting
- Advanced features including caller authentication, priority routing, and multi-step menus
- Troubleshooting techniques for common IVR issues
- Monitoring tools to track and debug call flows in real-time
For production deployment:
- Backup your Asterisk configuration before making changes
- Test all dialplans with test calls before going live
- Monitor logs during the first 24 hours after deployment
- Set appropriate queue timeouts based on your staffing
- Record audio prompts professionally to improve caller experience
- Document custom contexts for future maintenance
- Implement call recording for compliance and training
- Schedule regular database maintenance on your custom IVR tables
The IVR system scales with ViciDial's queue management, allowing hundreds of simultaneous calls with intelligent routing based on business rules, not just static menus.