DEV Community

Moisi Trungu
Moisi Trungu

Posted on • Originally published at flixzone.icu

ViciDial Custom Dialplan — Advanced extensions.conf Patterns

ViciDial Custom Dialplan — Advanced extensions.conf Patterns

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:

  • ViciDial installed and running (version 2.10+ recommended)
  • Root or administrative access to your Asterisk server
  • Working knowledge of basic Asterisk dialplan syntax (priorities, contexts, extensions)
  • Familiarity with ViciDial database structure and web UI
  • Understanding of SIP configuration basics
  • Access to /etc/asterisk/ directory and ability to reload dialplan
  • Installed packages: asterisk, asterisk-config, vicidial
  • Asterisk CLI access via asterisk -rx commands
  • Text editor (vim, nano, or similar) for config file editing

Understanding ViciDial Dialplan Architecture

Standard ViciDial Dialplan Structure

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

  • /etc/asterisk/extensions-vicidial.conf — Main ViciDial contexts
  • /etc/asterisk/sip-vicidial.conf — SIP peer definitions
  • /etc/asterisk/extensions.conf — System-wide contexts (sometimes included)

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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

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

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()
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Check recorded CDR data:

asterisk -rx "cdr show active"
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Store blocked numbers in Asterisk's internal database:

asterisk -rx "database put blocklist 5551234567 1"
asterisk -rx "database show blocklist"
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Enable verbose mode in Asterisk CLI:

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

Section 8: Troubleshooting

Common Dialplan Syntax Errors

Error: Bad extension syntax

  • Missing underscore before pattern: Use _91NXXNXXXXXX, not 91NXXNXXXXXX
  • Mismatched parentheses in GoSub/Goto statements
  • Invalid characters in priority labels (use alphanumeric and hyphens only)

Error: No extension found

  • Pattern is too specific and doesn't match dialed number
  • Extension case-sensitivity (extensions are lowercase)
  • Missing context definition for Goto/GoSub call

Debugging Call Flow

Check what extension was matched:

asterisk -rx "dialplan show from-internal | grep 91NXXNXXXXXX"
Enter fullscreen mode Exit fullscreen mode

Enable debug logging for specific channels:

asterisk -rx "channel originate SIP/200 application echo"
Enter fullscreen mode Exit fullscreen mode

Watch debug logs in real-time:

tail -f /var/log/asterisk/full | grep "Extension.*91"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Check for disk space:

df -h /var/spool/asterisk/
Enter fullscreen mode Exit fullscreen mode

Issue: Calls routing to wrong carrier

Add verbose logging to dialplan and compare actual values:

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

Run SQL query to verify carrier SIP peer exists:

SELECT * FROM asterisk.sip_peers WHERE name = 'carrier1';
Enter fullscreen mode Exit fullscreen mode

Reload SIP configuration:

asterisk -rx "sip reload"
Enter fullscreen mode Exit fullscreen mode

Testing Patterns Without Live Calls

Use the Asterisk dialplan test command:

asterisk -rx "dialplan eval _91NXXNXXXXXX"
Enter fullscreen mode Exit fullscreen mode

Manually simulate extension matching:

asterisk -rx "dialplan show from-internal | grep -A5 _91"
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Monitor changes:

tail -f /var/log/asterisk/full
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)