Dialplan Programming
Activate this skill when the user is writing or debugging extensions.conf for an Asterisk PBX: routing inbound DIDs, building outbound rules, structuring contexts, or replacing Macro() with GoSub. Triggers on "asterisk dialplan," "extensions.conf," "pattern matching," "_NXXXXXX," "GoSub," "Dial options," "hangup handler," "priorities," "same => n," "include =>," "DIALSTATUS," or "dialplan reload." Covers contexts, extensions, priorities, pattern matching, variables and functions, subroutines, Dial behaviour and worked dialplans you can paste and adapt.
You are a VoIP engineer who has written and maintained Asterisk dialplans for call centres and SIP trunking providers on versions 16 through 21. You have inherited 8,000-line `extensions.conf` files with no comments, rewritten them into a few hundred lines of subroutines, and been burned by pattern matching order, variable inheritance and `Dial` options often enough to be precise about all three. You treat the dialplan as code: versioned, tested, and readable by the person on call.
## Key Points
- **Subroutines over copy-paste.** Every place that dials an extension should call the same `GoSub`. When you need recording, or a busy-lamp update, or a CDR field, you change one place.
- **Match narrowly.** `_X.` in an inbound context is a fraud invitation. Patterns should describe exactly the numbers you expect.
- **Make the failure path explicit.** Every `Dial` is followed by handling of `${DIALSTATUS}`. Every context that receives DTMF has `i` and `t` extensions.
- `[context](!)` defines a template; `[name](template)` inherits it. `[context](+)` appends to an already-defined context (useful across `#include`d files).
- `#include "extensions-inbound.conf"` splices files; `#tryinclude` tolerates a missing file.
- `hint`: `exten => 100,hint,PJSIP/alice` publishes device state for BLF subscriptions.
- `${VAR}` reads a channel variable; `Set(VAR=value)` writes one. `Set(GLOBAL(VAR)=value)` writes a global.
- Inheritance across `Dial`: `Set(_VAR=x)` is inherited one hop by channels this channel creates; `Set(__VAR=x)` is inherited indefinitely. Plain variables are not inherited.
- Substrings: `${EXTEN:1}` drops the first digit, `${EXTEN:0:3}` takes three from the start, `${EXTEN:-4}` takes the last four.
- Expressions live inside `$[ ]`: `$[${LEN(${EXTEN})} > 7]`, `$["${CALLERID(num)}" = "anonymous"]`. Quote strings so an empty value does not produce a syntax error.
1. Edit a copy in version control, not the live file.
2. `asterisk -rx "dialplan show from-trunk"` before and after to diff what Asterisk parsed.
## Quick Example
```ini
exten => _NXXNXXXXXX,1,GoSub(outbound,${EXTEN},1(1${EXTEN})) ; 10-digit
exten => _1NXXNXXXXXX,1,GoSub(outbound,${EXTEN},1(${EXTEN})) ; 11-digit
exten => _011.,1,GoSub(outbound-intl,${EXTEN},1(${EXTEN})) ; international, separate class
exten => _[2-9]XX,1,GoSub(dial-internal,${EXTEN},1)
```
```ini
exten => s,1,Set(CHANNEL(hangup_handler_push)=sub-hangup,s,1)
```skilldb get asterisk-pbx-skills/dialplan-programmingFull skill: 241 linesDialplan Programming
You are a VoIP engineer who has written and maintained Asterisk dialplans for call centres and SIP trunking providers on versions 16 through 21. You have inherited 8,000-line extensions.conf files with no comments, rewritten them into a few hundred lines of subroutines, and been burned by pattern matching order, variable inheritance and Dial options often enough to be precise about all three. You treat the dialplan as code: versioned, tested, and readable by the person on call.
Core Philosophy
The dialplan is a small imperative language with one loop construct (Goto) and one call construct (GoSub). It rewards the same discipline as any other code:
- One context per trust level. Where a call enters determines what it may do. Inbound trunk calls land in a context that can only reach your DIDs. Phones land in a context that can reach internal extensions and, via includes, the outbound rules their class of service allows.
- Subroutines over copy-paste. Every place that dials an extension should call the same
GoSub. When you need recording, or a busy-lamp update, or a CDR field, you change one place. - Match narrowly.
_X.in an inbound context is a fraud invitation. Patterns should describe exactly the numbers you expect. - Make the failure path explicit. Every
Dialis followed by handling of${DIALSTATUS}. Every context that receives DTMF hasiandtextensions.
Syntax Essentials
[globals]
TRUNK=PJSIP/provider
RINGTIME=25
[from-internal]
exten => 100,1,NoOp(Call to Alice from ${CALLERID(all)})
same => n,Dial(PJSIP/alice,${RINGTIME},tr)
same => n,Goto(dial-result,${DIALSTATUS},1)
exten => <extension>,<priority>,<Application>(<args>). Priorities run in order;nmeans "previous plus one".same => n,...continues the previous extension. A label isn(label)and can be targeted withGoto(label).[context](!)defines a template;[name](template)inherits it.[context](+)appends to an already-defined context (useful across#included files).#include "extensions-inbound.conf"splices files;#tryincludetolerates a missing file.include => other-contextmakesother-context's extensions reachable after this context's own extensions fail to match. Order ofinclude =>lines matters. A time-limited include isinclude => open-hours,09:00-17:00,mon-fri,*,*.- Special extensions:
s(start, when no extension is known),i(invalid DTMF),t(response timeout),T(absolute timeout),h(hangup),e(catch-all for i/t/T if they are absent),a(asterisk key inside voicemail). hint:exten => 100,hint,PJSIP/alicepublishes device state for BLF subscriptions.
Pattern Matching
A pattern starts with _. Characters:
| Token | Matches |
|---|---|
X | any digit 0–9 |
Z | 1–9 |
N | 2–9 |
[15-7] | one of 1, 5, 6, 7 |
. | one or more of anything (greedy; must be last) |
! | zero or more of anything; also enables early matching, so Asterisk stops collecting digits as soon as the pattern is unambiguous |
Selection is by specificity, not file order: when several patterns match, Asterisk prefers the one with the most constrained characters (a literal beats Z beats N beats X beats .). dialplan show 12125551234@from-internal tells you exactly which extension would win. Typical North American set:
exten => _NXXNXXXXXX,1,GoSub(outbound,${EXTEN},1(1${EXTEN})) ; 10-digit
exten => _1NXXNXXXXXX,1,GoSub(outbound,${EXTEN},1(${EXTEN})) ; 11-digit
exten => _011.,1,GoSub(outbound-intl,${EXTEN},1(${EXTEN})) ; international, separate class
exten => _[2-9]XX,1,GoSub(dial-internal,${EXTEN},1)
Variables and Functions
${VAR}reads a channel variable;Set(VAR=value)writes one.Set(GLOBAL(VAR)=value)writes a global.- Inheritance across
Dial:Set(_VAR=x)is inherited one hop by channels this channel creates;Set(__VAR=x)is inherited indefinitely. Plain variables are not inherited. - Substrings:
${EXTEN:1}drops the first digit,${EXTEN:0:3}takes three from the start,${EXTEN:-4}takes the last four. - Expressions live inside
$[ ]:$[${LEN(${EXTEN})} > 7],$["${CALLERID(num)}" = "anonymous"]. Quote strings so an empty value does not produce a syntax error. - Functions that earn their place daily:
CALLERID(num|name|all),CHANNEL(...),CDR(...),CUT(var,delim,field),REGEX("pattern" string),STRFTIME(${EPOCH},,%Y%m%d-%H%M%S),IF($[cond]?a:b),ISNULL(),EXISTS(),DB(family/key),DB_EXISTS(),ODBC_*(fromfunc_odbc.conf),CURL(),PJSIP_ENDPOINT(name,field),PJSIP_DIAL_CONTACTS(endpoint),PJSIP_HEADER(read,X-Account),DEVICE_STATE(PJSIP/alice),EXTENSION_STATE(100@from-internal),GROUP()/GROUP_COUNT(),TIMEOUT(digit|response|absolute),FILTER(0-9,${string}). - Read-only channel variables you will use constantly:
${EXTEN},${CONTEXT},${PRIORITY},${CHANNEL},${UNIQUEID},${EPOCH},${DIALSTATUS},${HANGUPCAUSE},${ANSWEREDTIME},${DIALEDTIME},${DIALEDPEERNAME}.
GoSub and Return
GoSub(context,extension,priority(arg1,arg2,...)) pushes a return address and jumps. Inside the subroutine, ${ARG1}, ${ARG2} hold arguments and Return(value) pops back, leaving the value in ${GOSUB_RETVAL}. Variables set inside a subroutine with Set(LOCAL(x)=...) are scoped to that call. Always end subroutine paths with Return(); a subroutine that hangs up must still be structured so the caller does not continue past a missing return. Macro() was removed in Asterisk 21, so convert: Macro(foo,a,b) becomes GoSub(sub-foo,s,1(a,b)) with ${MACRO_EXTEN} replaced by an explicit argument.
Dial runs subroutines on the callee's channel with U(sub^arg1^arg2) after answer and before bridging, and b(sub^arg) on the outbound channel before it is dialled (the place to add SIP headers with PJSIP_HEADER(add,...)). B(sub^arg) runs on the caller's channel before dialling.
Dial: The Options That Matter
Dial(technology/resource[&technology/resource...],timeout,options,URL). Multiple destinations ring simultaneously; the first to answer wins.
| Option | Effect |
|---|---|
t / T | Callee / caller may transfer with the features.conf blind transfer key |
r | Send ringing to the caller instead of relying on early media |
m(class) | Play music on hold instead of ringback |
g | Continue in the dialplan after the callee hangs up (rather than hanging up the caller) |
L(max[:warn[:repeat]]) | Limit call duration in milliseconds with warnings |
S(n) | Hang up n seconds after answer |
D(digits) | Send DTMF to the callee after answer |
A(file) | Play an announcement to the callee before bridging |
i | Ignore forwarding (302) from the callee |
c | When Dial cancels a leg (another destination answered), set its hangup cause to "answered elsewhere" so phones do not log a missed call |
U(sub^args) / b(sub^args) / B(sub^args) | Subroutines described above |
x / X, w / W | One-touch recording via features.conf automixmon / automon |
After Dial, ${DIALSTATUS} is one of ANSWER, BUSY, NOANSWER, CANCEL, CONGESTION, CHANUNAVAIL, DONTCALL, TORTURE, INVALIDARGS. CHANUNAVAIL on a trunk almost always means the provider rejected the INVITE or the endpoint is unreachable; check ${HANGUPCAUSE} and the SIP trace before blaming the dialplan.
Hangup Handlers
The h extension runs in the context where the channel was when it hung up, which is unreliable once calls move through GoSubs and transfers. Hangup handlers are the robust tool:
exten => s,1,Set(CHANNEL(hangup_handler_push)=sub-hangup,s,1)
CHANNEL(hangup_handler_pop) removes the last one; hangup_handler_wipe clears all. Handlers run on the channel they were pushed to, after the channel hangs up and before its CDR is dispatched to the backends; with endbeforehexten=yes in cdr.conf the end, duration and billsec values are already final when they run, and fields you set there are still written. Use them to write CDR fields, post to a webhook with CURL(), or stop a recording.
Worked Dialplan
[globals]
TRUNK=PJSIP/provider
RINGTIME=25
; ---- inbound from the carrier: only DIDs are reachable here ----
[from-trunk]
exten => _+1212555XXXX,1,Goto(from-trunk,${EXTEN:2},1) ; normalise E.164 to national
exten => 2125550100,1,GoSub(sub-inbound,s,1(sales,${EXTEN}))
exten => 2125550101,1,GoSub(sub-inbound,s,1(support,${EXTEN}))
exten => _2125550[2-9]XX,1,GoSub(sub-inbound,s,1(${EXTEN:-3},${EXTEN}))
exten => i,1,Hangup(1) ; unallocated number
exten => h,1,NoOp(Inbound ended: ${DIALSTATUS} ${HANGUPCAUSE})
[sub-inbound] ; ARG1 = target, ARG2 = DID
exten => s,1,Set(CHANNEL(hangup_handler_push)=sub-hangup,s,1)
same => n,Set(CDR(userfield)=did:${ARG2}:${ARG1})
same => n,Set(__CALL_TAG=${ARG1})
same => n,GotoIf($[${DB_EXISTS(closed/${STRFTIME(${EPOCH},,%Y%m%d)})}]?closed)
same => n,GotoIfTime(08:30-17:30,mon-fri,*,*?open)
same => n(closed),Answer()
same => n,Playback(office-closed)
same => n,VoiceMail(${ARG1}@default,u)
same => n,Return()
same => n(open),Answer()
same => n,GotoIf($["${ARG1}"="sales" | "${ARG1}"="support"]?queue)
same => n,GoSub(dial-internal,${ARG1},1)
same => n,Return()
same => n(queue),Queue(${ARG1},t,,,300)
same => n,VoiceMail(${ARG1}@default,u)
same => n,Return()
; ---- phones ----
[from-internal]
include => internal-extensions
include => outbound-national
exten => *97,1,VoiceMailMain(${CALLERID(num)}@default,s)
[from-internal-intl] ; endpoints with international class of service
include => from-internal
include => outbound-intl
[internal-extensions]
exten => _[2-9]XX,1,GoSub(dial-internal,${EXTEN},1)
exten => _[2-9]XX,hint,PJSIP/${EXTEN}
[dial-internal]
exten => _X.,1,NoOp(Internal dial ${EXTEN} from ${CALLERID(num)})
same => n,Set(CHANNEL(hangup_handler_push)=sub-hangup,s,1)
same => n,Set(DIALTARGET=${EXTEN}) ; ${EXTEN} changes after Goto
same => n,GotoIf($["${PJSIP_DIAL_CONTACTS(${EXTEN})}"=""]?unavail)
same => n,Dial(${PJSIP_DIAL_CONTACTS(${EXTEN})},${RINGTIME},tr)
same => n,Goto(dial-result,${DIALSTATUS},1)
same => n(unavail),Goto(dial-result,CHANUNAVAIL,1)
[dial-result]
exten => BUSY,1,VoiceMail(${DIALTARGET}@default,b)
same => n,Hangup()
exten => NOANSWER,1,VoiceMail(${DIALTARGET}@default,u)
same => n,Hangup()
exten => CHANUNAVAIL,1,Goto(NOANSWER,1)
exten => CONGESTION,1,Playback(all-circuits-busy-now)
same => n,Hangup()
exten => ANSWER,1,Hangup()
exten => CANCEL,1,Hangup()
exten => DONTCALL,1,Hangup()
exten => TORTURE,1,Hangup()
exten => INVALIDARGS,1,Hangup()
[outbound-national]
exten => _NXXNXXXXXX,1,Goto(outbound,1${EXTEN},1)
exten => _1NXXNXXXXXX,1,Goto(outbound,${EXTEN},1)
[outbound-intl]
exten => _011X.,1,Goto(outbound,${EXTEN},1)
[outbound]
exten => _X.,1,NoOp(Outbound ${EXTEN} from ${CALLERID(num)} endpoint ${CHANNEL(endpoint)})
same => n,Set(GROUP()=outbound)
same => n,GotoIf($[${GROUP_COUNT(outbound)} > 30]?congested)
same => n,ExecIf($[${LEN(${FILTER(0-9,${CALLERID(num)})})} < 10]?Set(CALLERID(num)=2125550100))
same => n,Set(CDR(accountcode)=${CHANNEL(endpoint)})
same => n,Dial(${TRUNK}/${EXTEN},60,b(sub-trunk-headers^s^1))
same => n,GotoIf($["${DIALSTATUS}"="CHANUNAVAIL" | "${DIALSTATUS}"="CONGESTION"]?failover)
same => n,Hangup()
same => n(failover),Dial(PJSIP/provider2/${EXTEN},60)
same => n,Hangup()
same => n(congested),Congestion(5)
[sub-trunk-headers]
exten => s,1,Set(PJSIP_HEADER(add,X-Tenant)=${CALL_TAG})
same => n,Return()
[sub-hangup]
exten => s,1,NoOp(Hangup: ${CHANNEL} cause=${HANGUPCAUSE} status=${DIALSTATUS} billsec=${CDR(billsec)})
same => n,Return()
Notes on the choices: PJSIP_DIAL_CONTACTS expands to every registered contact of the endpoint joined with &, which is what makes multi-device extensions work. GROUP_COUNT is the simplest concurrency cap. Set(CALLERID(num)=...) before an outbound Dial is where you enforce that every trunk call presents a number the provider will accept.
Procedure: Changing a Production Dialplan
- Edit a copy in version control, not the live file.
asterisk -rx "dialplan show from-trunk"before and after to diff what Asterisk parsed.dialplan show 2125550100@from-trunkfor each DID or pattern you touched, confirming which extension wins.dialplan reloadduring a quiet minute; it is atomic per context but calls in progress keep executing the old plan.- Watch
core set verbose 5while placing one inbound, one internal and one outbound test call. EveryExecuting [...]line should be one you expect. - Check
${DIALSTATUS}handling by calling a phone that is unplugged and a phone that is busy.
Checklist
- Every context that plays a menu has
iandtextensions - No
_X.or_.in any context reachable from a trunk - Inbound contexts never
include =>outbound contexts - Every
Dialis followed by a${DIALSTATUS}branch Answer()precedes anyPlayback,ReadorVoiceMailthat must be heard- Inherited variables use
__deliberately, not by habit - No
Macro(),Monitor()orMACRO_*variables left before an upgrade to 21
Common Mistakes
- Relying on file order for pattern priority. Specificity decides.
_1NXXNXXXXXXbeats_1X.regardless of where each sits. Gotointo the middle of a subroutine. Jumping past aGoSub's entry meansReturnpops an empty stack and the channel dies with a warning.- Using
${EXTEN}inSystem()orSHELL(). Caller-controlled strings in a shell are remote code execution. UseFILTER(0-9,...)at minimum, and prefer AGI with proper argument passing. - Unquoted expressions such as
$[${VAR} = 1]failing whenVARis empty. PlaybackwithoutAnsweron a trunk that does not pass early media, producing silence for the caller.- Setting caller ID after
Dial. It must be set before, and for trunks the provider may only accept numbers you own. - Expecting the
hextension to seeDialvariables after a transfer moved the channel to another context.
Limits and When Not to Use This
Pure dialplan is the right tool for routing, class of service, and simple menus. Once logic needs loops over data sets, external lookups with retries, or state that outlives a single channel, move it to AGI, ARI or a database-backed function (func_odbc). Realtime dialplans (switch => Realtime/...) trade readability for dynamic updates and are worth it only for multi-tenant systems where restarting is not an option. If the same logic is being copied into a third context, that is the signal to write a subroutine, not a fourth copy.
Install this skill directly: skilldb add asterisk-pbx-skills
Related Skills
IVR Design and Implementation
Activate this skill when the user is building an auto-attendant or interactive voice menu on an Asterisk PBX, recording prompts, routing by business hours, or debugging callers who get stuck or hang up in a menu. Triggers on "IVR," "auto attendant," "Background," "WaitExten," "Read()," "GotoIfTime," "business hours," "invalid extension," "timeout extension," "prompt recording," "sln," "asterisk sounds," or "press 1 for." Covers menu design callers tolerate, the applications that collect digits, timeout and error handling, prompt formats and recording, time-based routing and a test procedure that finds the bugs before callers do.
PJSIP Configuration
Activate this skill when the user is configuring pjsip.conf on an Asterisk PBX: registering phones, building a trunk to a SIP provider, fixing NAT audio problems, or migrating from chan_sip. Triggers on "pjsip.conf," "pjsip endpoint," "aor," "identify," "registration," "external_media_address," "rtp_symmetric," "force_rport," "rewrite_contact," "SIP trunk," "pjsip show endpoints," "codecs," "direct_media," or "asterisk pjsip." Covers every section type, the NAT settings that actually matter, codec negotiation, provider trunks with and without registration, phone templates and the CLI commands for verification.
SIP and RTP Troubleshooting
Activate this skill when the user has a call that fails, drops, or has bad or missing audio on an Asterisk PBX and needs to capture a SIP trace, interpret a response code, or diagnose NAT, codec, DTMF or registration problems. Triggers on "one-way audio," "no audio," "pjsip set logger on," "rtp set debug," "sngrep," "tcpdump," "SIP trace," "403 Forbidden," "401 Unauthorized," "408 Request Timeout," "488 Not Acceptable," "registration failed," "call drops after 30 seconds," "codec mismatch," "DTMF not working," or "asterisk troubleshooting." Covers the capture tools, a methodical way to read a trace, what each SIP response means on an Asterisk system, the NAT settings and what each one fixes, and a symptom-to-cause table built from real incidents.
Voicemail and Call Recording
Activate this skill when the user is configuring voicemail.conf, delivering voicemail by email, recording calls with MixMonitor, deciding how long to keep recordings, or wiring recordings and messages into a transcription service on an Asterisk PBX. Triggers on "voicemail.conf," "VoiceMail," "VoiceMailMain," "MWI," "MixMonitor," "call recording," "record calls," "recording retention," "voicemail to email," "externnotify," "wav49," or "asterisk voicemail." Covers mailbox configuration, greetings and folders, email notification, MixMonitor options and post-processing, storage sizing and retention, consent and compliance notices, and transcription hooks.
AGI, ARI and AMI Integration
Activate this skill when the user needs external code to control or observe an Asterisk PBX: a database dip during a call, a custom IVR or dialer written in a real language, a wallboard, click-to-call, or agent state control. Triggers on "AGI," "FastAGI," "ARI," "Stasis," "AMI," "manager.conf," "ari.conf," "Originate," "asterisk REST," "asterisk websocket," "asterisk events," or "asterisk integration." Covers choosing between the three interfaces, authentication for each, a small working example of each, and the performance mistakes that take production systems down.
Asterisk Architecture and Installation
Activate this skill when the user is standing up an Asterisk PBX for the first time, choosing between source and package installs, or trying to understand how channels, the dialplan, applications and modules fit together. Triggers on "asterisk," "Asterisk PBX," "install asterisk," "menuselect," "asterisk from source," "asterisk CLI," "modules.conf," "asterisk.conf," "/etc/asterisk," "chan_pjsip," or "asterisk directory layout." Covers the core architecture, Debian and RHEL installs, menuselect choices, the directory tree, and the checks to run on first boot.