{ 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 toPOST /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
422as a failed check, or an infrastructure-as-code pipeline.
3. Round-trip: canvas → DSL
Since the canvas serializes every save through the same API, the DSL copy of a flow is oneGET away. Export the bookings flow’s definition after any canvas save:
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:
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 andPOST /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:
say / play / gather / dial / hangup — labels describing what the live call would emit), and a termination reason:
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: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:-
Canvas side. Support drags in a
playAudionode for the holiday greeting, rewiresmenu → "1"to it, and saves. The export from section 3 freezes the change into the repo: -
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.
See also
- Build and ship your first IVR flow — the full DSL/API contract side.
- Design IVR flows on the visual builder — the canvas reference page.
- IVR unattended PCI capture — collect card digits in PCI mode on a DTMF Input node.
- IVR intents — speech menus and per-tenant intent buckets.
- Jambonz verb extensions in Orbit — the platform verb layer your IVR nodes run on, with per-node analytics.