SubWorkflow sub-workflow
Execute a child workflow inline and wait for it to complete. The parent workflow pauses until the child finishes, then routes based on the child's final status.
When to Use
- Modular workflow design: Break a 50-node monolithic workflow into reusable sub-workflows — for example, extract a "Send Invoice" sub-workflow that is called from both the "New Order" and "Subscription Renewal" parent workflows without duplicating any steps.
- Shared processing logic: Multiple parent workflows invoke the same "Validate Customer" sub-workflow rather than embedding identical validation logic in each parent. A fix in the child propagates everywhere automatically.
- Multi-stage onboarding: A parent "Employee Onboarding" workflow sequentially calls sub-workflows for IT provisioning, HR paperwork, and Slack workspace setup — each sub-workflow is independently testable and replaceable.
- Conditional delegation: Route to different sub-workflows based on a runtime condition — invoke "Standard Approval" or "Expedited Approval" sub-workflow based on the request amount, keeping the decision logic in the parent and the execution logic in the children.
- Error isolation: Wrap risky external-API calls in a dedicated sub-workflow so the parent can handle success and failure cleanly via ports, without embedding try/catch logic at the top level.
Configuration
| Field | Required | Default | Description |
|---|---|---|---|
sub_workflow_id |
Required* | — | ProcessThread ID of the child workflow to invoke. Optional at configuration time — must be resolvable at runtime. Resolution order: InputData field sub_workflow_id → memory variable $var.sub_workflow_id → static config value. If none resolves, the node routes to the error port. |
sub_workflow_version_id |
Optional | 1 |
Version of the child workflow to execute. Increment to pin a parent to a specific child version, allowing safe child-workflow upgrades without affecting running parent instances. |
sub_workflow_id does not need to be a static integer. You can pass it through InputData from a trigger, store it in a workflow variable, or compute it with an expression node. This enables dynamic dispatch — selecting which child workflow to call based on runtime data.
Sample Configuration
Static child workflow ID
{
"sub_workflow_id": 42,
"sub_workflow_version_id": 3
}
Dynamic child workflow ID from a workflow variable
{
"sub_workflow_id": "{{ $var.approval_workflow_id }}",
"sub_workflow_version_id": 1
}
Child workflow ID resolved from InputData (no static config needed)
{
"sub_workflow_version_id": 1
}
// sub_workflow_id provided in InputData by the preceding node
Validation Errors
| Error Code / Message | Cause |
|---|---|
SUB_WORKFLOW_ID_NOT_RESOLVEDSub-workflow ID not resolved. Provide sub_workflow_id via InputData, memory variable, or static config. |
All three resolution sources (InputData, variable, static config) returned null or empty at runtime. The node routes to the error port — not a validation-time error. |
MAX_NESTING_DEPTH_EXCEEDEDSub-workflow nesting depth limit (10) exceeded. Check for circular workflow references. |
The call stack has reached 10 levels of nested sub-workflow invocations. Most commonly caused by circular references (A calls B calls A) or deeply recursive designs. Routes to error port at runtime. |
CHILD_WORKFLOW_NOT_FOUNDChild workflow ID {id} version {v} not found. |
The resolved sub_workflow_id and sub_workflow_version_id combination does not match any published workflow in the system. |
Output / Execution Ports
| Port | Fires When |
|---|---|
success |
Child workflow completed with a final status of "success" or "completed". All child outputs are available under the subworkflow.* namespace on this port. |
error |
Child workflow failed, was cancelled, exceeded the nesting limit, or the workflow ID could not be resolved. The error.message field describes the specific reason. |
success Port — Output Fields
| Field | Type | Description |
|---|---|---|
subworkflow.result | object | The final output data written by the child's StopWorkflow node — the primary return value of the child workflow. |
subworkflow.* | object | All child node outputs and variables are accessible under this prefix. For example, if the child has a node with key provisioning, its outputs are at subworkflow.provisioning.*. |
error Port — Output Fields
| Field | Type | Description |
|---|---|---|
error.message | string | Human-readable reason for failure: nesting limit exceeded, workflow ID not found, child workflow failed, or child workflow cancelled. |
error.code | string | Machine-readable error code: SUB_WORKFLOW_ID_NOT_RESOLVED, MAX_NESTING_DEPTH_EXCEEDED, CHILD_WORKFLOW_NOT_FOUND, CHILD_WORKFLOW_FAILED, or CHILD_WORKFLOW_CANCELLED. |
Sample Output
success port output (available via $output.launchSetup)
{
"subworkflow": {
"result": {
"userId": "u_88142",
"email": "jane.doe@acme.com",
"provisionedAt": "2026-05-26T09:14:02Z"
},
"provisioning": {
"adAccountCreated": true,
"adAccountId": "AD-88142"
},
"slackInvite": {
"channelId": "C04XYZABC",
"inviteSent": true
}
}
}
error port output (available via $output.launchSetup)
{
"error": {
"code": "CHILD_WORKFLOW_FAILED",
"message": "Child workflow 42 failed at node 'validateLicense' with: License key is expired."
}
}
Expression Reference
| Expression | Result |
|---|---|
{{ $output.launchSetup.subworkflow.result }} | The complete final output object from the child workflow's StopWorkflow node. |
{{ $output.launchSetup.subworkflow.result.userId }} | A specific field within the child's final result. |
{{ $output.launchSetup.subworkflow.provisioning.adAccountId }} | Output from a specific node named provisioning inside the child workflow. |
{{ $output.launchSetup.error.message }} | Human-readable failure reason (available on the error port path). |
{{ $output.launchSetup.error.code }} | Machine-readable error code for conditional error handling. |
{{ $parent.output.NodeName.fieldName }} and parent memory variables via {{ $parent.var.variableName }}. Parent inputs and variables are automatically prefixed into the child's InputData at invocation time.
Node Policies & GuardRails
| Policy Area | Recommendation |
|---|---|
| Circular reference prevention | Never design workflow A to call workflow B if B (directly or transitively) calls A. The 10-level nesting limit will catch it, but the workflow wastes execution time reaching that limit. Audit call graphs before publishing. |
| ID management | Store sub_workflow_id in a configuration variable or environment setting rather than hardcoding it. This allows environment-specific versions (dev/staging/prod child workflows) without changing the parent workflow definition. |
| Error port handling | Always connect the error port to a handler. Child failures — including network errors, validation failures, and nesting violations — do not propagate automatically. An unhandled error port will silently terminate the parent execution branch. |
| Independent child testing | Test child workflows in isolation using ManualTrigger before wiring them into a parent. A child that fails in isolation will fail the same way when called via SubWorkflow, and debugging is easier without the parent context. |
| Data minimisation | Pass only the fields the child needs via InputData. Avoid passing entire parent output objects — this increases payload size, makes child workflows harder to reuse in other contexts, and can expose sensitive data unnecessarily. |
| Version pinning | When the child workflow is actively developed, pin sub_workflow_version_id in production parent workflows. Unpinned calls always use version 1 — deploying a breaking change to the child will immediately break all parents calling it. |
Examples
Example 1 — Reusable Customer Validation Sub-Workflow
An order processing parent workflow calls a shared "Validate Customer" child workflow before proceeding. The child checks account status, credit limit, and region eligibility. The parent branches on success or failure without duplicating any validation logic.
WebhookTrigger [key: "orderReceived"]
(POST /orders/new)
└─► SubWorkflow [key: "validateCustomer"]
sub_workflow_id: 18 // "Validate Customer" workflow
sub_workflow_version_id: 2
├─► success ──► IfCondition [key: "checkCreditLimit"]
│ Condition: {{ $output.validateCustomer.subworkflow.result.creditApproved == true }}
│ ├─► true ──► [proceed to fulfillment]
│ └─► false ──► StopWorkflow (status: cancelled, message: "Credit limit exceeded")
└─► error ──► StopWorkflow (status: failed, message: {{ $output.validateCustomer.error.message }})
Example 2 — Sequential Onboarding Sub-Workflows
A parent "Employee Onboarding" workflow calls three sub-workflows in sequence: IT provisioning, HR paperwork, and Slack setup. Each sub-workflow is independently owned by a different team. The parent orchestrates the sequence and handles failures at each stage.
FormTrigger [key: "hrFormSubmitted"]
└─► SubWorkflow [key: "itProvisioning"]
sub_workflow_id: {{ $var.it_provisioning_workflow_id }}
├─► success ──► SubWorkflow [key: "hrPaperwork"]
│ sub_workflow_id: {{ $var.hr_paperwork_workflow_id }}
│ ├─► success ──► SubWorkflow [key: "slackSetup"]
│ │ sub_workflow_id: {{ $var.slack_setup_workflow_id }}
│ │ ├─► success ──► EmailSmtp (welcome email to new employee)
│ │ └─► error ──► Slack (alert: Slack setup failed for {{ $json.employeeName }})
│ └─► error ──► Slack (alert: HR paperwork failed)
└─► error ──► Slack (alert: IT provisioning failed — manual intervention required)
Example 3 — Dynamic Sub-Workflow Dispatch
An approval routing workflow selects which approval sub-workflow to call based on the request amount. Small requests go to a fast-track single-approver workflow; large requests go to a multi-stage committee approval workflow. The parent does not embed any approval logic itself.
WebhookTrigger [key: "approvalRequest"]
└─► IfCondition [key: "routeByAmount"]
Condition: {{ $json.amount > 10000 }}
├─► true ──► VariableAssignment [key: "setCommitteeId"]
│ approval_workflow_id = 77 // Committee Approval workflow
│ └─► SubWorkflow [key: "runApproval"]
│ sub_workflow_id: {{ $var.approval_workflow_id }}
│ ├─► success ──► HttpRequest (POST /orders/{{ $json.orderId }}/approve)
│ └─► error ──► StopWorkflow (status: failed, message: "Approval process failed")
└─► false ──► VariableAssignment [key: "setFastTrackId"]
approval_workflow_id = 31 // Fast-Track Approval workflow
└─► SubWorkflow [key: "runApproval"]
(same wiring as above)