← All Tutorials

ViciDial Custom Dialplan — Advanced extensions.conf Patterns

ViciDial Administration Intermediate 11 min read #60

Master complex call routing, dynamic variable manipulation, and production-grade dialplan logic to build scalable ViciDial campaigns with custom extensions.conf patterns

Prerequisites

Before implementing advanced dialplan patterns in ViciDial, ensure you have:

Understanding ViciDial Dialplan Architecture

Standard ViciDial Dialplan Structure

ViciDial uses Asterisk's dialplan system with several custom contexts. The primary configuration files are:

ViciDial creates channels named with the pattern SIP/xxxx-XXXXXX where xxxx is the extension and XXXXXX is the channel ID. Understanding this naming convention is critical for advanced pattern matching.

Key ViciDial Contexts to Know

[from-internal]           # Agent phone extensions
[from-trunk]              # Inbound calls from carriers
[from-vicidial-internal]  # Internal agent calls
[default]                 # Fallback context

The [from-internal] context handles outbound calls from agents, while [from-trunk] processes inbound calls received from your carrier.

Section 1: Pattern Matching Fundamentals

Understanding Asterisk Pattern Matching

Asterisk pattern matching uses specific wildcard characters:

Pattern Meaning
X Single digit 0-9
Z Single digit 1-9
N Single digit 2-9
[0-9] Character set (single digit)
_PATTERN Literal underscore prefix (required for patterns)
. Wildcard (any length)
! Negation (excludes following pattern)

ViciDial-Specific Pattern Matching

In production ViciDial systems, you'll work with several key pattern types:

; DTMF extension patterns for agent phones
_[2-9]XX          ; Standard 3-digit extensions (200-999)
_[2-9]XXXX        ; 4-digit extensions (2000-9999)
_91NXXNXXXXXX     ; North American NANP dialing (1-800-555-0100)
_9[2-9]XXXXXXX    ; Local 7-digit with trunk prefix
_9011.             ; International dialing

Creating Custom Pattern Examples

[from-internal]
; Route calls prefixed with 7 to external trunk
exten => _7XXXXXXXXXXXX,1,NoOp(Long distance call from ${CALLERID(num)})
 same => n,Set(MYTRUNK=SIP/carrier1)
 same => n,Dial(${MYTRUNK}/${EXTEN:1},30,tT)
 same => n,Hangup()

; Route calls prefixed with 6 to backup trunk
exten => _6XXXXXXXXXXXX,1,NoOp(Backup trunk routing)
 same => n,Dial(SIP/carrier2/${EXTEN:1},30,tT)
 same => n,Hangup()

; Internal extension routing (200-299)
exten => _2XX,1,NoOp(Routing to extension ${EXTEN})
 same => n,Dial(SIP/${EXTEN},20,tT)
 same => n,Voicemail(${EXTEN}@default)
 same => n,Hangup()

Section 2: Variable Manipulation and Dynamic Routing

Setting and Using Channel Variables

ViciDial passes multiple variables through the call chain. The most critical are:

${DIALEDNUMBER}     ; The number actually dialed
${EXTEN}            ; Current extension being processed
${CALLERID(num)}    ; Caller ID number
${CALLERID(name)}   ; Caller ID name
${CHANNEL}          ; Full channel name (SIP/200-00001234)
${CDR(billsec)}     ; Billable seconds
${UNIQUEID}         ; Unique call identifier

Advanced Variable Extraction

When working with long-distance numbers or special routing, extract parts of the EXTEN value:

[from-internal]
; Extract area code from NANP number prefixed with 9
exten => _91NXXNXXXXXX,1,NoOp(NANP routing - EXTEN=${EXTEN})
 same => n,Set(AREACODE=${EXTEN:2:3})
 same => n,Set(PREFIX=${EXTEN:5:3})
 same => n,Set(LINENUMBER=${EXTEN:8:4})
 same => n,Set(FULLNUMBER=${EXTEN:1})
 same => n,NoOp(Extracted - Area:${AREACODE} Prefix:${PREFIX} Line:${LINENUMBER})
 same => n,Dial(SIP/carrier1/${FULLNUMBER},30,tT)
 same => n,Hangup()

; Strip leading 9 for PSTN calls
exten => _9.,1,NoOp(Stripping leading 9, was ${EXTEN})
 same => n,Set(STRIPPED=${EXTEN:1})
 same => n,Set(ORIGEXTEN=${EXTEN})
 same => n,Goto(external,${STRIPPED},1)

Conditional Routing Based on Variables

