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

Source: https://srvscripts.com/guides/asterisk-dialplan-pattern-matching/
Updated: 2026-10-06
Publisher: srvScripts (https://srvscripts.com/)

**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:

| 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 `911` beats `_X.`.

- 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`).

- 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 (`_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](/guides/asterisk-freepbx-toll-fraud-prevention/) 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](https://docs.asterisk.org/Configuration/Dialplan/Pattern-Matching/) · [Asterisk: Dialplan Security](https://docs.asterisk.org/Deployment/Important-Security-Considerations/Dialplan-Security/) · [Asterisk best practices (FILTER, strict patterns)](https://github.com/asterisk/asterisk/blob/master/README-SERIOUSLY.bestpractices.md)

**Related:** [Install Asterisk 22 LTS on Debian 13: Step-by-Step Guide](/guides/install-asterisk-22-debian/) · [Troubleshoot SIP Calls with sngrep and the Asterisk PJSIP Logger](/guides/troubleshoot-sip-sngrep/) · [SIP Response Codes: Lookup for Every SIP Error, With Causes and Fixes](/tools/sip-response-codes/) · [fail2ban for Asterisk and FreePBX: Block SIP Password Guessing](/guides/fail2ban-asterisk-freepbx/)

**See also:** [Asterisk Dialplan Pattern Tester: Which Extension Matches?](/tools/dialplan-pattern-tester/)

## 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}).
