Business phone numbers and phone menus for the United States and Canada. FRES(510) 288-6288[email protected]Sign in

Home › Asterisk › Auto attendant dialplan

Asterisk

An Asterisk auto attendant, built line by line.

Most Asterisk menu examples stop at "press 1". This one is what a real business needs, in English and French, with every option checked against the Asterisk documentation and, where the documentation is silent or wrong, against the source code.

Last checked 26 October 2026 · 76 cited sources · 25 min read

A phone menu written as a dialplan

What this dialplan does

Most Asterisk menu examples stop at "press 1 for sales". A business needs the rest: a second language, open hours in the right time zone, holidays that move every year, desk phones that ring together, a mobile that rings next without its carrier voicemail swallowing the call, voicemail that arrives by email, and a way to test all of it without waiting for Christmas. This guide builds that, one context at a time, and then gives the whole file.

The call flow, in plain terms:

  1. The call arrives from the SIP trunk and is answered. Callers from a Quebec area code hear French first; everyone else hears English, with the other language one key away (8 for French, 9 for English).
  2. The dialplan checks for a holiday or a one-off closure, then the opening hours. Closed callers hear a closed message with an urgent option; open callers hear the menu.
  3. The menu offers new customers (1), existing jobs (2), an accounts sub-menu (3), hours and address (4), the office (0) and three direct extensions (601 to 603). Three invalid or silent attempts send the caller to a person, never to a hang-up.
  4. Each destination rings desk phones together, then one mobile over the trunk with a "press 1 to accept" screen, then voicemail, which is emailed with the recording attached.

Every application option and default stated here was checked today against docs.asterisk.org, whose reference pages are generated from the Asterisk 23 branch,2 and against the Asterisk source on GitHub wherever we say "the source". The project's versions page currently lists 22 (long-term support) and 23 as fully supported and 24 as a pre-release.1 Every phone number, PIN and email address in the files is fictional.

Four behaviours the reference pages do not tell you, all read in the source: a GotoIfTime range includes its final minute; TESTTIME is ignored by time-based includes; a missing French prompt falls back to English; and a missing menu prompt does not end the call, whatever the BackGround page says.

Contexts, extensions and priorities

The dialplan lives in extensions.conf and is divided into contexts, section headings in square brackets. The documentation describes them as the way to "enforce security boundaries between the various parts of our dialplan".7 That is the main design tool here: calls from the trunk can only reach the menu contexts, and nothing a caller presses can reach a context that dials out with digits the caller chose.

Inside a context, each line is exten => number,priority,application(parameters). Priorities run in order, and a gap stops the call: in the documentation's own example, with priorities 1, 2 and 4, "Asterisk would execute priorities one and two, but would then terminate the call, because it couldn't find priority number three". Writing n ("next") instead of a number avoids that, same => avoids repeating the extension, and a label such as n(menu) names a priority you can jump back to.7

When digits arrive, Asterisk looks for explicit extensions first, then patterns, then included contexts, then switches.7 Patterns start with an underscore: X is any digit, Z is 1 to 9, N is 2 to 9, brackets make a set, . matches one or more remaining characters and ! zero or more. The most specific pattern wins.8 Two warnings from the same page shape this file. First, the wildcards "match more than just digits", and "a malicious caller might try to use these wildcards to bypass security boundaries", which is why the direct extensions below are listed one by one rather than as a pattern. Second, if an extension runs out of priorities, Asterisk can fall through to the same priority of a less specific pattern; the page's example makes extension 6410 say "nine eight seven" and then "five four three two one", and it recommends ending extensions with Hangup().8 Every menu option here does.

The file starts with the [general] and [globals] sections. With autofallthrough at its default, an extension that runs out of things to do is ended "with BUSY, CONGESTION or HANGUP depending on Asterisk's best guess". clearglobalvars=yes matters more than it looks: without it, "global variables will persist through reloads, and even if deleted" from the file, so changing the on-call mobile and reloading would keep ringing the old one.9 Global variables are declared in [globals] and read like any variable.10

; extensions.conf: a bilingual small-business auto attendant.
; Written against the Asterisk 22 LTS and 23 documentation. Every number is fictional.

[general]
static=yes
writeprotect=no
clearglobalvars=yes           ; edits to [globals] take effect on "dialplan reload"

[globals]
BIZ_TZ=America/Toronto        ; zoneinfo name used by every time test below
BIZ_DID=+15145550100          ; the number customers dial
TRUNK=mytrunk                 ; name of the PJSIP endpoint for the SIP trunk
OFFICE=PJSIP/101&PJSIP/102    ; desk phones that ring together
SERVICE_DESK=PJSIP/103
ACCOUNTS_DESK=PJSIP/104
OWNER_MOBILE=+15145550143
ONCALL_MOBILE=+15145550142    ; whoever carries the after-hours phone this week

Our view: put every phone number and device in [globals]. The person who changes this week's on-call mobile should edit one line, not hunt through menus.

Where trunk calls land