[from-internal]
exten => _91NXXNXXXXXX,1,NoOp(Advanced conditional routing)
 same => n,Set(CALLINGEXT=${CHANNEL(name):4:3})
 same => n,GotoIf($[${CALLINGEXT} > 250]?premium:standard)

exten => _91NXXNXXXXXX,n(premium),NoOp(Premium route for ext ${CALLINGEXT})
 same => n,Dial(SIP/primary-carrier/${EXTEN:1},30,tT)
 same => n,Goto(next-step)

exten => _91NXXNXXXXXX,n(standard),NoOp(Standard route)
 same => n,Dial(SIP/secondary-carrier/${EXTEN:1},30,tT)

exten => _91NXXNXXXXXX,n(next-step),NoOp(Call completed)
 same => n,Hangup()

Section 3: ViciDial-Specific Dialplan Integration

Capturing ViciDial Campaign Variables

ViciDial populates the v_lead_id, v_campaign_id, and other variables. Use them in custom dialplan:

[from-vicidial-internal]
exten => _X.,1,NoOp(ViciDial outbound call - Campaign: ${v_campaign_id})
 same => n,Set(MYVAR1=${v_lead_id})
 same => n,NoOp(Lead ID: ${MYVAR1})
 same => n,Dial(SIP/${EXTEN},30,tT)
 same => n,Hangup()

To verify what variables ViciDial is passing, check the logs:

asterisk -rx "core set verbose 3"
tail -f /var/log/asterisk/messages | grep "NoOp"

Custom Dialplan Hooks for ViciDial Callbacks

Insert custom logic before calls are logged to vicidial_log:

[from-internal]
exten => _91NXXNXXXXXX,1,NoOp(ViciDial-tracked outbound call)
 same => n,Set(CDR(userfield)=CUSTOM_TAG_${UNIQUEID})
 same => n,Set(CDR(accountcode)=DEPT_${CALLERCONTEXT})
 same => n,Dial(SIP/carrier/${EXTEN:1},30,tT)
 same => n,NoOp(Hangup cause: ${DIALSTATUS})
 same => n,Hangup()

After the call disconnects, query vicidial_log to see your custom fields:

SELECT lead_id, phone_number, call_date, length_in_sec, custom_tag 
FROM vicidial_log 
WHERE call_date > DATE_SUB(NOW(), INTERVAL 1 HOUR) 
LIMIT 10;

Dynamic Caller ID Management

Enforce caller ID based on campaign or extension:

[from-internal]
exten => _91NXXNXXXXXX,1,NoOp(Dynamic CallerID routing)
 same => n,Set(MYCALLERID=${CALLERID(num)})
 same => n,GotoIf($[${MYCALLERID} = 0]?usecampaign:usedefault)

exten => _91NXXNXXXXXX,n(usecampaign),NoOp(Using campaign CallerID)
 same => n,Set(CALLERID(num)=${v_campaign_cid})
 same => n,Goto(docall)

exten => _91NXXNXXXXXX,n(usedefault),NoOp(Using default CallerID)
 same => n,Set(CALLERID(num)=5551234567)

exten => _91NXXNXXXXXX,n(docall),Dial(SIP/carrier/${EXTEN:1},30,tT)
 same => n,Hangup()

Section 4: Advanced Call Recording and Logging

Automatic Call Recording Patterns

ViciDial can record calls automatically. Use dialplan patterns to enforce recording:

[from-internal]
; Record all outbound calls to specific folder
exten => _91NXXNXXXXXX,1,NoOp(Recording outbound NANP call)
 same => n,Set(FILENAME=/var/spool/asterisk/monitor/vicidial_${UNIQUEID})
 same => n,MixMonitor(${FILENAME}.wav,b)
 same => n,Dial(SIP/carrier/${EXTEN:1},30,tT)
 same => n,StopMixMonitor()
 same => n,Hangup()

; Conditional recording based on campaign
exten => _91NXXNXXXXXX,1,NoOp(Campaign: ${v_campaign_id})
 same => n,GotoIf($["${v_campaign_id}" = "SALES"]?record:norecord)

exten => _91NXXNXXXXXX,n(record),MixMonitor(/var/spool/asterisk/monitor/SALES_${UNIQUEID}.wav,b)
 same => n,Goto(dial)

exten => _91NXXNXXXXXX,n(norecord),NoOp(Recording disabled for this campaign)

exten => _91NXXNXXXXXX,n(dial),Dial(SIP/carrier/${EXTEN:1},30,tT)
 same => n,Hangup()

