Skip to content
LogicrunFlowstable

Run 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 language argument 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.

FieldLabelTypeRequiredDefaultNotes
flowSlugWhich flow to runflowRequired""Pick from your saved flows. If you rename or delete the picked flow later, this node will fail to find it.
argsArgumentsflowArgsOptionalValues 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.
onErrorOn errorselectOptionalcontinueOptions: continue, endCall, goto. What to do if the sub-flow is missing or has no start node.
errorNodeIdError → node idtextOptionalvoicemailOnly 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. flowSlug is 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 with sub-flow not found, which routes through onError.
  • Recursion depth caps at 8. state.vars._flowStack records every nested invocation; once depth hits 8 the runtime takes the onError path with reason max sub-flow depth (8) exceeded. A flow that calls itself unconditionally hits this on the 9th hop, not the first.
  • Sub-flow shares vars and recording context. Mutations to vars.x inside 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's vars if 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, and config as 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 parameters array drives the runFlow inspector form, and the worker calls resolveFlowParameters on the merged { ...parent.bindingConfig, ...args } candidate as its first step in the runFlow case (invariant 14a–c). Declared defaults 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 onError path BEFORE the stack frame is pushed. When resolveFlowParameters reports any required keys still undefined after defaulting, the worker calls followError(\missing required parameters: ${missing.join(', ')}`)and breaks — no_flowStackmutation, nostate.flowswap, nobindingConfigchange on the parent. The parent's state is guaranteed untouched, so the chosenonError route (continue/endCall/goto`) is the only path the runtime takes from there.
  • onError: continue re-uses the flow.completed edge. That's the normal "sub-flow finished" transition. If you want a distinct error path, set onError: goto and point errorNodeId at 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.start doesn't resolve to a node in definition.nodes, the runtime takes onError with reason sub-flow has no start node. This usually means a hand-edited definition went out of sync; re-saving from the editor fixes it.