; ---- 1. Where calls from the trunk land (the endpoint's context=from-trunk) ----
[from-trunk]
; The extension is the user part of the INVITE's request URI. Providers format it
; differently (+15145550100, 15145550100, 5145550100): check once with
; "pjsip set logger on" and keep only the form yours sends.
exten => +15145550100,1,Goto(ivr-entry,s,1)
exten => 15145550100,1,Goto(ivr-entry,s,1)

A PJSIP endpoint's context option is the "Dialplan context for inbound sessions", and its default value is default.11 Set it explicitly on the trunk endpoint (context=from-trunk) and keep nothing in a context called default that can place an outbound call: an endpoint you forgot to configure lands there.

The extension Asterisk looks up is the user part of the INVITE's request URI; if that is empty, chan_pjsip uses s. If the extension does not exist in the endpoint's context, the call is not routed anywhere else: the source logs that it was "rejected because extension not found in context".12 Providers write the number differently, so look at one real INVITE (see the testing section) and keep only the form yours sends.

The menus themselves start at extension s. The documentation is clear that s is "not a catch-all extension", only the place analog calls and macros start, and that it is convenient precisely because it "can't be easily dialed" from BackGround() or WaitExten().13 A caller cannot press their way back to the top of a menu by accident.

Answering, choosing a language, checking the calendar

; ---- 2. Entry: answer, choose the first language, check the calendar ----
[ivr-entry]
exten => s,1,Answer()
 same => n,Set(CIDNUM=${CALLERID(num)})
 same => n,Set(NPA=${CIDNUM:-10:3})              ; area code from the last 10 digits
 same => n,Set(CHANNEL(language)=${IF(${DIALPLAN_EXISTS(quebec-npa,${NPA},1)}?fr:en)})
 same => n,Verbose(2,Call from ${CIDNUM}, area code ${NPA}, language ${CHANNEL(language)})
 same => n,Gosub(sub-closed-today,s,1)
 same => n,GotoIf($[${GOSUB_RETVAL} = 1]?closed,s,1)
 same => n,GotoIfTime(08:00-16:59,mon-fri,*,*,${BIZ_TZ}?ivr-main,s,1)
 same => n,GotoIfTime(09:00-11:59,sat,*,*,${BIZ_TZ}?ivr-main,s,1)
 same => n,Goto(closed,s,1)

; A lookup table, not a menu: nothing includes this context, so no caller can reach it.
; Quebec area codes from the NANPA NPA report of 26 Oct 2026.
[quebec-npa]
exten => 263,1,NoOp()
exten => 273,1,NoOp()         ; assigned; NANPA lists an in-service date of 27 Feb 2027
exten => 354,1,NoOp()
exten => 367,1,NoOp()
exten => 418,1,NoOp()
exten => 438,1,NoOp()
exten => 450,1,NoOp()
exten => 468,1,NoOp()
exten => 514,1,NoOp()
exten => 579,1,NoOp()
exten => 581,1,NoOp()
exten => 819,1,NoOp()
exten => 873,1,NoOp()

Answer(). Answering first puts the channel up before anything else happens. "By default, Asterisk will wait for media for up to 500 ms", and the i option answers without waiting.14 BackGround() would answer the call anyway unless given its n option,2 but an explicit Answer() keeps the order of events obvious when you read a trace.

The first language. ${CIDNUM:-10:3} takes the last ten digits and then the first three of those, so +15145550188 and 5145550188 both give 514: a negative skip counts from the end, and the length caps the result.15 DIALPLAN_EXISTS() returns 1 if a context, extension and priority exist and 0 otherwise,16 IF() picks a value on that,17 and CHANNEL(language) is read-write: it is the "language for sounds played".18 An anonymous or short number simply fails the lookup and gets English.

A lookup table as a context. [quebec-npa] is never included or jumped to, so no caller can reach it; it exists only to be asked about. The codes come from NANPA's area-code report dated 26 October 2026, which lists twelve Quebec codes in service and a thirteenth, 273, assigned with an in-service date of 27 February 2027.19 It is in the table already, so the dialplan does not need editing on that date. Our Quebec area-code page keeps the same list.

A business located in Quebec

The area-code rule suits a business outside Quebec with Quebec customers. For a business in Quebec, the Office québécois de la langue française states that, under the Charter of the French Language, a business's greeting messages must be in French, and that where another language is used French must have priority; its example for a central customer-service line starts in French, gives a short instruction in English, and returns to French.20 In that case set CHANNEL(language)=fr for every caller and keep the English option. This is the regulator's published guidance, not legal advice.

The calendar. Closures are checked before hours, because a holiday on a Tuesday is still a Tuesday. The closure test is a subroutine that ends with Return(1) or Return(0); the value comes back in GOSUB_RETVAL.21 Open hours are two GotoIfTime lines; if neither matches, the call falls through to closed. Both are explained in the hours section below.

; ---- 3. The main menu (open hours) ----
[ivr-main]
exten => s,1,Set(TRIES=0)
 same => n,Set(TIMEOUT(digit)=3)                ; pause allowed between digits of 601
 same => n(menu),Background(custom/main-menu)   ; sounds/en/custom/ or sounds/fr/custom/
 same => n,WaitExten(6)                         ; nothing after this, so a timeout goes to t

exten => 1,1,Verbose(2,Menu 1: new customer)
 same => n,Gosub(sub-ring,s,1(${OFFICE},20,${OWNER_MOBILE},20,100))
 same => n,Hangup()

exten => 2,1,Verbose(2,Menu 2: existing job)
 same => n,Gosub(sub-ring,s,1(${SERVICE_DESK},20,${ONCALL_MOBILE},25,200))
 same => n,Hangup()

exten => 3,1,Goto(ivr-accounts,s,1)             ; a sub-menu

exten => 4,1,Background(custom/hours-and-address)  ; a key press skips ahead
 same => n,Goto(s,menu)

exten => 0,1,Gosub(sub-ring,s,1(${OFFICE},25,,,100))
 same => n,Hangup()

exten => 8,1,Set(CHANNEL(language)=fr)          ; "Pour le service en français, faites le 8"
 same => n,Goto(s,menu)
exten => 9,1,Set(CHANNEL(language)=en)          ; "For service in English, press 9"
 same => n,Goto(s,menu)

; Dial-by-extension, listed one by one: no wildcard pattern a caller can feed.
exten => 601,1,Gosub(sub-ring,s,1(PJSIP/101,20,,,100))
 same => n,Hangup()
exten => 602,1,Gosub(sub-ring,s,1(PJSIP/102,20,,,100))
 same => n,Hangup()
exten => 603,1,Gosub(sub-ring,s,1(PJSIP/103,20,,,200))
 same => n,Hangup()

exten => i,1,GotoIf($[${INC(TRIES)} >= 3]?giveup,1)
 same => n,Playback(option-is-invalid)
 same => n,Goto(s,menu)

exten => t,1,GotoIf($[${INC(TRIES)} >= 3]?giveup,1)
 same => n,Playback(are-you-still-there)
 same => n,Goto(s,menu)

exten => giveup,1,Verbose(2,No valid choice after ${TRIES} attempts)
 same => n,Gosub(sub-ring,s,1(${OFFICE},20,,,100))
 same => n,Hangup()

Why BackGround and not Playback. BackGround() plays its files "while waiting for an extension to be dialed by the calling channel", and a digit pressed mid-sentence sends the caller straight to that extension.2 Playback() is the uninterruptible one: its own reference points to BackGround "for playing sound files that are interruptible".22 A menu built on Playback makes every repeat caller sit through the whole recording. We use Playback only for short notices (the invalid-entry and "are you still there" prompts), and option 4 uses BackGround for the same reason as the menu.

BackGround's options are worth knowing: s skips playback if the channel is not answered, n plays without answering, p makes the prompt uninterruptible, and m means "only break if a digit hit matches a one digit extension in the destination context".2 We do not use m: it would stop the 6 of extension 601 from interrupting the prompt.

WaitExten. BackGround stops listening when the prompt ends, and the documentation says WaitExten() "should be used" to keep waiting.2 Its argument is in seconds and may be fractional; its m(class) option plays hold music and d a dial tone while it waits.23 Without an argument it uses the response timeout,24 which defaults to 10 seconds.25

What happens on a timeout depends on what follows WaitExten. The source checks, in order: if the extension has another priority, it logs "Timeout ... continuing" and runs it; otherwise it goes to t; otherwise, if an e extension exists, it raises a RESPONSETIMEOUT exception; otherwise it logs a warning and hangs up.3 That is why WaitExten is the last priority of each menu here: put anything after it and t never runs.

i and t. When digits entered during BackGround or WaitExten match nothing in the context, the call goes to i; when the caller enters nothing and there are no more priorities, it goes to t. e can catch i, t and T in one place.13 The documentation's tutorial shows the verbose trace you will see: "Invalid extension '3' in context 'demo-menu'", then the i priorities.26 The two prompts used here, option-is-invalid and are-you-still-there, are not in the core sound set: the tutorial notes its menu prompts come from the extra sounds packages,29 and the French extra package contains both.30

Digit timing. The digit timeout is "the maximum amount of time permitted between digits", 5 seconds by default, and "if an extension typed in is valid, it will not have to timeout to be tested".25 The source adds a detail that decides how you number a menu: when a single digit pressed during BackGround is a complete extension and no longer extension could start with it, Asterisk jumps at once; if a longer one could, it waits.3 With extensions 601 to 603, pressing 6 waits for more digits, which is what we want. Had we used 1XX extensions, every caller pressing 1 would wait for the digit timeout before anything happened. That is also why TIMEOUT(digit) is lowered to 3 seconds.

The retry counter. INC() "increments the value of a variable, while returning the updated value"; its reference warns that INC(${MyVAR}) "is wrong, as INC expects the variable name".27 TRIES is set at priority 1, before the menu label, so looping back does not reset it, and i and t share it. On the third failure the caller goes to giveup, a named extension no keypad can dial, which rings the office and then takes a message. Our view: never hang up on a confused caller. Three tries and a person is the right default for a small business.

One more reason to test every Goto: if a target does not exist, Asterisk tries i in the current context, then h, and otherwise hangs up; jump to a context that does not exist and neither can be found.28

Business hours, time zones and holidays

GotoIfTime. The syntax is GotoIfTime(times,weekdays,mdays,months,[timezone]?labeliftrue:labeliffalse); if the chosen label is omitted, execution simply continues.31 The parser in the source accepts names or numbers for days and months, ranges that wrap around (fri-mon), several ranges joined with &, and keeps one bit per minute.4 The sample file's comment that minutes are "fine-grained only down to the closest even minute" is out of date: the code says "1-minute per bit".94

The end minute is included

After filling a range, the source marks the end minute too ("Do the last one"). So 08:00-17:00 still matches at 17:00:59. To stop answering the menu at five o'clock, write 08:00-16:59, as this file does.4

Time zones. The fifth field is a zoneinfo name such as America/Toronto; the source passes it to the time conversion, and with no fifth field the time is evaluated in the server's own zone.4 Our view: always pass it, from one global. Servers are often set to UTC, and a business with offices in two zones needs it anyway.

Why not time-based includes? Asterisk can also attach a time to an include: include => daytime,9:00-17:00,mon-fri,*,*, with an optional time zone as a fifth field.9 It works, but includes are searched after a context's own extensions,7 the documentation warns against "overlapping patterns or duplicate extensions among included contexts",32 and there is a practical problem: you cannot test them. The TESTTIME() function sets an alternate time "to test dialplan timing conditions at times other than the current time",33 and GotoIfTime reads it,3 but a timed include is checked against the current clock only.5 We keep all time logic in GotoIfTime so it can be tested.

TESTTIME's reference says the date is "in ISO 8601 format"; the parser in the source reads YYYY/MM/DD HH:MM:SS followed by an optional zone name, with slashes.3334 The test extensions in the internal context use that form.

; ---- 8. Holidays and one-off closures. Returns 1 if closed all day today. ----
[sub-closed-today]
exten => s,1,Set(TODAY=${STRFTIME(${EPOCH},${BIZ_TZ},%Y-%m-%d)})
 same => n,GotoIf(${DB_EXISTS(closed/${TODAY})}?yes)   ; CLI: database put closed 2026-12-24 1
 ; Fixed dates recur every year: the time specification has no year field.
 same => n,GotoIfTime(*,*,1,jan,${BIZ_TZ}?yes)          ; New Year's Day
 same => n,GotoIfTime(*,*,25,dec,${BIZ_TZ}?yes)         ; Christmas Day
 same => n,GotoIfTime(*,mon,1-7,sep,${BIZ_TZ}?yes)      ; Labour Day (CA) and Labor Day (US)
 ; Canada (federal list; add your province's days)
 same => n,GotoIfTime(*,mon,18-24,may,${BIZ_TZ}?yes)    ; Victoria Day: Monday before 25 May
 same => n,GotoIfTime(*,*,1,jul,${BIZ_TZ}?yes)          ; Canada Day
 same => n,GotoIfTime(*,mon,2,jul,${BIZ_TZ}?yes)        ; 2 July when 1 July is a Sunday
 same => n,GotoIfTime(*,mon,8-14,oct,${BIZ_TZ}?yes)     ; Thanksgiving: second Monday
 same => n,GotoIfTime(*,*,26,dec,${BIZ_TZ}?yes)         ; Boxing Day
 ; United States (federal list; delete what you do not observe)
 same => n,GotoIfTime(*,mon,25-31,may,${BIZ_TZ}?yes)    ; Memorial Day: last Monday
 same => n,GotoIfTime(*,*,4,jul,${BIZ_TZ}?yes)          ; Independence Day
 same => n,GotoIfTime(*,thu,22-28,nov,${BIZ_TZ}?yes)    ; Thanksgiving: fourth Thursday
 same => n,Return(0)
 same => n(yes),Return(1)

Fixed and floating holidays. A time specification has four fields and an optional zone, and no year,4 so a date such as 25 December recurs every year. A holiday defined as "the nth weekday" becomes a weekday plus a seven-day window, because the month, day of the month and day of the week must all match.4 In the United States, Memorial Day is "the last Monday in May", Labor Day "the first Monday in September" and Thanksgiving "the fourth Thursday in November".35 In Canada, Victoria Day is "the first Monday immediately preceding May 25", and Canada Day is 1 July, except that "When July 1 is a Sunday, July 2 is a legal holiday"; that rule needs one line, Monday 2 July, because 2 July is a Monday exactly when 1 July is a Sunday.37 The federal statutes we read do not fix Canadian Thanksgiving's date; the Canada Revenue Agency's list gives Monday 12 October for 2026, the second Monday.3638

The lists here are a starting point, not advice on which days to close. The Canada Labour Code's general holidays also include Good Friday, the National Day for Truth and Reconciliation on 30 September and Remembrance Day,36 the US federal list has five more days,35 and provinces and states add their own.

Holidays that move, and one-off closures. Good Friday follows Easter (3 April in 2026, according to the CRA),38 so it cannot be written as a recurring rule. The first line of the subroutine handles it and every snowstorm: STRFTIME(epoch,timezone,format) formats today's date in the business's zone,39 ${EPOCH} is the current Unix time,40 and DB_EXISTS() returns 1 if a key exists in the Asterisk database.41 Whoever runs the system closes a day from the CLI with database put closed 2026-12-24 1 and reopens it with database del closed 2026-12-24.42 No reload, no file edit.

Ring groups and sequential fallback

; ---- 6. Ring desks together, then one mobile, then voicemail ----
; Gosub(sub-ring,s,1(desks,seconds,mobile,seconds,mailbox)); leave a field empty to skip it.
[sub-ring]
exten => s,1,Set(DESKSTATUS=NONE)
 same => n,GotoIf($["${ARG1}" = ""]?mobile)
 same => n,Dial(${ARG1},${ARG2})                ; without g, returns only if nobody connected
 same => n,Set(DESKSTATUS=${DIALSTATUS})
 same => n,Verbose(2,Desk phones: ${DIALSTATUS})
 same => n(mobile),GotoIf($["${ARG3}" = ""]?vm)
 same => n,Dial(PJSIP/${ARG3}@${TRUNK},${ARG4},f(${BIZ_DID})U(sub-screen^${CIDNUM}))
 same => n,Verbose(2,Mobile leg: ${DIALSTATUS})  ; still ANSWER after a refused screen
 same => n(vm),VoiceMail(${ARG5}@default,${IF($["${DESKSTATUS}" = "BUSY"]?b:u)})
 same => n,Return()

Simultaneous ring. Dial() takes devices joined with &. All of them ring; "As soon as one of the requested channels answers", the caller is connected and "All other channels that were requested will then be hung up".43 (Copy the reference's own parallel example with care: it reads Dial(PJSIP/alice&PJIP/bob,45), with PJSIP misspelt.43) To dial a PJSIP endpoint by name, PJSIP/101 uses the first contact of the AOR of the same name; PJSIP_DIAL_CONTACTS() rings every contact; and PJSIP/number@mytrunk sends the number to the trunk's contact.45

Always give a timeout. If you do not, the reference says it "defaults to 136 years".43 Option r indicates ringing to the caller "even if the called party isn't actually ringing", and m(class) plays hold music until someone answers.43

Sequential fallback, and what DIALSTATUS is for. Dial "will continue if no requested channels can be called, or if the timeout expires". If someone answers, the call is bridged, and only the g option would bring the caller back to the dialplan when the other side hangs up.43 So in this subroutine, reaching the line after a Dial already means nobody was connected, and the next stage can run without testing anything. DIALSTATUS records why: CHANUNAVAIL (for example, the endpoint "is not registered"), CONGESTION, NOANSWER, BUSY (the reference notes some phones send 486 Busy when Do Not Disturb is on), ANSWER, CANCEL and a few privacy values.43 If the office phones are unplugged, the first Dial returns at once with CHANUNAVAIL and the mobile rings immediately. We keep the desk result to choose the voicemail greeting, the same busy-or-unavailable pattern the documentation's tutorial uses.24

Why the mobile is a second stage. Dial hangs up the other legs as soon as one answers,43 and the source hangs them up before it runs the screening subroutine described next.44 If a mobile rang alongside the desk phones and its carrier voicemail picked up, the desk phones would stop ringing before the screen could reject the voicemail. Ringing desks first and the mobile second avoids that.

Forwarding to mobiles: caller ID and screening

; ---- 7. Screen the mobile leg. Runs on the mobile's channel after it answers. ----
[sub-screen]
exten => s,1,Set(CHANNEL(language)=en)          ; the employee's language, not the caller's
 same => n,Playback(custom/screen-intro)        ; "Business call from"
 same => n,SayDigits(${ARG1:-10})
 same => n,Read(ACCEPT,custom/screen-press-1,1,,2,4)
 same => n,GotoIf($["${ACCEPT}" = "1"]?accept)
 same => n,Set(GOSUB_RESULT=CONTINUE)           ; drop this leg; the caller goes on to voicemail
 same => n(accept),Return()

Whose number the mobile sees. For each new leg, Dial copies the caller's identity into that leg's connected-line information, unless the f option forces a value.44 chan_pjsip builds the outgoing From header from that connected-line identity, and an endpoint's from_user setting then replaces the user part (and drops the display name) whatever the dialplan did.12 The reference describes from_user as the "Username to use in From header for requests to this endpoint".11 By default, then, the employee's mobile shows the customer's number.

Whether your trunk will carry a caller ID that is not one of your numbers is the provider's policy; Dial's own reference notes that "some PSTNs do not allow CallerID to be set to anything other than the numbers assigned to you".43 We did not test any provider's policy for this guide. The file uses f(${BIZ_DID}), which the reference describes as "force the CallerID sent to x".43 In the source it applies to every leg, not only forwarded calls, and sets the presentation to allowed unless the u option says otherwise.44 The mobile shows the business number, and the screen reads out the customer's.

Two trunk settings interact with this. If a caller withheld their number, chan_pjsip writes it into the From header only when the endpoint's trust_id_outbound allows it,12 which the reference describes as whether Asterisk "will send private identification information to the endpoint".11 And send_pai (send a P-Asserted-Identity header) defaults to no; some providers ask for it.11

Screening with U(). The U(x^arg) option runs a subroutine on the called channel "before connecting to the calling channel", starting at extension s, priority 1, of the named context; arguments are separated with ^. The routine can set GOSUB_RESULT to ABORT, CONGESTION, BUSY, CONTINUE ("Hangup the called party and allow the calling party to continue dialplan execution at the next priority") or GOTO.43 Here the employee hears "Business call from" and the customer's last ten digits; SayDigits() speaks them in the channel's language.47 Read() then waits for one digit, with two attempts and a 4-second timeout.46 A person presses 1; a carrier voicemail does not, so the leg is dropped and the caller continues to the company's voicemail instead of the employee's personal one.

The customer's number is passed as an argument because the screen runs on the mobile's channel, which does not see the caller's variables: only variables set with a _ or __ prefix are inherited by channels created from the current one.48

DIALSTATUS lies after a refused screen

When the screen sets CONTINUE, DIALSTATUS still reads ANSWER: the source records ANSWER when the leg picks up, and the CONTINUE branch does not change it. A BUSY result, by contrast, sets DIALSTATUS to BUSY.44 Do not branch on DIALSTATUS = ANSWER after a screened Dial; this subroutine does not need to, for the reason given in the ring-group section.

Our view: keep the screen short. The caller is waiting while it plays, and an employee answering while driving needs one instruction, not three.

Voicemail, and voicemail by email

; voicemail.conf
[general]
format=wav49|wav               ; the first format is the one attached to the email
attach=yes
[email protected]
fromstring=Phone system
emailsubject=New voicemail from ${VM_CALLERID} (${VM_DUR})
emailbody=Mailbox ${VM_MAILBOX} (${VM_NAME}) has a new ${VM_DUR} message from ${VM_CALLERID}, left ${VM_DATE}.\n\nThe recording is attached.\n
emaildateformat=%A %d %B %Y, %H:%M
mailcmd=/usr/sbin/sendmail -t  ; the built-in default; needs a working mail transfer agent
maxsecs=180
minsecs=3
maxlogins=3

[zonemessages]
eastern=America/Toronto|'vm-received' Q 'digits/at' IMp

[default]
; mailbox => PIN,name,email(s) separated by |,pager email,options separated by |
100 => 4826,Office,[email protected]|[email protected],,tz=eastern
200 => 7391,Service,[email protected],,tz=eastern
300 => 5150,Accounts,[email protected],,tz=eastern|delete=yes

VoiceMail(). The syntax is VoiceMail(mailbox@context,options). u plays the unavailable greeting, b the busy greeting, s skips the instructions and U marks the message urgent. During the greeting, 0 jumps to the o extension and * to the a extension of the current context, and the result is left in VMSTATUS (SUCCESS, USEREXIT or FAILED). One line deserves attention: "Dialplan execution will stop if the specified mailbox does not exist."49 A typo in a mailbox number is a dropped call, so test every one. The sample file adds that pressing 0 for an operator needs operator=yes, which is off by default, and an o extension.52 We leave it off.

VoiceMailMain() lets a mailbox owner listen and record greetings; with no mailbox it asks for one, and with no context it uses default.50 Its s option skips the PIN, and the documentation's advice is to "exercise extreme caution" with it.51 Here it is reachable only from staff phones, on *98.

The mailbox line is <mailbox>=<password>,<name>,<email>,<pager_email>,<options>; several email addresses are separated with |, and a PIN prefixed with a minus sign cannot be changed by the user.52 Asterisk rewrites this file itself when a user changes a PIN,52 so keep it out of any configuration tool that would overwrite it.

Email settings. attach=yes attaches the recording; the attached format is "the first format specified in the format parameter" unless a mailbox sets attachfmt.52 wav49 is GSM in a WAV container and wav is signed linear in WAV,54 so the first is small and the second is larger and plain. The sample warns that changing the format list on a system that already holds messages requires converting those messages by hand.52 emailsubject and emailbody take variables such as ${VM_NAME}, ${VM_DUR}, ${VM_MAILBOX}, ${VM_CALLERID} and ${VM_DATE}, and the body is limited to 8,190 characters.52 maxmsg defaults to 100 messages per folder and minsecs to no minimum;52 we set three seconds so hang-up clicks do not reach anyone's inbox. tz picks a zone from [zonemessages] for the date announced before each message.52

Email failures are silent

mailcmd defaults to /usr/sbin/sendmail -t.52 app_voicemail writes the message to a temporary file and launches the command in the background, ( mailcmd < tmp ; rm -f tmp ) &,53 so the dialplan never learns whether the email left. The server needs a working mail transfer agent, and its log is where a failure shows. Be careful with delete=yes, which deletes the voicemail from the server "after notification" and is meant for people who want voicemail only by email:52 combined with a failed send, the message is gone. We use it on one mailbox to show the option; switch it on only once mail delivery is monitored.

Prompts: files, formats and recording your own

Where files live. Sounds are read from the sounds subdirectory of the data directory, /var/lib/asterisk by default.55 Applications take file names without an extension, and Asterisk picks the file.222 Each set of custom prompts in this file therefore exists twice: sounds/en/custom/main-menu.wav and sounds/fr/custom/main-menu.wav, both played by BackGround(custom/main-menu).

How the language picks the file. The documentation explains that the channel's language is split at underscores and used to build directory names, from the most specific to the least, ending at sounds itself.56 The source adds a last step the page leaves out: if nothing is found, it tries the default language.6 That is en unless asterisk.conf says otherwise.57 In practice a French caller who reaches a prompt you have not recorded in French hears the English one rather than silence, which is better than nothing and still a defect. Listen to every prompt in both languages.

A missing prompt does not end the call. The BackGround reference says "If one of the requested sound files does not exist, call processing will be terminated."2 The source does something different: it logs a warning, stops playing, sets BACKGROUNDSTATUS to FAILED and returns normally, so the caller lands in WaitExten in silence.3 Either way the caller has a bad experience, so check BACKGROUNDSTATUS or, better, check the files before a reload.

Formats. When several formats of the same prompt exist, Asterisk "will automatically pick the least CPU intensive format that is available".58 Signed linear WAV at 8 kHz is wav, 16 kHz raw signed linear is .sln16, and µ-law and A-law are handled by the PCM format module; the documentation suggests sox for producing signed linear files from WAV.54 For a studio recording, -r sets the sample rate, -c the channels, -b the bits per sample, -e signed-integer the encoding, and -t raw writes headerless data:63

sox studio-take.wav -r 8000 -c 1 -b 16 /var/lib/asterisk/sounds/en/custom/main-menu.wav
sox studio-take.wav -t raw -r 16000 -c 1 -b 16 -e signed-integer /var/lib/asterisk/sounds/en/custom/main-menu.sln16

Our view: provide the 8 kHz WAV for every prompt, add .sln16 if your phones use a wideband codec, and if your trunk is G.711 µ-law only, a .ulaw copy saves a conversion on every call.

Recording over the phone. The internal context records prompts with Record(), whose syntax is Record(filename.format,silence,maxduration,options); # ends the recording, the result is in RECORD_STATUS, and core show file formats lists the formats your build can write.64 The documentation's own tutorial records en/custom-menu.gsm and then plays custom-menu,65 and the source creates a relative path's directories under sounds/.66 The recording extension is protected with VMAuthenticate(), which checks the PIN of the named mailbox from voicemail.conf.67 Our view: a phone recording is fine for a holiday notice; record the main menu with a decent microphone in a quiet room.

The stock prompts. Prompts distributed with Asterisk are licensed under Creative Commons Attribution-Share Alike 3.0.62 The build's sound selection offers English, French and other languages in core and extra sets, and of the prompt packages only the English core set in GSM is enabled by default,61 so French has to be selected or downloaded: core and extra French packages are published in nine formats, from alaw to wav.59 The French core set's CREDITS file lists its translators and funders, who include two Canadian companies, and its prompt list includes the voicemail instructions ("Veuillez laissez votre message après le bip sonore...").60

The bilingual menu

The menu is bilingual without a single French line in the dialplan. CHANNEL(language) decides which directory every prompt comes from,18 so the same BackGround(custom/main-menu) plays the English recording, which ends "Pour le service en français, faites le 8", or the French one, which ends "For service in English, press 9". Keys 8 and 9 set the language and replay the menu. Reserving both keys in both languages leaves eight options, 0 to 7, which is the same layout our bilingual menu guide recommends.

Other ways to set the language: a PJSIP endpoint's language option sets it for every channel on that endpoint,11 which suits a separate French number on its own endpoint, and BackGround's langoverride argument forces a language for one prompt.2 SayDigits() follows the channel language,47 and so do the stock voicemail instructions, which exist in the French core set.60 The mailbox greeting does not: it is a recording made by the mailbox owner, so record it in both languages, French first if you are in Quebec.20

Our view: write both scripts before you record either, keep the option labels parallel, and have a native speaker of the variety your customers use read the French one. The site's call-routing page covers which language should come first.

; ---- 4. A sub-menu: accounts ----
[ivr-accounts]
exten => s,1,Set(TRIES=0)
 same => n(menu),Background(custom/accounts-menu)
 same => n,WaitExten(6)

exten => 1,1,Gosub(sub-ring,s,1(${ACCOUNTS_DESK},25,,,300))
 same => n,Hangup()

exten => 2,1,Playback(custom/payment-options)
 same => n,Goto(s,menu)

exten => *,1,Goto(ivr-main,s,menu)              ; star: back to the main menu

exten => i,1,GotoIf($[${INC(TRIES)} >= 3]?ivr-main,giveup,1)
 same => n,Playback(option-is-invalid)
 same => n,Goto(s,menu)

exten => t,1,GotoIf($[${INC(TRIES)} >= 3]?ivr-main,giveup,1)
 same => n,Playback(are-you-still-there)
 same => n,Goto(s,menu)

Moving between menus is a Goto: the caller does not come back to where they were. Repeated logic (ringing a team, screening, checking holidays) is a Gosub, which "replaces (is recommended in place of, and deprecates) the Macro application", takes arguments that appear as ${ARG1}, ${ARG2} and so on, and must end with Return() if it is to return.68 The accounts menu has its own i and t, a * extension to go back (the documentation's own day menu uses * as an extension name),65 and on a third failure it reuses the main menu's giveup.

TRIES is a channel variable, so it follows the caller from context to context; the accounts menu resets it on entry so a caller who needed two tries in the main menu is not cut short in the sub-menu. Jumping back with Goto(ivr-main,s,menu) lands after the main menu's reset, so the count carries on from there.

Our view: two levels at most. A sub-menu is worth it when its options would otherwise push the main menu past five or six choices; past two levels, callers stop listening and press 0.

The complete extensions.conf

The pieces above, in order, plus the staff context: desk-to-desk calls, voicemail access, prompt recording and the test extensions used in the next section.

; ---- 9. Staff phones (their endpoints use context=internal) ----
[internal]
exten => _10X,1,Dial(PJSIP/${EXTEN},30)         ; desk to desk
 same => n,Hangup()
exten => *98,1,VoiceMailMain(@default)          ; asks for mailbox number and PIN
exten => *71,1,Gosub(sub-record,s,1(en/custom/main-menu))
 same => n,Hangup()
exten => *72,1,Gosub(sub-record,s,1(fr/custom/main-menu))
 same => n,Hangup()
exten => *90,1,Goto(ivr-entry,s,1)                                  ; the menu, as now
exten => *91,1,Set(TESTTIME()=2026/12/25 10:00:00 America/Toronto)  ; Christmas morning
 same => n,Goto(ivr-entry,s,1)
exten => *92,1,Set(TESTTIME()=2026/10/27 22:00:00 America/Toronto)  ; a Tuesday night
 same => n,Goto(ivr-entry,s,1)

[sub-record]
exten => s,1,Answer()
 same => n,VMAuthenticate(100@default)          ; only someone with the office PIN
 same => n,Record(${ARG1}.wav,3,120)            ; # to stop; 3 s silence or 120 s also stop
 same => n,Wait(1)
 same => n,Playback(${ARG1})
 same => n,Return()

Every endpoint for a staff phone uses context=internal; the trunk endpoint uses context=from-trunk. If these phones also make outside calls, read our emergency calling page before you write that part: the US rules on dialling 911 from multi-line systems concern exactly that outbound dialplan, which this guide does not cover. The full file:

; extensions.conf: a bilingual small-business auto attendant.
; Written against the Asterisk 22 LTS and 23 documentation. Every number is fictional.

[general]
static=yes
writeprotect=no
clearglobalvars=yes           ; edits to [globals] take effect on "dialplan reload"

[globals]
BIZ_TZ=America/Toronto        ; zoneinfo name used by every time test below
BIZ_DID=+15145550100          ; the number customers dial
TRUNK=mytrunk                 ; name of the PJSIP endpoint for the SIP trunk
OFFICE=PJSIP/101&PJSIP/102    ; desk phones that ring together
SERVICE_DESK=PJSIP/103
ACCOUNTS_DESK=PJSIP/104
OWNER_MOBILE=+15145550143
ONCALL_MOBILE=+15145550142    ; whoever carries the after-hours phone this week

; ---- 1. Where calls from the trunk land (the endpoint's context=from-trunk) ----
[from-trunk]
; The extension is the user part of the INVITE's request URI. Providers format it
; differently (+15145550100, 15145550100, 5145550100): check once with
; "pjsip set logger on" and keep only the form yours sends.
exten => +15145550100,1,Goto(ivr-entry,s,1)
exten => 15145550100,1,Goto(ivr-entry,s,1)

; ---- 2. Entry: answer, choose the first language, check the calendar ----
[ivr-entry]
exten => s,1,Answer()
 same => n,Set(CIDNUM=${CALLERID(num)})
 same => n,Set(NPA=${CIDNUM:-10:3})              ; area code from the last 10 digits
 same => n,Set(CHANNEL(language)=${IF(${DIALPLAN_EXISTS(quebec-npa,${NPA},1)}?fr:en)})
 same => n,Verbose(2,Call from ${CIDNUM}, area code ${NPA}, language ${CHANNEL(language)})
 same => n,Gosub(sub-closed-today,s,1)
 same => n,GotoIf($[${GOSUB_RETVAL} = 1]?closed,s,1)
 same => n,GotoIfTime(08:00-16:59,mon-fri,*,*,${BIZ_TZ}?ivr-main,s,1)
 same => n,GotoIfTime(09:00-11:59,sat,*,*,${BIZ_TZ}?ivr-main,s,1)
 same => n,Goto(closed,s,1)

; A lookup table, not a menu: nothing includes this context, so no caller can reach it.
; Quebec area codes from the NANPA NPA report of 26 Oct 2026.
[quebec-npa]
exten => 263,1,NoOp()
exten => 273,1,NoOp()         ; assigned; NANPA lists an in-service date of 27 Feb 2027
exten => 354,1,NoOp()
exten => 367,1,NoOp()
exten => 418,1,NoOp()
exten => 438,1,NoOp()
exten => 450,1,NoOp()
exten => 468,1,NoOp()
exten => 514,1,NoOp()
exten => 579,1,NoOp()
exten => 581,1,NoOp()
exten => 819,1,NoOp()
exten => 873,1,NoOp()

; ---- 3. The main menu (open hours) ----
[ivr-main]
exten => s,1,Set(TRIES=0)
 same => n,Set(TIMEOUT(digit)=3)                ; pause allowed between digits of 601
 same => n(menu),Background(custom/main-menu)   ; sounds/en/custom/ or sounds/fr/custom/
 same => n,WaitExten(6)                         ; nothing after this, so a timeout goes to t

exten => 1,1,Verbose(2,Menu 1: new customer)
 same => n,Gosub(sub-ring,s,1(${OFFICE},20,${OWNER_MOBILE},20,100))
 same => n,Hangup()

exten => 2,1,Verbose(2,Menu 2: existing job)
 same => n,Gosub(sub-ring,s,1(${SERVICE_DESK},20,${ONCALL_MOBILE},25,200))
 same => n,Hangup()

exten => 3,1,Goto(ivr-accounts,s,1)             ; a sub-menu

exten => 4,1,Background(custom/hours-and-address)  ; a key press skips ahead
 same => n,Goto(s,menu)

exten => 0,1,Gosub(sub-ring,s,1(${OFFICE},25,,,100))
 same => n,Hangup()

exten => 8,1,Set(CHANNEL(language)=fr)          ; "Pour le service en français, faites le 8"
 same => n,Goto(s,menu)
exten => 9,1,Set(CHANNEL(language)=en)          ; "For service in English, press 9"
 same => n,Goto(s,menu)

; Dial-by-extension, listed one by one: no wildcard pattern a caller can feed.
exten => 601,1,Gosub(sub-ring,s,1(PJSIP/101,20,,,100))
 same => n,Hangup()
exten => 602,1,Gosub(sub-ring,s,1(PJSIP/102,20,,,100))
 same => n,Hangup()
exten => 603,1,Gosub(sub-ring,s,1(PJSIP/103,20,,,200))
 same => n,Hangup()

exten => i,1,GotoIf($[${INC(TRIES)} >= 3]?giveup,1)
 same => n,Playback(option-is-invalid)
 same => n,Goto(s,menu)

exten => t,1,GotoIf($[${INC(TRIES)} >= 3]?giveup,1)
 same => n,Playback(are-you-still-there)
 same => n,Goto(s,menu)

exten => giveup,1,Verbose(2,No valid choice after ${TRIES} attempts)
 same => n,Gosub(sub-ring,s,1(${OFFICE},20,,,100))
 same => n,Hangup()

; ---- 4. A sub-menu: accounts ----
[ivr-accounts]
exten => s,1,Set(TRIES=0)
 same => n(menu),Background(custom/accounts-menu)
 same => n,WaitExten(6)

exten => 1,1,Gosub(sub-ring,s,1(${ACCOUNTS_DESK},25,,,300))
 same => n,Hangup()

exten => 2,1,Playback(custom/payment-options)
 same => n,Goto(s,menu)

exten => *,1,Goto(ivr-main,s,menu)              ; star: back to the main menu

exten => i,1,GotoIf($[${INC(TRIES)} >= 3]?ivr-main,giveup,1)
 same => n,Playback(option-is-invalid)
 same => n,Goto(s,menu)

exten => t,1,GotoIf($[${INC(TRIES)} >= 3]?ivr-main,giveup,1)
 same => n,Playback(are-you-still-there)
 same => n,Goto(s,menu)

; ---- 5. Closed: nights, weekends, holidays ----
[closed]
exten => s,1,Background(custom/closed)          ; hours, "press 2 if it is urgent", or wait
 same => n,WaitExten(5)

exten => 2,1,Verbose(2,After-hours urgent line)
 same => n,Gosub(sub-ring,s,1(,,${ONCALL_MOBILE},30,200))
 same => n,Hangup()

exten => t,1,VoiceMail(100@default,u)
 same => n,Hangup()

exten => i,1,Goto(t,1)

; ---- 6. Ring desks together, then one mobile, then voicemail ----
; Gosub(sub-ring,s,1(desks,seconds,mobile,seconds,mailbox)); leave a field empty to skip it.
[sub-ring]
exten => s,1,Set(DESKSTATUS=NONE)
 same => n,GotoIf($["${ARG1}" = ""]?mobile)
 same => n,Dial(${ARG1},${ARG2})                ; without g, returns only if nobody connected
 same => n,Set(DESKSTATUS=${DIALSTATUS})
 same => n,Verbose(2,Desk phones: ${DIALSTATUS})
 same => n(mobile),GotoIf($["${ARG3}" = ""]?vm)
 same => n,Dial(PJSIP/${ARG3}@${TRUNK},${ARG4},f(${BIZ_DID})U(sub-screen^${CIDNUM}))
 same => n,Verbose(2,Mobile leg: ${DIALSTATUS})  ; still ANSWER after a refused screen
 same => n(vm),VoiceMail(${ARG5}@default,${IF($["${DESKSTATUS}" = "BUSY"]?b:u)})
 same => n,Return()

; ---- 7. Screen the mobile leg. Runs on the mobile's channel after it answers. ----
[sub-screen]
exten => s,1,Set(CHANNEL(language)=en)          ; the employee's language, not the caller's
 same => n,Playback(custom/screen-intro)        ; "Business call from"
 same => n,SayDigits(${ARG1:-10})
 same => n,Read(ACCEPT,custom/screen-press-1,1,,2,4)
 same => n,GotoIf($["${ACCEPT}" = "1"]?accept)
 same => n,Set(GOSUB_RESULT=CONTINUE)           ; drop this leg; the caller goes on to voicemail
 same => n(accept),Return()

; ---- 8. Holidays and one-off closures. Returns 1 if closed all day today. ----
[sub-closed-today]
exten => s,1,Set(TODAY=${STRFTIME(${EPOCH},${BIZ_TZ},%Y-%m-%d)})
 same => n,GotoIf(${DB_EXISTS(closed/${TODAY})}?yes)   ; CLI: database put closed 2026-12-24 1
 ; Fixed dates recur every year: the time specification has no year field.
 same => n,GotoIfTime(*,*,1,jan,${BIZ_TZ}?yes)          ; New Year's Day
 same => n,GotoIfTime(*,*,25,dec,${BIZ_TZ}?yes)         ; Christmas Day
 same => n,GotoIfTime(*,mon,1-7,sep,${BIZ_TZ}?yes)      ; Labour Day (CA) and Labor Day (US)
 ; Canada (federal list; add your province's days)
 same => n,GotoIfTime(*,mon,18-24,may,${BIZ_TZ}?yes)    ; Victoria Day: Monday before 25 May
 same => n,GotoIfTime(*,*,1,jul,${BIZ_TZ}?yes)          ; Canada Day
 same => n,GotoIfTime(*,mon,2,jul,${BIZ_TZ}?yes)        ; 2 July when 1 July is a Sunday
 same => n,GotoIfTime(*,mon,8-14,oct,${BIZ_TZ}?yes)     ; Thanksgiving: second Monday
 same => n,GotoIfTime(*,*,26,dec,${BIZ_TZ}?yes)         ; Boxing Day
 ; United States (federal list; delete what you do not observe)
 same => n,GotoIfTime(*,mon,25-31,may,${BIZ_TZ}?yes)    ; Memorial Day: last Monday
 same => n,GotoIfTime(*,*,4,jul,${BIZ_TZ}?yes)          ; Independence Day
 same => n,GotoIfTime(*,thu,22-28,nov,${BIZ_TZ}?yes)    ; Thanksgiving: fourth Thursday
 same => n,Return(0)
 same => n(yes),Return(1)

; ---- 9. Staff phones (their endpoints use context=internal) ----
[internal]
exten => _10X,1,Dial(PJSIP/${EXTEN},30)         ; desk to desk
 same => n,Hangup()
exten => *98,1,VoiceMailMain(@default)          ; asks for mailbox number and PIN
exten => *71,1,Gosub(sub-record,s,1(en/custom/main-menu))
 same => n,Hangup()
exten => *72,1,Gosub(sub-record,s,1(fr/custom/main-menu))
 same => n,Hangup()
exten => *90,1,Goto(ivr-entry,s,1)                                  ; the menu, as now
exten => *91,1,Set(TESTTIME()=2026/12/25 10:00:00 America/Toronto)  ; Christmas morning
 same => n,Goto(ivr-entry,s,1)
exten => *92,1,Set(TESTTIME()=2026/10/27 22:00:00 America/Toronto)  ; a Tuesday night
 same => n,Goto(ivr-entry,s,1)

[sub-record]
exten => s,1,Answer()
 same => n,VMAuthenticate(100@default)          ; only someone with the office PIN
 same => n,Record(${ARG1}.wav,3,120)            ; # to stop; 3 s silence or 120 s also stop
 same => n,Wait(1)
 same => n,Playback(${ARG1})
 same => n,Return()

With the voicemail.conf from the voicemail section, that is the whole system. Neither file contains a password for the trunk; that lives in pjsip.conf.

Testing it before customers do

A plain statement first: we have not run this exact file on a live server for this guide. Every line was checked against the documentation and the source, not executed, so run the checks below on your own system before you point a number at it.

The console. Attach to the running server with asterisk -r, or asterisk -rvvv to start at verbosity 3;69 core set verbose 3 does the same from inside.70 The documentation's first troubleshooting step is to set verbosity to three or higher and watch as calls are placed, and to use dialplan show <context> and dialplan show <exten>@<context> to see exactly what will run.71 After editing, dialplan reload rereads the file.72 To see an application's documentation for the version you actually run, use core show application Dial; voicemail show users lists the mailboxes Asterisk loaded.73

The trunk. pjsip set logger on prints SIP traffic,74 which shows you the request URI your provider sends (and therefore the extension [from-trunk] needs) and the From header your mobile legs carry.

Calls without a second phone. channel originate PJSIP/101 extension s@ivr-entry rings desk phone 101 and, when you answer, drops you into the menu as if you had called in. Originated calls have a 30-second timeout, and without a context the command uses default.75 From a staff phone, *90 does the same; *91 and *92 run the menu with TESTTIME set to Christmas morning and a Tuesday night, which exercises the holiday list and the closed path without changing the server clock.333 The AstDB closure uses the real date, so test it with database put closed <today> 1 and remove it afterwards.42

Logs. The documentation's example logger.conf writes notices, warnings, errors, verbose output, DTMF and fax events to a file called full.76 Keep DTMF in it: when a customer says "I pressed 2 and nothing happened", the log shows what they actually pressed.

ScenarioHow to trigger itWhat should happen
Open, English*90 on a weekday morningEnglish menu; 1 rings 101 and 102 together
Quebec callerCall from a 514 or 418 mobileFrench menu first; 9 switches to English
Invalid entriesPress 5 three times"Invalid" twice, then the office rings
SilencePress nothing"Are you still there" twice, then the office rings
Direct extensionDial 602Only 102 rings; voicemail 100 after 20 s
Nobody at the desksOption 1, no answerDesks for 20 s, then the mobile with the screen
Mobile voicemailOption 2; let the mobile go to its voicemailThe screen gets no 1; the caller reaches mailbox 200
After hours*92Closed message; 2 rings the on-call mobile
Holiday*91Closed message
One-off closuredatabase put closed <today> 1Closed message; delete the key afterwards
EmailLeave a message in each mailboxEmail with attachment; nothing in the mail log's errors
Missing promptRename one French promptEnglish plays instead; restore the file

Scroll the table sideways to see every column.

Our view: run the whole table after every change to the dialplan, not just the line you edited, and call the real number from an outside phone once a month.

When this is the wrong tool

Asterisk is a good choice for this if you already run it, if the menu has to reach desk phones on your own network, or if you need behaviour no hosted service offers. The cost is the server around the dialplan: SIP security, updates within a supported release series,1 monitoring, mail delivery and backups, for as long as the business has the number.

If what you need is a menu that forwards to mobiles and takes messages, a hosted service is less work. You can build the same flow on a programmable voice API (our Twilio guides do), or use a hosted auto attendant such as ours. For more on running Asterisk itself, see the Asterisk guides.

Further reading

Longer research pieces on this topic, each with its own source list and a downloadable PDF.

Sources

Every regulatory and pricing claim on this page links to the primary source it came from. Figures were checked on the date shown at the top of the page; rules and prices change, so verify before relying on one.

  1. Asterisk documentation, "Asterisk Versions" (release series, LTS and standard support periods, current status table). Retrieved 26 Oct 2026
    https://docs.asterisk.org/About-the-Project/Asterisk-Versions/
  2. Asterisk documentation, BackGround() application reference (synopsis, BACKGROUNDSTATUS, options s, n, m, p, langoverride, context). Generated from the Asterisk 23 branch. Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Applications/BackGround/
  3. Asterisk source, main/pbx_builtins.c (pbx_builtin_answer, pbx_builtin_waitexten, pbx_builtin_background, pbx_builtin_gotoiftime). master branch, retrieved 26 Oct 2026
    https://raw.githubusercontent.com/asterisk/asterisk/master/main/pbx_builtins.c
  4. Asterisk source, main/pbx_timing.c (ast_build_timing, get_timerange, get_range, ast_check_timing2: parsing and evaluation of time specifications). master branch, retrieved 26 Oct 2026
    https://raw.githubusercontent.com/asterisk/asterisk/master/main/pbx_timing.c
  5. Asterisk source, main/pbx_include.c (include_valid: how a time-restricted include is evaluated). master branch, retrieved 26 Oct 2026
    https://raw.githubusercontent.com/asterisk/asterisk/master/main/pbx_include.c
  6. Asterisk source, main/file.c (fileexists_test and fileexists_core: the language search order for sound files). master branch, retrieved 26 Oct 2026
    https://raw.githubusercontent.com/asterisk/asterisk/master/main/file.c
  7. Asterisk documentation, "Contexts, Extensions, and Priorities" (syntax, priority n, same =>, labels, dialplan search order). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Configuration/Dialplan/Contexts-Extensions-and-Priorities/
  8. Asterisk documentation, "Pattern Matching" (X, Z, N, sets, . and !, sort order, fall-through warning, Caller ID matching). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Configuration/Dialplan/Pattern-Matching/
  9. Asterisk source, configs/samples/extensions.conf.sample ([general] options static, writeprotect, autofallthrough, clearglobalvars; time-based include syntax). master branch, retrieved 26 Oct 2026
    https://raw.githubusercontent.com/asterisk/asterisk/master/configs/samples/extensions.conf.sample
  10. Asterisk documentation, "Global Variables Basics". Retrieved 26 Oct 2026
    https://docs.asterisk.org/Configuration/Dialplan/Variables/Global-Variables-Basics/
  11. Asterisk documentation, res_pjsip module configuration reference (endpoint options context, language, from_user, send_pai, trust_id_inbound, trust_id_outbound; global default_from_user). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Module_Configuration/res_pjsip/
  12. Asterisk source, res/res_pjsip_session.c (get_destination: inbound extension from the request URI; set_from_header: outbound From from the connected-line identity and from_user). master branch, retrieved 26 Oct 2026
    https://raw.githubusercontent.com/asterisk/asterisk/master/res/res_pjsip_session.c
  13. Asterisk documentation, "Special Asterisk Dialplan Extensions" (a, e, h, i, o, s, t, T). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Configuration/Dialplan/Special-Dialplan-Extensions/
  14. Asterisk documentation, Answer() application reference (500 ms media wait, i option). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Applications/Answer/
  15. Asterisk documentation, "Manipulating Variables Basics" (substring syntax with skip and length). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Configuration/Dialplan/Variables/Manipulating-Variables-Basics/
  16. Asterisk documentation, DIALPLAN_EXISTS() function reference. Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Functions/DIALPLAN_EXISTS/
  17. Asterisk documentation, IF() function reference. Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Functions/IF/
  18. Asterisk documentation, CHANNEL() function reference (language: "R/W language for sounds played"). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Functions/CHANNEL/
  19. NANPA, NPA report (CSV), file date 10/26/2026: Quebec area codes, in-service flags and dates, including 273 (assigned, in-service date 27-Feb-2027). File date 26 Oct 2026
    https://reports.nanpa.com/public/npa_report.csv
  20. Office québécois de la langue française, Vitrine linguistique, "Système de réponse vocale interactif, répondeur et boîte vocale" (greeting messages and the Charter of the French Language). Retrieved 26 Oct 2026
    https://vitrinelinguistique.oqlf.gouv.qc.ca/23273/la-redaction-et-la-communication/communications-telephoniques/systeme-de-reponse-vocale-interactif-repondeur-et-boite-vocale
  21. Asterisk documentation, Return() application reference (GOSUB_RETVAL). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Applications/Return/
  22. Asterisk documentation, Playback() application reference. Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Applications/Playback/
  23. Asterisk documentation, WaitExten() application reference (seconds, m and d options). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Applications/WaitExten/
  24. Asterisk documentation, "Background and WaitExten Applications" (tutorial; default response timeout; busy-or-unavailable voicemail example). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Deployment/Basic-PBX-Functionality/Auto-attendant-and-IVR-Menus/Background-and-WaitExten-Applications/
  25. Asterisk documentation, TIMEOUT() function reference (absolute, digit default 5 s, response default 10 s). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Functions/TIMEOUT/
  26. Asterisk documentation, "Handling Special Extensions" (tutorial with i and t, and the verbose trace of an invalid entry). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Deployment/Basic-PBX-Functionality/Auto-attendant-and-IVR-Menus/Handling-Special-Extensions/
  27. Asterisk documentation, INC() function reference. Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Functions/INC/
  28. Asterisk documentation, Goto() application reference (bogus targets fall to i, then h, then hang up). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Applications/Goto/
  29. Asterisk documentation, "Creating a Simple IVR Menu" (notes that the example prompts are in the extra sounds packages). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Deployment/Basic-PBX-Functionality/Auto-attendant-and-IVR-Menus/Creating-a-Simple-IVR-Menu/
  30. Asterisk project, asterisk-extra-sounds-fr-gsm-current.tar.gz (file list read: contains option-is-invalid and are-you-still-there). Downloaded 26 Oct 2026
    https://downloads.asterisk.org/pub/telephony/sounds/asterisk-extra-sounds-fr-gsm-current.tar.gz
  31. Asterisk documentation, GotoIfTime() application reference (syntax with optional timezone; omitted label continues). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Applications/GotoIfTime/
  32. Asterisk documentation, "Include Statements Basics" (overlapping patterns among included contexts). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Configuration/Dialplan/Include-Statements/Include-Statements-Basics/
  33. Asterisk documentation, TESTTIME() function reference. Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Functions/TESTTIME/
  34. Asterisk source, main/pbx.c (testtime_write: the date format TESTTIME actually parses). master branch, retrieved 26 Oct 2026
    https://raw.githubusercontent.com/asterisk/asterisk/master/main/pbx.c
  35. 5 U.S.C. § 6103, Holidays (legal public holidays and their date rules), via the Legal Information Institute. Retrieved 26 Oct 2026
    https://www.law.cornell.edu/uscode/text/5/6103
  36. Canada Labour Code, R.S.C. 1985, c. L-2, s. 166 (definition of "general holiday"). Retrieved 26 Oct 2026
    https://laws-lois.justice.gc.ca/eng/acts/L-2/section-166.html
  37. Holidays Act, R.S.C. 1985, c. H-5 (Canada Day, Victoria Day). Retrieved 26 Oct 2026
    https://laws-lois.justice.gc.ca/eng/acts/H-5/FullText.html
  38. Canada Revenue Agency, "Public holidays" (2026 dates, including Good Friday 3 April and Thanksgiving 12 October). Retrieved 26 Oct 2026
    https://www.canada.ca/en/revenue-agency/services/tax/public-holidays.html
  39. Asterisk documentation, STRFTIME() function reference. Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Functions/STRFTIME/
  40. Asterisk documentation, "Asterisk Standard Channel Variables" (EPOCH). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Configuration/Dialplan/Variables/Channel-Variables/Asterisk-Standard-Channel-Variables/
  41. Asterisk documentation, DB_EXISTS() function reference. Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Functions/DB_EXISTS/
  42. Asterisk source, main/db.c (CLI usage of database put, get, del, deltree, show). master branch, retrieved 26 Oct 2026
    https://raw.githubusercontent.com/asterisk/asterisk/master/main/db.c
  43. Asterisk documentation, Dial() application reference (parallel dialling, default timeout, DIALSTATUS values, options f, g, m, r, U and GOSUB_RESULT). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Applications/Dial/
  44. Asterisk source, apps/app_dial.c (connected line copied to each new leg; f option; legs hung up before the U subroutine runs; DIALSTATUS after GOSUB_RESULT). master branch, retrieved 26 Oct 2026
    https://raw.githubusercontent.com/asterisk/asterisk/master/apps/app_dial.c
  45. Asterisk documentation, "Dialing PJSIP Channels" (endpoint, PJSIP_DIAL_CONTACTS, number@trunk). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Configuration/Channel-Drivers/SIP/Configuring-res_pjsip/Dialing-PJSIP-Channels/
  46. Asterisk documentation, Read() application reference (maxdigits, attempts, timeout). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Applications/Read/
  47. Asterisk documentation, SayDigits() application reference. Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Applications/SayDigits/
  48. Asterisk documentation, Set() application reference (variable inheritance with _ and __). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Applications/Set/
  49. Asterisk documentation, VoiceMail() application reference (options b, u, s, U; keys 0 and *; VMSTATUS). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Applications/VoiceMail/
  50. Asterisk documentation, VoiceMailMain() application reference. Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Applications/VoiceMailMain/
  51. Asterisk documentation, "Voicemail" overview (warning on the s option). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Configuration/Applications/Voicemail/
  52. Asterisk source, configs/samples/voicemail.conf.sample (format, attach, attachfmt, maxmsg, minsecs, operator, delete, emailsubject, emailbody, mailcmd, zonemessages, mailbox line format). master branch, retrieved 26 Oct 2026
    https://raw.githubusercontent.com/asterisk/asterisk/master/configs/samples/voicemail.conf.sample
  53. Asterisk source, apps/app_voicemail.c (SENDMAIL default and how mailcmd is launched). master branch, retrieved 26 Oct 2026
    https://raw.githubusercontent.com/asterisk/asterisk/master/apps/app_voicemail.c
  54. Asterisk documentation, "Asterisk Audio and Video Capabilities" (format table, wav and wav49, signed linear rates, sox). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Operation/Asterisk-Audio-and-Video-Capabilities/
  55. Asterisk documentation, "Directory and File Structure" (astdatadir and the sounds directory). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Fundamentals/Directory-and-File-Structure/
  56. Asterisk documentation, "Sound Prompt Searching based on Channel Language". Retrieved 26 Oct 2026
    https://docs.asterisk.org/Deployment/Internationalization-and-Localization/Sound-Prompt-Searching-based-on-Channel-Language/
  57. Asterisk source, configs/samples/asterisk.conf.sample (defaultlanguage = en). master branch, retrieved 26 Oct 2026
    https://raw.githubusercontent.com/asterisk/asterisk/master/configs/samples/asterisk.conf.sample
  58. Asterisk documentation, "Asterisk Sounds Packages" (least CPU-intensive format chosen automatically). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Deployment/Internationalization-and-Localization/Asterisk-Sounds-Packages/
  59. Asterisk project downloads, sounds directory (core and extra French packages in alaw, g722, g729, gsm, siren7, siren14, sln16, ulaw and wav). Retrieved 26 Oct 2026
    https://downloads.asterisk.org/pub/telephony/sounds/
  60. Asterisk project, asterisk-core-sounds-fr-gsm-current.tar.gz, version 1.6.1 (CREDITS file and core-sounds-fr.txt prompt texts). Downloaded 26 Oct 2026
    https://downloads.asterisk.org/pub/telephony/sounds/asterisk-core-sounds-fr-gsm-current.tar.gz
  61. Asterisk source, sounds/sounds.xml (menuselect sound package options and which are enabled by default). master branch, retrieved 26 Oct 2026
    https://raw.githubusercontent.com/asterisk/asterisk/master/sounds/sounds.xml
  62. Asterisk documentation, "Voice Prompts and Music on Hold License". Retrieved 26 Oct 2026
    https://docs.asterisk.org/About-the-Project/License-Information/Voice-Prompts-and-Music-on-Hold-License/
  63. SoX manual page, sox(1): -r, -c, -b, -e and -t options (Linux man-page mirror; the project site's HTML manual returned 404 today). Retrieved 26 Oct 2026
    https://linux.die.net/man/1/sox
  64. Asterisk documentation, Record() application reference (format, silence, maxduration, options, RECORD_STATUS). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Applications/Record/
  65. Asterisk documentation, "Record Application" (tutorial: recording a custom menu prompt, and a day menu with a * extension). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Deployment/Basic-PBX-Functionality/Auto-attendant-and-IVR-Menus/Record-Application/
  66. Asterisk source, apps/app_record.c (create_destination_directory: relative paths are created under sounds/). master branch, retrieved 26 Oct 2026
    https://raw.githubusercontent.com/asterisk/asterisk/master/apps/app_record.c
  67. Asterisk documentation, VMAuthenticate() application reference. Retrieved 26 Oct 2026
    https://docs.asterisk.org/Latest_API/API_Documentation/Dialplan_Applications/VMAuthenticate/
  68. Asterisk documentation, "Gosub" (subroutines overview: replaces Macro, arguments as ARG1..ARGn, Return). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Configuration/Dialplan/Subroutines/Gosub/
  69. Asterisk documentation, "Running Asterisk" (remote console with -r, verbosity with -rvvv). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Operation/Running-Asterisk/
  70. Asterisk documentation, "Basic Logging Commands" (core set verbose, core set debug). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Operation/Logging/Basic-Logging-Commands/
  71. Asterisk documentation, "Making a Phone Call" (verbosity three or higher; dialplan show for troubleshooting). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Deployment/Basic-PBX-Functionality/Making-a-Phone-Call/
  72. Asterisk documentation, "Creating Dialplan Extensions" (dialplan reload). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Deployment/Basic-PBX-Functionality/Creating-Dialplan-Extensions/
  73. Asterisk documentation, "CLI Syntax and Help Commands" (core show application, voicemail show users). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Operation/Asterisk-Command-Line-Interface/CLI-Syntax-and-Help-Commands/
  74. Asterisk documentation, "Asterisk PJSIP Troubleshooting Guide" (pjsip set logger on). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Configuration/Channel-Drivers/SIP/Configuring-res_pjsip/Asterisk-PJSIP-Troubleshooting-Guide/
  75. Asterisk documentation, "Creating and Manipulating Channels from the CLI" (channel originate, 30-second timeout). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Operation/Asterisk-Command-Line-Interface/Creating-and-Manipulating-Channels-from-the-CLI/
  76. Asterisk documentation, "Logging Configuration" (logger.conf levels, including dtmf). Retrieved 26 Oct 2026
    https://docs.asterisk.org/Configuration/Core-Configuration/Logging-Configuration/