Call Detail Record (CDR) Enhancement

Populate ViciDial's CDR tables with custom data:

[from-internal]
exten => _91NXXNXXXXXX,1,NoOp(Enhanced CDR logging)
 same => n,Set(CDR(userfield)=ROUTE_CARRIER_001)
 same => n,Set(CDR(accountcode)=ACCT_${CALLERCONTEXT})
 same => n,Dial(SIP/carrier/${EXTEN:1},30,tT)
 same => n,Set(FINALSTATUS=${DIALSTATUS})
 same => n,NoOp(Call status: ${FINALSTATUS})
 same => n,Hangup()

Check recorded CDR data:

asterisk -rx "cdr show active"

View in database:

SELECT uniqueid, src, dst, calldate, duration, billsec, userfield, accountcode 
FROM cdr 
WHERE dst LIKE '1%' AND calldate > DATE_SUB(NOW(), INTERVAL 1 HOUR) 
ORDER BY calldate DESC;

Section 5: Failover and Redundancy Patterns

Primary and Secondary Trunk Routing

Implement automatic failover when a carrier is unavailable:

[from-internal]
exten => _91NXXNXXXXXX,1,NoOp(Dual-carrier failover routing)
 same => n,Set(ATTEMPT=1)

exten => _91NXXNXXXXXX,n(retry),GotoIf($[${ATTEMPT} > 2]?fail:attempt)

exten => _91NXXNXXXXXX,n(attempt),NoOp(Attempt ${ATTEMPT})
 same => n,GotoIf($[${ATTEMPT} = 1]?primary:secondary)

exten => _91NXXNXXXXXX,n(primary),NoOp(Trying primary carrier)
 same => n,Dial(SIP/carrier-primary/${EXTEN:1},20,tT)
 same => n,GotoIf($["${DIALSTATUS}" = "ANSWER"]?success:tryagain)

exten => _91NXXNXXXXXX,n(secondary),NoOp(Trying secondary carrier)
 same => n,Dial(SIP/carrier-secondary/${EXTEN:1},20,tT)
 same => n,GotoIf($["${DIALSTATUS}" = "ANSWER"]?success:fail)

exten => _91NXXNXXXXXX,n(tryagain),Set(ATTEMPT=$[${ATTEMPT} + 1])
 same => n,Goto(retry)

exten => _91NXXNXXXXXX,n(success),NoOp(Call successful on ${ATTEMPT} attempt)
 same => n,Hangup()

exten => _91NXXNXXXXXX,n(fail),NoOp(All carriers exhausted)
 same => n,PlayBack(pbx-invalid)
 same => n,Hangup()

Load Balancing Across Multiple Carriers

Distribute calls across carriers based on weight:

[from-internal]
exten => _91NXXNXXXXXX,1,NoOp(Load-balanced routing)
 same => n,Set(RANDOM=${RANDOM(1,100)})
 same => n,NoOp(Random value: ${RANDOM})
 same => n,GotoIf($[${RANDOM} <= 40]?carrier1:carrier2check)

exten => _91NXXNXXXXXX,n(carrier1),NoOp(Routing via Carrier 1 - 40%)
 same => n,Dial(SIP/carrier1/${EXTEN:1},30,tT)
 same => n,Goto(end)

exten => _91NXXNXXXXXX,n(carrier2check),GotoIf($[${RANDOM} <= 80]?carrier2:carrier3)

exten => _91NXXNXXXXXX,n(carrier2),NoOp(Routing via Carrier 2 - 40%)
 same => n,Dial(SIP/carrier2/${EXTEN:1},30,tT)
 same => n,Goto(end)

exten => _91NXXNXXXXXX,n(carrier3),NoOp(Routing via Carrier 3 - 20%)
 same => n,Dial(SIP/carrier3/${EXTEN:1},30,tT)

exten => _91NXXNXXXXXX,n(end),Hangup()

Section 6: Inbound Call Routing and DID Handling

Inbound DID Matching Patterns

ViciDial receives inbound calls in the [from-trunk] context. Map DIDs to campaigns:

[from-trunk]
; Specific 10-digit DID
exten => 5551234567,1,NoOp(Inbound DID for SALES campaign)
 same => n,Set(v_campaign_id=SALES_TEAM)
 same => n,Goto(distribute,1)

; DID range for support
exten => _555123[4-6]XXX,1,NoOp(Support DID range)
 same => n,Set(v_campaign_id=SUPPORT_QUEUE)
 same => n,Goto(distribute,1)

