Short answer: A pattern starts with an underscore. X matches 0-9, Z 1-9, N 2-9, [2-4] a set, . one or more of anything and ! zero or more. When several patterns match, Asterisk picks the most specific one (exact numbers first, then the narrowest character sets, then ., then !), not the one written first. Test any number with dialplan show 2125551234@context.
We ran these commands on our lab server (Debian 12, FreePBX 17.0.33, Asterisk 22.11) on 6 October 2026, using a temporary test context that we removed afterwards. The output blocks below are from that run.
Table of Contents
Pattern characters
A plain extension such as 911 or 200 matches only itself. Put an underscore in front and the rest is read as a pattern:
| Character | Matches | Example |
|---|---|---|
X | Any one digit 0-9 | _XXX = any three digits |
Z | Any one digit 1-9 | _00Z. = 00 followed by a non-zero digit, then more |
N | Any one digit 2-9 | _NXXNXXXXXX = a 10-digit North American number |
[1237-9] | One character from the set; - makes a range | _[2-4]XX = 200 to 499 |
. | One or more characters of any kind | _011. = 011 plus at least one more |
! | Zero or more characters | _X! = any digit, with or without more after it |
Two details catch people out. First, . and ! match characters, not just digits, so letters, *, # and symbols get through. Second, X. needs at least two characters: one for the X and at least one for the .. A single dialled digit does not match _X.. Our lab test below shows this.
Build a test context
The safest way to learn patterns is a context that nothing routes into. On FreePBX, add it to /etc/asterisk/extensions_custom.conf (FreePBX does not overwrite that file). On plain Asterisk, add it to extensions.conf. Back the file up first:
cp -p /etc/asterisk/extensions_custom.conf /root/extensions_custom.conf.bak
This is the pattern part of the test context we used on lab3 (we named it srvs-test-voip). Each line only runs NoOp(), which writes a note to the log and does nothing else:
[srvs-test-voip]
exten => 911,1,NoOp(literal 911)
exten => _NXXNXXXXXX,1,NoOp(10-digit NANP)
exten => _1NXXNXXXXXX,1,NoOp(11-digit with leading 1)
exten => _011.,1,NoOp(international 011 prefix)
exten => _00Z.,1,NoOp(international 00 prefix)
exten => _[2-4]XX,1,NoOp(internal extension 200-499)
exten => _X.,1,NoOp(catch-all X dot)
exten => _X!,1,NoOp(catch-all X bang)
Load it without restarting anything:
asterisk -rx "dialplan reload"
asterisk -rx "dialplan show srvs-test-voip"
dialplan show lists literal extensions first, then the patterns in sorted order. It does not keep the order you typed them in.
Test numbers with dialplan show
dialplan show <number>@<context> lists every extension that matches, best match first. These are real results from lab3 (file and line references trimmed):
### 911
'911' => 1. NoOp(literal 911)
'_X.' => 1. NoOp(catch-all X dot)
'_X!' => 1. NoOp(catch-all X bang)
### 2125551234
'_NXXNXXXXXX' => 1. NoOp(10-digit NANP)
'_X.' => 1. NoOp(catch-all X dot)
'_X!' => 1. NoOp(catch-all X bang)
### 00442071234567
'_00Z.' => 1. NoOp(international 00 prefix)
'_X.' => 1. NoOp(catch-all X dot)
'_X!' => 1. NoOp(catch-all X bang)
### 301
'_[2-4]XX' => 1. NoOp(internal extension 200-499)
'_X.' => 1. NoOp(catch-all X dot)
'_X!' => 1. NoOp(catch-all X bang)
### 501
'_X.' => 1. NoOp(catch-all X dot)
'_X!' => 1. NoOp(catch-all X bang)
### 9
'_X!' => 1. NoOp(catch-all X bang)
Notice the last two. 501 is outside [2-4]XX, so only the catch-alls match. A single 9 matches only _X!, because _X. needs at least one more character after the first digit.
Which pattern wins: the ordering rules
The listing is useful, but it is worth proving which line actually runs. We placed test calls into the context with Local channels and read the log:
asterisk -rx "channel originate Local/2125551234@srvs-test-voip application Wait 1"
grep "NoOp(" /var/log/asterisk/full | tail -4
Result on lab3 (timestamps and thread IDs trimmed):
Executing [911@srvs-test-voip:1] NoOp("Local/911@srvs-test-voip-00000003;2", "literal 911") in new stack
Executing [2125551234@srvs-test-voip:1] NoOp("Local/2125551234@srvs-test-voip-00000004;2", "10-digit NANP") in new stack
Executing [501@srvs-test-voip:1] NoOp("Local/501@srvs-test-voip-00000005;2", "catch-all X dot") in new stack
Executing [9@srvs-test-voip:1] NoOp("Local/9@srvs-test-voip-00000006;2", "catch-all X bang") in new stack
The first match in the dialplan show list is the one that ran every time. That follows the ordering rules in the Asterisk documentation:
- Dashes are ignored, except inside a character set range.
- Literal extensions sort before patterns, so
911beats_X.. - Patterns with the most constrained characters win: at each position, a set that matches fewer digits sorts first (
NbeforeX,[2-4]beforeX). - Sets of equal size are sorted in ASCII order.
.sorts after any character set.!sorts after..
So a catch-all _X. never hides a more specific route, wherever you write it in the file. The exception is include =>: Asterisk searches the current context first and then included contexts in order, so a broad pattern in the main context can win over a precise one in an included context.
Common patterns for real dial plans
| Goal | Pattern | Notes |
|---|---|---|
| 3-digit internal extensions 200-499 | _[2-4]XX | No wildcard, so nothing extra can be appended |
| North American 10-digit | _NXXNXXXXXX | Area code and exchange cannot start with 0 or 1 |
| North American 11-digit | _1NXXNXXXXXX | Leading 1 |
| US toll-free | _1800NXXXXXX, _1888NXXXXXX … | One line per toll-free code you allow |
| International from US (011) | _011. | Broad by design; protect it (see below) |
| International from most of the world (00) | _00Z. | Z stops 000 matching |
| UK national | _0[1-3]XXXXXXXXX | Example only; check your country’s numbering plan |
| Strip a 9 for an outside line | _9NXXNXXXXXX then ${EXTEN:1} | Removes the first digit before dialling |
In FreePBX you rarely write these by hand. Outbound Routes have a Dial Patterns section with prepend, prefix, match pattern and CallerID fields, and the pattern field uses the same syntax without the leading underscore. On FreePBX 17 the route also has a Dial patterns wizards menu that fills in common sets such as 7, 10 and 11 digit patterns, US Toll Free Patterns, US Emergency and US International.
Security: keep wildcards away from Dial()
The Asterisk project’s own best-practices file warns that . and ! pass any characters. With a pattern like _X. feeding Dial(PJSIP/${EXTEN}), a caller can send a string such as 500&PJSIP/itsp/14165551212 and turn one call into two, the second out through your provider. The fixes:
- Use the narrowest pattern that works (
_XXXrather than_X.for extensions). - Strip anything that is not a digit before dialling:
Dial(PJSIP/${FILTER(0-9,${EXTEN})}). - Never put outbound or international routes in a context that untrusted callers (trunks, anonymous SIP) can reach.
- Do not pass
${CALLERID(num)}or${CALLERID(name)}unfiltered intoSystem(),SHELL()or the MixMonitor command argument; the Asterisk 22 MixMonitor help itself warns about command injection.
Our toll-fraud prevention checklist builds on these rules.
Clean up and common problems
Remove the test context by restoring your backup, then reload and confirm it is gone:
cp -p /root/extensions_custom.conf.bak /etc/asterisk/extensions_custom.conf
asterisk -rx "dialplan reload"
asterisk -rx "dialplan show srvs-test-voip"
On lab3 the last command returned There is no existence of 'srvs-test-voip' context, which is what you want to see.
- Pattern never matches: check the leading underscore. Without it,
NXXNXXXXXXis a literal extension made of letters. - Single digit not matched by
_X.: expected; use_X!or add a literal extension. - Wrong route chosen: run
dialplan show number@contextand look for a more specific pattern, or a broad pattern in the current context hiding one in an include. - Dialplan changes not live: run
dialplan reload(plain Asterisk) orfwconsole reload(FreePBX). - Warning about
_.in the log: Asterisk logs “The use of ‘_.’ for an extension is strongly discouraged and can have unexpected behavior” when it loads a bare_.or_!. Use_X.or_X!instead.
Official documentation: Asterisk: Pattern Matching · Asterisk: Dialplan Security · Asterisk best practices (FILTER, strict patterns)
Related: Install Asterisk 22 LTS on Debian 13: Step-by-Step Guide · Troubleshoot SIP Calls with sngrep and the Asterisk PJSIP Logger · SIP Response Codes: Lookup for Every SIP Error, With Causes and Fixes · fail2ban for Asterisk and FreePBX: Block SIP Password Guessing
See also: Asterisk Dialplan Pattern Tester: Which Extension Matches?
Frequently asked questions
What does _X. mean in Asterisk?
The underscore marks a pattern, X matches one digit 0-9 and the dot matches one or more further characters. So _X. matches any string of two or more characters that starts with a digit.
What is the difference between . and ! in a pattern?
The dot needs at least one character, the exclamation mark accepts zero. _X. does not match a single digit, but _X! does.
Does the order of lines in extensions.conf decide which pattern wins?
No. Asterisk sorts literal extensions first, then patterns from most to least specific, with . and ! last. Use dialplan show number@context to see the order.
How do I test a pattern without making a call?
Run asterisk -rx “dialplan show 2125551234@yourcontext”. It lists every matching extension, best match first.
Is _X. dangerous?
It can be if the matched value goes straight into Dial() in a context untrusted callers reach, because . also matches symbols such as &. Use tighter patterns or FILTER(0-9,${EXTEN}).