ParallelJoin parallel-join
Wait for all parallel lanes opened by a ParallelFork to complete, merge their outputs into a unified result structure, and resume sequential workflow execution. Reads exclusively from ExecutionMemory — no configuration required.
lane_outputs keyed by lane node key. Every branch connected to the Fork's success port must eventually connect into the paired Join.
When to Use
- Merge parallel API results: After 3 concurrent enrichment API calls (credit, fraud, loyalty), Join merges all responses into a single unified object for the decision node downstream.
- Aggregate parallel notification outcomes: After sending notifications via email, SMS, and Slack simultaneously, Join collects each channel's delivery status so a downstream node can determine if at least one channel succeeded.
- Consolidate parallel validation results: After running 4 parallel compliance checks, Join assembles all pass/fail results so a downstream IfCondition can check whether all validations passed.
- Resume sequential execution after parallel work: Whenever a workflow uses a ParallelFork, a ParallelJoin must follow to return to single-thread sequential execution mode and make all lane outputs available.
Configuration
Output Ports
| Port | Fires When |
|---|---|
success | All parallel lanes have completed successfully (or with handled errors when FailFast: false). Lane outputs are available in lane_outputs. Sequential execution resumes. |
error | One or more lanes failed AND the paired Fork has FailFast: true. Exception details from the failing lane are available. Wire this to a CatchBlock or error notification node. |
OutputData Fields
Fields available downstream via $output.<joinNodeKey>:
| Field | Type | Description |
|---|---|---|
join_id | string | The node key of this Join node — for audit and logging purposes. |
lanes_completed | string | ISO 8601 timestamp of when the final lane completed and the Join fired. |
lane_count | int | Total number of parallel lanes that were collected. Matches the number of branches connected to the paired Fork's success port. |
lane_outputs | dict | Dictionary of lane results keyed by lane_<laneNodeKey>. Each entry contains the final output data of that lane's last node. Access: $output.myJoin.lane_outputs.lane_creditCheck. |
Accessing Lane Outputs
After the Join fires, each lane's output is accessible by key. The key format is lane_ followed by the node key of the first node in that lane (the node directly connected to the Fork's success port).
| Expression | Returns |
|---|---|
{{ $output.myJoin.lane_outputs.lane_creditCheck }} | Full output object of the creditCheck lane. |
{{ $output.myJoin.lane_outputs.lane_creditCheck.score }} | Specific field from that lane's output. |
{{ $output.myJoin.lane_count }} | Total number of lanes collected. |
{{ $output.myJoin.lanes_completed }} | ISO timestamp when all lanes finished. |
Sample Output
Success Port — All Branches Joined
ParallelJoin collects outputs from all forked branches and merges them into a single output when all are complete.
{
"_join": {
"waitedFor": 3,
"receivedCount": 3,
"timedOutCount": 0,
"joinStrategy": "all",
"totalWaitMs": 1650
},
"merged": {
"inventory": { "totalItems": 1432, "lowStockCount": 12 },
"pricing": { "priceListVersion": "2025-03", "updatedCount": 89 },
"suppliers": { "activeSuppliers": 24, "pendingOrders": 7 }
}
}
Timeout Port (if join times out)
{
"_join": {
"waitedFor": 3,
"receivedCount": 2,
"timedOutCount": 1,
"timedOutBranches": ["fetchSuppliers"],
"joinStrategy": "all",
"timeoutMs": 5000
}
}
Expression Reference
| Expression | Description |
|---|---|
{{ $output.joinNode.merged.inventory.totalItems }} | Access merged output from the "inventory" branch. |
{{ $output.joinNode._join.receivedCount }} | Number of branches that completed successfully. |
{{ $output.joinNode._join.timedOutBranches[0] }} | Name of the first branch that timed out, if any. |
{{ $output.joinNode._join.totalWaitMs }} | Total milliseconds waited for branches to complete. |
Node Policies and GuardRails
- One Join per Fork: Each ParallelFork must have exactly one paired ParallelJoin. Do not connect multiple Join nodes to a single Fork — this produces undefined behaviour in lane tracking.
- All Fork branches must reach the Join: Every branch connected to the Fork's
successport must have a path that leads to the Join node. A branch that terminates without reaching the Join leaves the Join waiting indefinitely. - Access lanes by node key, not by index: Use
lane_outputs.lane_myNodeKeyrather than positional access. Lane completion order is non-deterministic; positional references will produce incorrect results. - Lane outputs are merged at the Join: The Join assembles all lane data into a single context. This is the correct place to access cross-lane results — not inside individual lanes, where other lanes' data is not yet available.
- Wire the error port when FailFast is true: If the paired Fork uses
FailFast: true, always connect the Join'serrorport to appropriate error handling. An unwired error port causes the workflow instance to fault with no recovery path.
Pattern Examples
Pattern 1 — Three-Channel Notification with Result Check
Send notifications in parallel; after Join, check whether all channels succeeded or whether any failed. Escalate if all channels failed.
ParallelFork [key: "notifyFork"]
└─► success ──┬── EmailSmtp [key: "sendEmail"]
├── SlackNode [key: "sendSlack"]
└── HttpRequest [key: "sendSms"]
(POST /sms/send)
──► ParallelJoin [key: "notifyJoin"]
└─► success
emailResult: {{ $output.notifyJoin.lane_outputs.lane_sendEmail.status }}
slackResult: {{ $output.notifyJoin.lane_outputs.lane_sendSlack.ok }}
smsResult: {{ $output.notifyJoin.lane_outputs.lane_sendSms.statusCode }}
└─► IfCondition [key: "checkAnySucceeded"]
Condition: {{ $output.notifyJoin.lane_outputs.lane_sendEmail.status === 'sent' ||
$output.notifyJoin.lane_outputs.lane_sendSlack.ok === true }}
├─► true ──► [at least one channel succeeded]
└─► false ──► [escalate: create manual contact task]
Pattern 2 — Concurrent Enrichment Merge
Fetch credit score, fraud risk, and loyalty tier concurrently. After Join, combine all three into a unified risk profile for the decision engine.
ParallelFork [key: "enrichFork"]
└─► success ──┬── HttpRequest [key: "creditApi"] GET /credit/{{ $json.customerId }}
├── HttpRequest [key: "fraudApi"] GET /fraud/{{ $json.customerId }}
└── HttpRequest [key: "loyaltyApi"] GET /loyalty/{{ $json.customerId }}
──► ParallelJoin [key: "enrichJoin"]
└─► success ──► VariableAssignment [key: "buildProfile"]
riskProfile = {
creditScore: {{ $output.enrichJoin.lane_outputs.lane_creditApi.score }},
fraudRisk: {{ $output.enrichJoin.lane_outputs.lane_fraudApi.riskLevel }},
loyaltyPoints: {{ $output.enrichJoin.lane_outputs.lane_loyaltyApi.points }}
}
└─► [decision engine node uses $var.riskProfile]
Pattern 3 — Parallel Validation with All-Pass Gate
Run 4 compliance checks in parallel. After Join, verify all 4 passed before approving the application.
ParallelFork [key: "validateFork"] FailFast: false
└─► success ──┬── HttpRequest [key: "idCheck"] GET /verify/identity
├── HttpRequest [key: "creditCheck"] GET /verify/credit
├── HttpRequest [key: "incomeCheck"] GET /verify/income
└── HttpRequest [key: "addressCheck"] GET /verify/address
──► ParallelJoin [key: "validateJoin"]
└─► success ──► IfCondition [key: "allPassed"]
Condition: {{
$output.validateJoin.lane_outputs.lane_idCheck.passed &&
$output.validateJoin.lane_outputs.lane_creditCheck.passed &&
$output.validateJoin.lane_outputs.lane_incomeCheck.passed &&
$output.validateJoin.lane_outputs.lane_addressCheck.passed
}}
├─► true ──► [approve application]
└─► false ──► [route to human review]