; Catch-all for unmapped DIDs
exten => _X.,1,NoOp(Unknown DID: ${EXTEN})
 same => n,PlayBack(pbx-invalid)
 same => n,Hangup()

[distribute]
exten => 1,1,NoOp(Distributing to campaign ${v_campaign_id})
 same => n,Queue(${v_campaign_id},tT)
 same => n,Hangup()

Inbound Call Screening with Dialplan

Implement number-blocking logic before agents answer:

[from-trunk]
exten => _X.,1,NoOp(Inbound call screening for DID ${EXTEN})
 same => n,Set(CALLER=${CALLERID(num)})
 same => n,Set(BLOCKED=0)
 same => n,GotoIf($["${CALLER}" = "anonymous"]?blocked:checkdb)

exten => _X.,n(checkdb),NoOp(Checking blocked list)
 same => n,Set(BLOCKED=${DB(blocklist/${CALLER})})
 same => n,GotoIf($[${BLOCKED} = 1]?blocked:allowcall)

exten => _X.,n(blocked),NoOp(Call from ${CALLER} is blocked)
 same => n,PlayBack(pbx-invalid)
 same => n,Hangup()

exten => _X.,n(allowcall),NoOp(Call from ${CALLER} allowed)
 same => n,Goto(distribute,1)

[distribute]
exten => 1,1,Queue(MAIN_QUEUE,tT)
 same => n,Hangup()

Store blocked numbers in Asterisk's internal database:

asterisk -rx "database put blocklist 5551234567 1"
asterisk -rx "database show blocklist"

Section 7: Performance Optimization and Best Practices

Minimizing Dialplan Evaluation Time

Use GoSub() for modular, reusable code:

[from-internal]
exten => _91NXXNXXXXXX,1,NoOp(Using GoSub for routing logic)
 same => n,GoSub(routing-context,validate-number,1(${EXTEN}))
 same => n,Set(CARRIERID=${GOSUB_RETURN})
 same => n,Dial(SIP/${CARRIERID}/${EXTEN:1},30,tT)
 same => n,Hangup()

[routing-context]
exten => validate-number,1,NoOp(Received argument: ${ARG1})
 same => n,GotoIf($[${LEN(${ARG1})} < 11]?invalid:valid)

exten => validate-number,n(invalid),Return(carrier-default)
exten => validate-number,n(valid),Return(carrier-premium)

Caching Variables with Dialplan Subroutines

Reduce database lookups:

[from-internal]
exten => _91NXXNXXXXXX,1,NoOp(Cache extension routing info)
 same => n,Set(CACHEKEY=route_${CALLERCONTEXT})
 same => n,Set(CACHED=${DB(routecache/${CACHEKEY})})
 same => n,GotoIf($[! -z ${CACHED}]?usecached:lookup)

exten => _91NXXNXXXXXX,n(usecached),NoOp(Using cached value: ${CACHED})
 same => n,Set(ROUTECARRIER=${CACHED})
 same => n,Goto(dial)

exten => _91NXXNXXXXXX,n(lookup),NoOp(Performing fresh lookup)
 same => n,Set(ROUTECARRIER=carrier1)
 same => n,DB(routecache/${CACHEKEY})=${ROUTECARRIER})

exten => _91NXXNXXXXXX,n(dial),Dial(SIP/${ROUTECARRIER}/${EXTEN:1},30,tT)
 same => n,Hangup()

Debug Logging for Complex Patterns

Enable verbose logging selectively:

[from-internal]
exten => _91NXXNXXXXXX,1,NoOp(=== START CALL ROUTING ===)
 same => n,NoOp(Caller: ${CALLERID(num)} | Extension: ${EXTEN})
 same => n,NoOp(Channel: ${CHANNEL} | UniqueID: ${UNIQUEID})
 same => n,NoOp(=== END DEBUG ===)
 same => n,Dial(SIP/carrier/${EXTEN:1},30,tT)
 same => n,Hangup()

Enable verbose mode in Asterisk CLI:

asterisk -rx "core set verbose 3"
tail -f /var/log/asterisk/messages | grep "NoOp"

Section 8: Troubleshooting

Common Dialplan Syntax Errors

Error: Bad extension syntax

Error: No extension found

Debugging Call Flow

Check what extension was matched:

asterisk -rx "dialplan show from-internal | grep 91NXXNXXXXXX"

Enable debug logging for specific channels:

asterisk -rx "channel originate SIP/200 application echo"

Watch debug logs in real-time:

tail -f /var/log/asterisk/full | grep "Extension.*91"

