Skip to main content
One IVR flow, two doors in. The visual canvas in the dashboard (Voice → IVR Builder) and the Flow DSL over the API write the same { nodes, edges } graph, run the same validator, and publish the same live snapshot. This walkthrough puts the two side by side, then walks the full round trip — export, import, simulate, validate, roll back — so you can pick the right surface per change and switch mid-flight without re-authoring. To keep it concrete, one bookings-flow example threads through every section: a two-node flow that greets, offers a Menu with key 1 → Bookings, and queues into the bookings queue. Create it on either surface — both produce the same serialized definition:

1. Authoring surfaces compared

The DSL is the JSON graph above, submitted to POST /api/v1/voice/ivr-flows and updated with PUT /api/v1/voice/ivr-flows/:id. Each entry in nodes carries an id, a type (ivrStart, playAudio, menu, speechInput, queue, transfer, voicemail, hangup), and an optional data block; each edge in edges links a source to a target, with a sourceHandle naming the branch ("1", "default", an intent name). The full node catalog, per-type data payloads, and validator codes are in Build and ship your first IVR flow. On the canvas you drag nodes from the left palette, drag edges from a source handle to a target node, and edit each node’s data block in the right-hand config panel. A Menu node exposes one handle per option key plus default; a Speech Input node exposes one per intent plus nomatch. The canvas never stores a different shape — what you see is serialized to the same graph and sent to the same endpoints. Side by side, the same two-node branch: Save stays a draft until you publish — nothing a caller can reach. Publish promotes the draft to the live snapshot, and Assign to Number from the toolbar (or the number’s Routing tab) attaches the flow to a DID. Design IVR flows on the visual builder is the full canvas reference.

2. When to choose the canvas vs the DSL

Both surfaces are tenant-owned authoring choices — pick per change, not per team.
  • Choose the canvas when routing reads better as a diagram: a multi-level menu, a branch a teammate should review at a glance, the speech-grammar editor for a Speech Input node, or per-node funnel analytics. It is also the only place with a guided validator panel, an in-product simulator, and one-click rollback.
  • Choose the DSL when the definition belongs in a repo: flows reviewed in pull requests, templates rendered across many tenants or brands, a CI gate that PUTs a candidate and treats 422 as a failed check, or an infrastructure-as-code pipeline.
Teams mix both per flow over its lifetime. Because the graph is the contract either way, keep the DSL as the source of truth in version control and use the canvas for review and iteration.

3. Round-trip: canvas → DSL

Since the canvas serializes every save through the same API, the DSL copy of a flow is one GET away. Export the bookings flow’s definition after any canvas save:
The response carries the draft plus the live snapshot the callers currently reach:
Write data.definition (or data.published_definition for the live copy) to ivr/bookings.json and check it in — that is the versioned DSL the rest of this walkthrough operates on.

4. Round-trip: DSL → canvas

Importing is the same route in reverse. PUT the exported definition back, then reopen the flow on the canvas — the builder fetches the draft over the same GET and renders the same tree:
Shape the file of the graph, not the whole GET response:
Every create and update runs the graph validator before persistence, and this is the one case where the round trip differs by direction: canvas saves avoid malformed graphs client-side, while a hand-edited DSL can always drift. If the PUT returns 422 IVR_FLOW_GRAPH_INVALID, nothing was stored — fix the details.errors list and retry. Valid graphs reopen on the canvas with the exact nodes, handles, and branch labels from the file.

5. Simulate from both doors

The canvas Simulate dialog and POST /api/v1/voice/ivr-flows/:id/simulate walk the same graph the same way. Take each dialog turn — digits, speech, or an intent — and replay it with curl:
The walk reports the reached-node path, the handle it took at each branch, the verb categories emitted (say / play / gather / dial / hangup — labels describing what the live call would emit), and a termination reason:
Optional body fields mirror the dialog’s other knobs: source: "published" walks the live snapshot instead of the draft; caller_language answers language-branch routing; business_hours_open answers a time-check node; branch_handles mocks data-dip and queue-overflow decisions; context seeds call context; definition simulates an unsaved graph straight from a file. Every field is optional. Run three walks per change — happy path, an unmatched option (falls through default), and silence (falls through timeout) — before the change can ring a phone. In CI, treat an unexpected path or termination_reason as a failed check.

6. Validation parity

One validator guards both doors: every canvas Validate click, every publish, and every create/update over the API runs the same structural check. Where the parity ends is in presentation — the panel, the one-click focus, and the red edge highlight are canvas-only; the API returns the identical issue list as plain objects: So a pre-commit hook and a dashboard operator reject — or accept — the same candidate. An editor who dismisses the canvas panel sees it stay dismissed until a different validator response arrives; an API caller just diffs the errors list.

7. Editor behaviors on the canvas

Small behaviors that change how you work, canvas-only by nature:
  • Snap-to-tree. Dropped nodes land at the current insert point, Auto-layout re-sorts the tree into a readable vertical flow, and Fit view re-frames a long canvas.
  • Hover hints. Palette entries carry a one-line description (Entry point, Collect digits, Press 1, 2, 3...); toolbar buttons explain Validate, Auto-layout, Fit view, History, and the unsaved-changes indicator.
  • Selection safety. Delete/Backspace removes the selected node, but the keys are suspended while autosave is in an error state so a failed save never costs you a node.

8. Rollback parity

Every publish appends a version. The canvas History button and the versions API list the same trail:
Confirm a revert on the canvas, or POST the same revert over the API — either way the revert itself is a publish: the prior snapshot becomes the new head and the attached DID keeps answering on the reverted definition, with no manual re-edit and no re-attach:
The response returns the post-revert snapshot plus reverted_from_version, and the revert lands as a first-class history row at the new head.

9. Mixed-team workflow example

Support edits the holiday branch on the canvas while the pipeline owns the template:
  1. Canvas side. Support drags in a playAudio node for the holiday greeting, rewires menu → "1" to it, and saves. The export from section 3 freezes the change into the repo:
  2. CI side. The pipeline re-renders the same flow from its DSL template for tenant B — same bookings tree, a different queue name — and PUTs it to tenant B’s flow id. Support’s canvas review and the template rollout move through the same contract, so a reviewer reads the graph diff, not a screenshot, and either side can be the source of the next change.
Pick one source of truth per flow — a template, a repo file, or the dashboard — and route every change through it. The graph is the contract; the doors are just editors.

See also