runFlowstableRun sub-flow
Calls one of your other saved flows like a subroutine. Fields you pass under Arguments become {{config.x}} inside that flow. When the sub-flow ends, this…
What it does
Calls one of your other saved flows like a subroutine. Pick the flow by
slug, fill in the Arguments form (auto-generated from the picked
flow's declared parameters on its start node), and the worker swaps
into the sub-flow at its start node. Argument values are rendered as
templates against the current call's vars and event, then merged
into the sub-flow's bindingConfig so the sub-flow body can read them
as {{config.<key>}}. Anything you don't pass falls back to the
parameter's default, and the sub-flow's start-node parameters declare
the typed shape — string, number, boolean, enum, audioUrl,
queue, extension, or flow.
The sub-flow shares the same vars bag, the same call legs, and the
same recording context as its parent — it isn't a new call, it's a
nested cursor. When the sub-flow ends (or hits an end sentinel), the
parent picks back up at the transition wired off flow.completed.
Recursion is bounded: a _flowStack lives on state.vars and the
runtime caps depth at 8 so a flow that calls itself can't explode
the worker. The onError field controls behavior when something goes
wrong before the sub-flow can run — missing slug, sub-flow not found,
sub-flow has no start node — choosing between continue (follow
flow.completed like a normal exit), endCall (hangup immediately),
and goto (jump to the node id in errorNodeId).
When to use it
- Factor a reusable IVR fragment (after-hours greeting, language router, voicemail prompt) into one shared flow that several bindings invoke
- Build a "language picker" parent flow that calls the same body
flow with a different
languageargument per branch - Compose a queue + voicemail fall-through as two flows so each can be edited independently
- Promote a fragment of a long flow into a sub-flow once it's battle-tested — the parent shrinks to a single node
- Share a single "log this call to CRM" sub-flow across every flow in the org
Configuration
Calls one of your other saved flows like a subroutine. Fields you pass under Arguments become {{config.x}} inside that flow. When the sub-flow ends, this flow picks back up at the next step.
| Field | Label | Type | Required | Default | Notes |
|---|---|---|---|---|---|
flowSlug | Which flow to run | flow | Required | "" | Pick from your saved flows. If you rename or delete the picked flow later, this node will fail to find it. |
args | Arguments | flowArgs | Optional | — | Values passed as the sub-flow's config. Available there as {{config.x}}. Templates resolve against the current call's vars first. The form below is generated from the picked flow's declared parameters. |
onError | On error | select | Optional | continue | Options: continue, endCall, goto. What to do if the sub-flow is missing or has no start node. |
errorNodeId | Error → node id | text | Optional | voicemail | Only used when On error is set to "Go to a specific node". |
Outgoing events: flow.completed
Examples
Simple parameterless call
Drop into a shared after-hours flow when the main flow detects the call is outside business hours.
{
"id": "after-hours",
"type": "runFlow",
"config": {
"flowSlug": "after-hours-greeting",
"args": null,
"onError": "continue"
},
"on": { "flow.completed": "end" }
}
Parameterized sub-flow with a typed string
Invoke the shared greeting flow with a per-binding message. Inside
that flow, {{config.message}} renders the value below.
{
"id": "say-shared-greeting",
"type": "runFlow",
"config": {
"flowSlug": "shared-greeting",
"args": {
"message": "Thanks for calling Acme Sales — this is the gold tier line."
},
"onError": "goto",
"errorNodeId": "fallback-say"
},
"on": { "flow.completed": "ring-queue" }
}
Gotchas
- Slug, not UUID.
flowSlugis a string slug (after-hours-greeting) unique within the org. The worker resolves it at run time with a scoped DB lookup. Renaming or deleting the picked flow doesn't update this node's config — it just starts failing withsub-flow not found, which routes throughonError. - Recursion depth caps at 8.
state.vars._flowStackrecords every nested invocation; once depth hits 8 the runtime takes theonErrorpath with reasonmax sub-flow depth (8) exceeded. A flow that calls itself unconditionally hits this on the 9th hop, not the first. - Sub-flow shares
varsand recording context. Mutations tovars.xinside the sub-flow are visible to the parent after it resumes. Recordings started in a sub-flow are owned by the same call. That's normal — but it does mean a sub-flow can clobber a parent'svarsif it uses generic names. Prefix sub-flow-private vars (vars._myflow_x) when isolation matters. - Args render against the parent's state at the moment of
invocation. The renderer sees
vars,event, andconfigas they exist when the runFlow node fires. Once the sub-flow starts it sees those values frozen as{{config.<key>}}; subsequent parent mutations don't propagate. - Typed parameters are resolved at runFlow entry. The sub-flow's
start-node
parametersarray drives the runFlow inspector form, and the worker callsresolveFlowParameterson the merged{ ...parent.bindingConfig, ...args }candidate as its first step in the runFlow case (invariant 14a–c). Declareddefaults are applied to anything the caller didn't pass, every value is coerced to the declared type, and templated values ({{vars.x}}) pass through coercion unchanged so the runtime can resolve them at use-time. - Required-missing parameters take the
onErrorpath BEFORE the stack frame is pushed. WhenresolveFlowParametersreports anyrequiredkeys still undefined after defaulting, the worker callsfollowError(\missing required parameters: ${missing.join(', ')}`)and breaks — no_flowStackmutation, nostate.flowswap, nobindingConfigchange on the parent. The parent's state is guaranteed untouched, so the chosenonErrorroute (continue/endCall/goto`) is the only path the runtime takes from there. onError: continuere-uses theflow.completededge. That's the normal "sub-flow finished" transition. If you want a distinct error path, setonError: gotoand pointerrorNodeIdat a recovery node — otherwise a missing sub-flow looks identical to a successful one to the wiring.- The picked flow's start node must exist. If the flow row is
found but its
definition.startdoesn't resolve to a node indefinition.nodes, the runtime takesonErrorwith reasonsub-flow has no start node. This usually means a hand-edited definition went out of sync; re-saving from the editor fixes it.
