Portal Community

When to Use

Configuration

FieldRequiredDefaultDescription
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_idmemory variable $var.sub_workflow_idstatic 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.
 Runtime ID resolution: 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 / MessageCause
SUB_WORKFLOW_ID_NOT_RESOLVED
Sub-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_EXCEEDED
Sub-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_FOUND
Child 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

PortFires 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

FieldTypeDescription
subworkflow.resultobjectThe final output data written by the child's StopWorkflow node — the primary return value of the child workflow.
subworkflow.*objectAll 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

FieldTypeDescription
error.messagestringHuman-readable reason for failure: nesting limit exceeded, workflow ID not found, child workflow failed, or child workflow cancelled.
error.codestringMachine-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

ExpressionResult
{{ $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.
 Child accessing parent data: Inside the child workflow, the parent's node outputs are accessible via {{ $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 AreaRecommendation
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)