Skip to main content

Dialplan overview

One Extension document is one dialplan: an ordered list of steps that runs top to bottom. metadata.name is the number it answers on.

configVersion: lyno/v1
kind: Extension
metadata:
name: "200"
tenant: acme
spec:
description: Support follow-me
steps:
- id: greeting
type: play
file: ../sounds/welcome.wav
when:
days: [mon, tue, wed, thu, fri]
hours: "09:00-17:30"

- id: follow-me
type: dial
moh: default
target:
kind: followme
stages:
- targets: ["101"]
timeout: 15s
- targets: ["100", "external:0612345678"]
timeout: 25s

- id: nobody-home
type: play
file: ../sounds/closed.wav

- id: done
type: hangup
FieldRequiredMeaning
metadata.nameyesThe number this plan answers for.
metadata.tenantyesThe tenant it belongs to.
spec.descriptionnoLabel shown in the editor and in logs.
spec.stepsyesOrdered steps. At least one is required.

Steps stay a list inside the document rather than becoming documents of their own: they are a sequence by nature, and one plan is one thing to read.

The execution rules

These six rules explain every dialplan you will ever write:

  1. Steps run top to bottom.
  2. A step whose time condition does not match is skipped, and execution continues with the next one.
  3. A dial that nobody answers continues to the next step. This is the whole reason "greet, then follow-me, then announce we are closed" works. A queue nobody answered in time, and an ivr nobody made a choice on, behave the same way — that is what makes "queue, then voicemail" free.
  4. A dial that is answered bridges the legs and ends the plan.
  5. A failing step does not kill the plan. The error is logged and execution continues, so a missing sound file still lets the follow-me behind it run.
  6. A plan that runs off the end hangs the caller up. An explicit hangup is only needed to end a plan early, or to make the intent obvious.

Execution also stops when a hangup step runs, or when the caller hangs up.

A goto, and an ivr option pointing at an extension, hand the call to another plan and do not come back. Loops are refused at startup, and a call may enter at most 20 plans in total — which is the backstop for the choices a caller makes at a menu, since those cannot be checked in advance.

Time is sampled once

The clock is read once, when the plan starts. Every time condition in that run is evaluated against the same instant, so a call that begins at 17:29:59 does not change behaviour halfway through.

Step types

TypeFieldsBehaviour
playfileAnswers the call if needed and plays a WAV file.
dialtarget, timeout, mohRings a destination and bridges the first answer.
mohmohPlays music on hold until the caller hangs up.
hangupEnds the call.
ivrmenuPlays one of the tenant's menus and follows the key pressed.
gotoextensionHands the call to another plan of the same tenant.
queuequeue, timeout, mohParks the caller in a queue.
recordaction, confirmArms or disarms recording for this call.
voicemailmailbox, greetingPlays a greeting and takes a message.
voicemail_checkmailboxAsks for a PIN and plays the mailbox back.
dndaction, confirmSets do-not-disturb for the endpoint that dialled.
forwardaction, mode, collect, prompt, confirmSets or clears forwarding for the endpoint that dialled.

Every step also accepts an optional id (used in logs) and an optional when time condition. Full details in Step types.

dnd and forward are why a feature code is an ordinary extension: nothing restricts metadata.name to digits, so a plan can answer on *78.

Common shapes

A simple extension

kind: Extension
metadata: { name: "100", tenant: acme }
spec:
description: Alice
steps:
- id: ring
type: dial
timeout: 30s
target: {kind: endpoint, endpoint: "100"}

Office hours, with an out-of-hours announcement

kind: Extension
metadata: { name: "200", tenant: acme }
spec:
description: Reception
steps:
- id: open
type: dial
timeout: 25s
when:
days: [mon, tue, wed, thu, fri]
hours: "09:00-17:00"
target: {kind: ring_group, group: reception}

- id: closed
type: play
file: ../sounds/closed.wav

- id: done
type: hangup

Out of hours the dial is skipped and the announcement plays. During office hours the announcement is only reached when nobody picked up — the fall-through rule doing the work.

Park the caller on hold

kind: Extension
metadata: { name: "700", tenant: acme }
spec:
description: Parking
steps:
- id: hold
type: moh
moh: jazz

moh never returns while the call is up, so it is effectively terminal.

A menu, then a queue, then a mailbox

kind: Extension
metadata: { name: "600", tenant: acme }
spec:
description: Main line
steps:
- {id: menu, type: ivr, menu: main} # falls through if nobody chooses
- {id: wait, type: queue, queue: support} # falls through when maxWait runs out
- {id: box, type: voicemail, mailbox: "100"}

Three steps, no special cases: each falls through when it does not resolve the call, exactly like a dial nobody answered.

Where the numbers come from

An Extension document is reached in three ways:

  • an inbound route whose destination is this extension;
  • a phone in the same tenant dialling the number;
  • another extension's dial step targeting it.

An extension does not need a matching account. Extension 200 above is an entry point with no phone behind it. Conversely, a dial target of kind endpoint must be an account extension — that is what validation checks.

When a plan is named after an account extension, the plan wins: a phone dialling 100 runs the Extension document called 100 rather than ringing the account directly. That is how an extension gets "ring, then voicemail" for internal callers as well as external ones — and it is worth knowing before you name a service plan after a number somebody already has.