Call Flows
A call flow is how you program what happens on a call. It’s a versioned JSON node-graph: the same document drives a React-Flow visual builder and the runtime interpreter. If you’ve used Twilio Studio, the model will feel familiar; the difference is that every live call runs inside its own stateful edge object.
The shape of a flow
Section titled “The shape of a flow”A flow is a small JSON document:
{ "id": "flow_01jzqwjn803wtnhc445bvjenzc", "name": "Support line", "version": 3, "start": "welcome", "nodes": [ /* FlowNode[] */ ], "edges": [ /* FlowEdge[] */ ]}nodesare actions. Each has anid, atype, an editor-onlyposition, and typeddata(discriminated bykind).edgesconnect nodes. Each has asource, atarget, and, crucially, an optionalsourceHandlethat names the branch outcome it represents ("digit:1","timeout","answered","in_hours", …).startis the id of the entry node.
// a node{ "id": "welcome", "type": "menu", "data": { "kind": "menu", "promptText": "Press 1 for sales, 2 for support", "maxDigits": 1, "timeoutSec": 5 } }
// an edge: "when the menu returns digit 1, go to salesQ"{ "id": "e3", "source": "welcome", "target": "salesQ", "sourceHandle": "digit:1" }How a flow executes
Section titled “How a flow executes”Each live call walking a flow gets its own CallSessionDO (one stateful edge object per call). The engine runs an poll-based command loop:
/flow start ─▶ CallSessionDO.start(graph) │ emits the first COMMAND (e.g. "play this menu prompt") ▼ the media engine executes it ─▶ /flow step { callId, outcome, data } │ CallSessionDO.resume(outcome) ▼ follows the edge whose sourceHandle == outcome ─▶ next COMMAND ▼ ... until a terminal node (hangup) or a 2-party bridge ...Each node emits a concrete command to the edge (play, gather, dial, enqueue, record, conference, hangup). The edge executes it and calls /flow back with the outcome; the interpreter follows the matching edge and emits the next command. The loop is driven entirely by the brain, and the edge holds no flow logic.
Pure-logic nodes resolve in-object
Section titled “Pure-logic nodes resolve in-object”Nodes that don’t need the edge (condition, timeCondition, httpRequest, setVar) are resolved inside the stateful edge object with no extra round-trip. A timeCondition evaluates your schedule and picks an edge; an httpRequest calls your API and branches on the result; a condition evaluates an expression. Only nodes that touch the caller (prompts, menus, dial) cost an edge round-trip.
Edges and outcomes
Section titled “Edges and outcomes”The interpreter selects the next node by matching the returned outcome against each outgoing edge’s sourceHandle:
- A node with multiple outcomes (a
menureturnsdigit:1,digit:2,timeout,invalid) has one edge per handle. - If no edge matches the outcome, the engine follows the default edge (the one with no
sourceHandle). - Special fallback: a
holidayoutcome with noholidayedge falls back to theout_of_hoursedge. This lets you treat holidays and after-hours the same with a single edge, or split them when you need to.
Bridges don’t “advance”
Section titled “Bridges don’t “advance””When a dial, ringGroup, forward, or transfer node reports answered and you’ve drawn no explicit answered edge, the engine emits a no-op: the call is now a 2-party bridge and simply lives until someone hangs up. You only draw an answered edge if you want to do something after the bridge ends.
The node catalog (at a glance)
Section titled “The node catalog (at a glance)”| Category | Nodes |
|---|---|
| Entry | entry |
| Play / speak | play (audio), say (TTS) |
| Gather input | menu (IVR/DTMF), collect (capture digits → variable) |
| Connect | dial, ringGroup, enqueue (queue), forward, transfer, conference |
| Messaging | voicemail |
| Logic | condition, timeCondition, httpRequest, setVar |
| Contact-center | callback, lastCalledUser |
| Terminal | hangup |
See the full Flow Node Reference for every node’s data fields and outcomes.
A worked example
Section titled “A worked example”Business hours → IVR → queue, with after-hours voicemail and an invalid-input fallback:
{ "id": "flow_01jzqwjn803wtnhc445bvjenzc", "name": "Support line", "version": 1, "start": "hours", "nodes": [ { "id": "hours", "type": "timeCondition", "data": { "kind": "timeCondition", "scheduleId": "sch_01j0jvpe0wzc6g05bktfew0xyx" } }, { "id": "welcome", "type": "menu", "data": { "kind": "menu", "promptText": "Press 1 for sales, 2 for support", "maxDigits": 1, "timeoutSec": 5, "maxFailures": 2 } }, { "id": "salesQ", "type": "enqueue", "data": { "kind": "enqueue", "queueId": "q_01jx67hmyhnks8yq7m76g5qrrj" } }, { "id": "supQ", "type": "enqueue", "data": { "kind": "enqueue", "queueId": "q_01j160r23gw5zkpfjme3fwzs5k" } }, { "id": "vm", "type": "voicemail", "data": { "kind": "voicemail", "mailboxId": "vm_01jg8asp59ps9rtt7azhrhvcre" } }, { "id": "bye", "type": "hangup", "data": { "kind": "hangup" } } ], "edges": [ { "id": "e1", "source": "hours", "target": "welcome", "sourceHandle": "in_hours" }, { "id": "e2", "source": "hours", "target": "vm", "sourceHandle": "out_of_hours" }, { "id": "e3", "source": "welcome", "target": "salesQ", "sourceHandle": "digit:1" }, { "id": "e4", "source": "welcome", "target": "supQ", "sourceHandle": "digit:2" }, { "id": "e5", "source": "welcome", "target": "vm", "sourceHandle": "timeout" }, { "id": "e6", "source": "welcome", "target": "bye", "sourceHandle": "invalid" } ]}Variables
Section titled “Variables”A flow carries a vars bag through the call. Sources include:
- the route target’s
dest_xform(small JSON applied on the hop into the flow), - the inbound caller-ID, exposed as
caller_cid, setVarnodes,collectnodes (which save captured digits to a named variable),httpRequestresponses (saved viasaveAs).
condition nodes branch on these; prompts and downstream nodes can reference them.
Synthesized flows for direct destinations
Section titled “Synthesized flows for direct destinations”You don’t have to build a flow for simple routing. If a DID or extension points directly at a ring_group, queue, voicemail, conference, or forward, the platform synthesizes a one-node flow on the fly and runs it through the same interpreter, so direct routes and full visual flows share one execution path.
Synthesis even chains the destination’s configured exits: a DID pointed straight at a queue whose timeout_dest is a voicemail becomes a real multi-node graph (enqueue → on-timeout → voicemail) automatically, up to a small depth cap. See Numbers & Extensions and Overflow & Exit Destinations.
Versioning & publishing
Section titled “Versioning & publishing”A call_flow row has a version and an is_published flag. The routing brain only loads published flows, and the version that ran is recorded on the call’s CDR, so you can change a flow safely and know exactly which version handled any given call. Bump the version on publish; in-flight calls keep running the version they started on.
Limitations & roadmap
Section titled “Limitations & roadmap”- The
saynode (free-text TTS) requires a TTS provider for arbitrary text; today the platform ships a fixed, pre-generated multilingual prompt catalog. Free-text synthesis at call time is an open item. callbackandlastCalledUserare present in the model but currently fall through to their default edge (sticky-agent and callback-lead behaviors are designed, not yet wired).- Splicing a nested published sub-flow from a synthesized exit is a roadmap item.
Next: the Flow Node Reference.