Dial Plans
Build visual call routing flows with dial plans.
Overview
A dial plan defines how incoming calls are routed through a series of steps, represented as a flowchart. Each step in the flowchart is called a node. Nodes check conditions (like business hours), ring people, play audio, or hand the call to an application, then route to the next step based on the outcome.
Think of it like a flowchart you'd draw on a whiteboard:
┌───────────┐
│ Start │
└─────┬─────┘
│
▼
┌─────────────┐ open ┌───────────────┐
│ Check Hours │────────────▶│ Reception │
│ [Schedule] │ │ [InternalDial]│
└──────┬──────┘ └───────┬───────┘
│ │
│ closed timeout
│ │
└───────────┬────────────────┘
│
▼
┌───────────┐
│ Voicemail │
└───────────┘
In this example:
- Every call starts at the "Check Hours" schedule node
- If the schedule is open, the call rings reception
- If reception doesn't answer (timeout), or if the schedule is closed, the call routes to voicemail
Creating a Dial Plan
- SDK
- cURL
const dialPlan = await dialstack.dialPlans.create(
{
name: 'Main Line Routing',
entry_node: 'check_hours',
nodes: [
{
id: 'check_hours',
type: 'schedule',
config: {
schedule: 'sched_01h2xcejqtf2nbrexx3vqjhp50',
open: 'reception',
closed: 'voicemail',
},
},
{
id: 'reception',
type: 'internal_dial',
config: {
target: 'user_01h2xcejqtf2nbrexx3vqjhp45',
timeout: 30,
next: 'voicemail',
},
},
{
id: 'voicemail',
type: 'internal_dial',
config: {
target: 'user_01h2xcejqtf2nbrexx3vqjhp45',
timeout: 0, // straight to the user's voicemail
},
},
],
},
{ dialstackAccount: 'acct_01h2xcejqtf2nbrexx3vqjhp41' }
);
curl -X POST https://api.dialstack.ai/v1/dialplans \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "DialStack-Account: acct_01h2xcejqtf2nbrexx3vqjhp41" \
-H "Content-Type: application/json" \
-d '{
"name": "Main Line Routing",
"entry_node": "check_hours",
"nodes": [
{
"id": "check_hours",
"type": "schedule",
"config": {
"schedule": "sched_01h2xcejqtf2nbrexx3vqjhp50",
"open": "reception",
"closed": "voicemail"
}
},
{
"id": "reception",
"type": "internal_dial",
"config": {
"target": "user_01h2xcejqtf2nbrexx3vqjhp45",
"timeout": 30,
"next": "voicemail"
}
},
{
"id": "voicemail",
"type": "internal_dial",
"config": {
"target": "user_01h2xcejqtf2nbrexx3vqjhp45",
"timeout": 0
}
}
]
}'
Key Concepts
Entry Node
The entry_node specifies which node executes first. Think of it as where the "Start" arrow points in your flowchart.
Exit Targets
Each node's exits are fields in its config that name the next node (open/closed on a schedule node, next on most others). Omit an exit to end the call at that point, unless the node type says otherwise below.
Node IDs
Node IDs are strings you define (like "check_hours" or "reception"). They must be unique within a dial plan and are used to wire nodes together.
Visual Position
Nodes can include optional position coordinates (x, y) for rendering in a visual editor. If omitted, nodes are auto-positioned.
Drafts
A dial plan can be saved before it is finished: the ID a node references (a schedule, target, clip, phone number or voice app) may be left empty. At call time, a node with an empty reference is skipped to its next exit, or ends the call when it has none. A menu with no prompt clip waits for a digit without playing anything.
Node Types
Every node has an id, a type, an optional position, and a config whose shape depends on the type. The full schemas are in the API reference.
| Type | What it does | Exits |
|---|---|---|
schedule | Branches on whether a schedule is open | open, closed, holiday |
internal_dial | Rings a user, ring group, call queue, or nested plan | next |
ring_all_users | Rings every user in the account at once | next |
external_dial | Dials an outside phone number | next |
menu | Plays a prompt and routes on the digit the caller presses | per-digit next_node, timeout_next_node, invalid_next_node |
audio_clip | Plays an audio clip | next |
voice_app | Hands the call to, or notifies, a voice app | next (notify mode) |
hang_up | Ends the call | none |
schedule
Routes the call based on whether a schedule is currently open, closed, or on a holiday.
| Field | Type | Description |
|---|---|---|
schedule | string | ID of the schedule to check |
open | string | Node to route to while the schedule is open |
closed | string | Node to route to while it is closed. Holidays route here too unless holiday is set |
holiday | string | Optional. Node to route to on a holiday, when you want holidays handled apart from closed hours |
internal_dial
Rings an internal target. If the call is answered, the dial plan ends; on no answer or busy, the call follows next.
| Field | Type | Description |
|---|---|---|
target | string | ID of a user, ring group, call queue, nested dial plan, or shared voicemail box |
timeout | integer | Required. Ring time in seconds, 0–300. 0 skips the target and continues at next |
timeout_override | boolean | Makes timeout govern a target that has its own timing (a ring group, a call queue, a user with Find Me / Follow Me). Defaults to false. Can't be combined with timeout: 0 |
next | string | Node to route to on no answer or busy |
The target decides how some of these behave:
- User: with no
next, an unanswered or skipped call goes to the user's voicemail. - Ring group: with no
next, an unanswered call runs the group's own timeout action; a skipped one (timeout: 0) ends. - Shared voicemail box: the call goes straight to voicemail and the plan ends;
timeoutandnextare not used. - Call queue:
nextis the queue's timeout exit, taken when the caller's maximum wait elapses or the queue refuses them. Leave it unset to run the queue's own timeout action instead; a skipped queue (timeout: 0) with nonextends the call. - Nested dial plan: the nested plan takes over the call;
timeout,timeout_overrideandnextare not used.
target replaces the older target_id key, which is still accepted. A voice app is also still accepted as a target, but new plans should use the voice_app node instead.
ring_all_users
Rings every user in the account at the same time and connects the first to answer.
| Field | Type | Description |
|---|---|---|
timeout | integer | Required. Ring time in seconds, 1–300 |
next | string | Node to route to when nobody answers. With none, the call goes to the voicemail of the user with the lowest extension |
external_dial
Dials an outside phone number. If the call is answered, the dial plan ends.
| Field | Type | Description |
|---|---|---|
phone_number | string | The number to dial, in E.164 format |
timeout | integer | Required. Ring time in seconds, 1–120 |
next | string | Node to route to on no answer or busy |
menu
Plays a prompt (an auto attendant, or IVR) and routes on the digit the caller presses.
| Field | Type | Description |
|---|---|---|
prompt_clip | string | ID of the audio clip to play |
timeout | integer | Required. Seconds to wait for a digit after the prompt, 1–30 |
options | array | Up to 12 entries, each with a digit (0–9, * or #), the next_node to route to, and an optional label for the editor |
timeout_next_node | string | Node to route to when no digit is pressed |
invalid_next_node | string | Node to route to when a digit with no option is pressed |
extension_entry_enabled | boolean | Lets callers dial any extension in the account from this menu, as well as the options. Defaults to false |
Unlike other nodes, a menu exit left empty replays the prompt rather than ending the call (or ends it, when the menu has no prompt clip). Enabling extension_entry_enabled makes every extension in the account reachable by anyone who can reach this dial plan.
audio_clip
Plays an audio clip, then routes to the next node.
| Field | Type | Description |
|---|---|---|
clip | string | ID of the audio clip to play |
next | string | Node to route to after the clip |
The older type name sound_clip is still accepted with the same config.
voice_app
Hands the call to a voice app, or notifies one as the call passes through.
| Field | Type | Description |
|---|---|---|
voice_app | string | ID of the voice app |
mode | string | control (default): the voice app receives call.received and takes over the call; the plan ends here. notify: the voice app receives call.notify and the call continues to next |
next | string | In notify mode, the node the call continues to |
notify mode can't reference the voice app an AI agent manages: saving the plan returns 422.
hang_up
Ends the call. Its config is an empty object, {}.
Examples
After-Hours Voicemail
Route calls to voicemail when closed:
Start → Check Hours [Schedule] → (open) → Sales [Internal Dial → ring group]
→ (closed/holiday) → Voicemail
{
"name": "After-Hours Voicemail",
"entry_node": "check_hours",
"nodes": [
{
"id": "check_hours",
"type": "schedule",
"config": {
"schedule": "sched_01h2xcejqtf2nbrexx3vqjhp50",
"open": "sales",
"closed": "voicemail"
}
},
{
"id": "sales",
"type": "internal_dial",
"config": {
"target": "rg_01h2xcejqtf2nbrexx3vqjhp51",
"timeout": 30,
"next": "voicemail"
}
},
{
"id": "voicemail",
"type": "internal_dial",
"config": {
"target": "svm_01h2xcejqtf2nbrexx3vqjhp53",
"timeout": 0
}
}
]
}
Multi-Level Routing
Chain schedule checks, here an on-call line ahead of business hours:
Start → Check On-Call [Schedule] → (open) → On-Call [Internal Dial]
→ (closed/holiday) → Check Hours [Schedule] → (open) → Reception [Internal Dial]
→ (closed/holiday) → Voicemail
{
"name": "Multi-Level Routing",
"entry_node": "check_on_call",
"nodes": [
{
"id": "check_on_call",
"type": "schedule",
"config": { "schedule": "ON_CALL_SCHEDULE_ID", "open": "on_call", "closed": "check_hours" }
},
{
"id": "on_call",
"type": "internal_dial",
"config": { "target": "ON_CALL_USER_ID", "timeout": 30, "next": "voicemail" }
},
{
"id": "check_hours",
"type": "schedule",
"config": {
"schedule": "BUSINESS_HOURS_SCHEDULE_ID",
"open": "reception",
"closed": "voicemail"
}
},
{
"id": "reception",
"type": "internal_dial",
"config": { "target": "RECEPTION_USER_ID", "timeout": 30, "next": "voicemail" }
},
{
"id": "voicemail",
"type": "internal_dial",
"config": { "target": "SHARED_VOICEMAIL_BOX_ID", "timeout": 0 }
}
]
}
Main Menu
Greet the caller and let them choose:
Start → Main Menu [Menu] → (1) → Sales [Internal Dial]
→ (2) → Support [Internal Dial → call queue]
→ (no input) → Goodbye [Audio Clip] → Hang Up
{
"name": "Main Menu",
"entry_node": "main_menu",
"nodes": [
{
"id": "main_menu",
"type": "menu",
"config": {
"prompt_clip": "aud_01h2xcejqtf2nbrexx3vqjhp54",
"timeout": 5,
"options": [
{ "digit": "1", "next_node": "sales", "label": "Sales" },
{ "digit": "2", "next_node": "support", "label": "Support" }
],
"timeout_next_node": "goodbye"
}
},
{
"id": "sales",
"type": "internal_dial",
"config": { "target": "rg_01h2xcejqtf2nbrexx3vqjhp51", "timeout": 30 }
},
{
"id": "support",
"type": "internal_dial",
"config": { "target": "qu_01h2xcejqtf2nbrexx3vqjhp61", "timeout": 30 }
},
{
"id": "goodbye",
"type": "audio_clip",
"config": { "clip": "aud_01h2xcejqtf2nbrexx3vqjhp55", "next": "end" }
},
{ "id": "end", "type": "hang_up", "config": {} }
]
}
Fallback Chain
Try multiple users, then a cell phone, before voicemail:
Start → Reception [Internal Dial] → (timeout 30s) → Manager [Internal Dial] → (timeout 20s) → Cell [External Dial] → Voicemail
{
"name": "Fallback Chain",
"entry_node": "reception",
"nodes": [
{
"id": "reception",
"type": "internal_dial",
"config": {
"target": "user_01h2xcejqtf2nbrexx3vqjhp45",
"timeout": 30,
"next": "manager"
}
},
{
"id": "manager",
"type": "internal_dial",
"config": {
"target": "user_01h2xcejqtf2nbrexx3vqjhp46",
"timeout": 20,
"next": "cell"
}
},
{
"id": "cell",
"type": "external_dial",
"config": {
"phone_number": "+14155551234",
"timeout": 20,
"next": "voicemail"
}
},
{
"id": "voicemail",
"type": "internal_dial",
"config": {
"target": "user_01h2xcejqtf2nbrexx3vqjhp45",
"timeout": 0
}
}
]
}
Transcribe, then route
Notify a voice app so it can listen to the call, then ring everyone:
{
"name": "Transcribed Main Line",
"entry_node": "transcribe",
"nodes": [
{
"id": "transcribe",
"type": "voice_app",
"config": {
"voice_app": "va_01h2xcejqtf2nbrexx3vqjhp49",
"mode": "notify",
"next": "everyone"
}
},
{
"id": "everyone",
"type": "ring_all_users",
"config": { "timeout": 24 }
}
]
}
API Reference
- Dial Plans — Create, update, and delete dial plans
- Schedules — Define business hours for schedule nodes