Emergency server help: get in touch

Asterisk Dialplan Pattern Matching: X, N, Z, [ ], . and ! Explained

How Asterisk dialplan patterns work (X, Z, N, sets, . and !), which pattern wins, and how to test with dialplan show, with real output from Asterisk 22.

Published 8 min read

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.

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:

CharacterMatchesExample
XAny one digit 0-9_XXX = any three digits
ZAny one digit 1-9_00Z. = 00 followed by a non-zero digit, then more
NAny 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:

  1. Dashes are ignored, except inside a character set range.
  2. Literal extensions sort before patterns, so 911 beats _X..
  3. Patterns with the most constrained characters win: at each position, a set that matches fewer digits sorts first (N before X, [2-4] before X).
  4. Sets of equal size are sorted in ASCII order.
  5. . sorts after any character set.
  6. ! 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

GoalPatternNotes
3-digit internal extensions 200-499_[2-4]XXNo wildcard, so nothing extra can be appended
North American 10-digit_NXXNXXXXXXArea code and exchange cannot start with 0 or 1
North American 11-digit_1NXXNXXXXXXLeading 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]XXXXXXXXXExample 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 (_XXX rather 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 into System(), 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, NXXNXXXXXX is 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@context and 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) or fwconsole 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}).

Free website test

Is your website set up right?

Check SSL, security headers, redirects, robots.txt, sitemap, llms.txt and security.txt in one test. It takes about 30 seconds.