ViciDial-Specific Issues

Issue: Variables not populating from ViciDial

Verify variables are being set by the ViciDial dialer script. Check /var/log/asterisk/messages:

grep "v_campaign_id\|v_lead_id" /var/log/asterisk/messages

If missing, verify the ViciDial script has permission to execute Asterisk commands:

ls -la /var/run/asterisk.ctl
usermod -aG asterisk root  # Or appropriate user

Issue: Call recordings not appearing

Verify the monitor directory exists and has write permissions:

mkdir -p /var/spool/asterisk/monitor
chown asterisk:asterisk /var/spool/asterisk/monitor
chmod 755 /var/spool/asterisk/monitor

Check for disk space:

df -h /var/spool/asterisk/

Issue: Calls routing to wrong carrier

Add verbose logging to dialplan and compare actual values:

exten => _91NXXNXXXXXX,1,NoOp(Expected: carrier1 | Actual: ${ROUTECARRIER})

Run SQL query to verify carrier SIP peer exists:

SELECT * FROM asterisk.sip_peers WHERE name = 'carrier1';

Reload SIP configuration:

asterisk -rx "sip reload"

Testing Patterns Without Live Calls

Use the Asterisk dialplan test command:

asterisk -rx "dialplan eval _91NXXNXXXXXX"

Manually simulate extension matching:

asterisk -rx "dialplan show from-internal | grep -A5 _91"

Section 9: Advanced Production Patterns

Time-Based Routing

Route calls differently based on hour of day:

[from-internal]
exten => _91NXXNXXXXXX,1,NoOp(Time-based routing)
 same => n,Set(HOUR=${STRFTIME(${EPOCH},%-H)})
 same => n,GotoIf($[${HOUR} >= 9 && ${HOUR} < 17]?businesshours:afterhours)

exten => _91NXXNXXXXXX,n(businesshours),NoOp(Business hours routing)
 same => n,Dial(SIP/primary-carrier/${EXTEN:1},30,tT)
 same => n,Goto(end)

exten => _91NXXNXXXXXX,n(afterhours),NoOp(After-hours routing)
 same => n,Dial(SIP/backup-carrier/${EXTEN:1},30,tT)

exten => _91NXXNXXXXXX,n(end),Hangup()

Geographic Routing

Route based on area code using a database:

[from-internal]
exten => _91NXXNXXXXXX,1,NoOp(Geographic routing based on area code)
 same => n,Set(AREACODE=${EXTEN:2:3})
 same => n,Set(REGION=${DB(regions/${AREACODE})})
 same => n,GotoIf($[! -z ${REGION}]?routeregion:routedefault)

exten => _91NXXNXXXXXX,n(routeregion),NoOp(Routing to ${REGION} carrier)
 same => n,Set(CARRIER=${DB(carriers/${REGION})})
 same => n,Dial(SIP/${CARRIER}/${EXTEN:1},30,tT)
 same => n,Goto(end)

exten => _91NXXNXXXXXX,n(routedefault),Dial(SIP/default-carrier/${EXTEN:1},30,tT)

exten => _91NXXNXXXXXX,n(end),Hangup()

Populate the database:

asterisk -rx "database put regions 415 california"
asterisk -rx "database put regions 212 newyork"
asterisk -rx "database put carriers california carrier-west"
asterisk -rx "database put carriers newyork carrier-east"

Summary

Advanced ViciDial custom dialplan patterns enable sophisticated call routing, failover logic, and integration with external systems. Key takeaways:

  1. Pattern matching forms the foundation—master _X, Z, N, and . wildcards
  2. Variable manipulation with ${EXTEN:offset:length} extracts specific call components
  3. Conditional logic (GotoIf, GoSub) creates dynamic routing rules
  4. ViciDial integration leverages campaign, lead, and caller ID variables for contextual routing
  5. Recording and logging with MixMonitor and CDR enhance compliance and analytics
  6. Failover patterns ensure reliability across multiple carriers
  7. Caching and modular code optimize performance in high-volume environments
  8. Verbose logging and systematic testing prevent production issues

Reload your dialplan safely:

asterisk -rx "dialplan reload"

Monitor changes:

tail -f /var/log/asterisk/full

Test thoroughly in a staging environment before deploying to production. Advanced dialplan patterns are powerful but require careful testing to avoid dropped calls and routing errors.

Need expert help with your setup?

VoIP infrastructure consulting, AI voice agent integration, monitoring stacks, scaling — I've done it all in production.

Get a Free Consultation