Parallel Fork/Join Example
This worked example triages a CVE by running two independent steps in parallel — assessing impact and notifying the security team — then joining to produce a single report. It exercises a fork, an AND-join, and branch-aware simulation.
The workflow
fetch-cve fans out to analyze-impact and notify-team (both edges are unconditional, so they run
in parallel). Both branches converge on create-report, which waits for both before it runs.
{
"id": "cve-analyze-and-notify",
"name": "CVE Analyze and Notify",
"description": "Assess impact and notify the security team in parallel, then report.",
"version": 1,
"nodes": [
{
"id": "start", "type": "start", "name": "Start",
"config": { "inputs": [{ "name": "cveId", "type": "string", "required": true }] },
"position": { "x": 40, "y": 200 }
},
{
"id": "fetch-cve", "type": "action", "name": "Fetch CVE Details",
"config": {
"actionType": "fetch-cve",
"inputs": { "CVE ID": "context.cveId" },
"outputs": [{ "name": "cve", "type": "object", "required": true }]
},
"position": { "x": 260, "y": 200 }
},
{
"id": "analyze-impact", "type": "action", "name": "Analyze Impact",
"config": {
"actionType": "analyze-impact",
"inputs": { "CVE": "context.cve" },
"outputs": [{ "name": "severity", "type": "string", "required": true }]
},
"position": { "x": 500, "y": 100 }
},
{
"id": "notify-team", "type": "action", "name": "Notify Security Team",
"config": {
"actionType": "send-notification",
"inputs": { "CVE ID": "context.cveId" },
"outputs": [{ "name": "notifiedAt", "type": "string", "required": true }]
},
"position": { "x": 500, "y": 300 }
},
{
"id": "create-report", "type": "action", "name": "Create Report",
"config": {
"actionType": "create-report",
"inputs": { "CVE ID": "context.cveId", "Severity": "context.severity" },
"outputs": [{ "name": "reportUrl", "type": "string", "required": true }]
},
"position": { "x": 760, "y": 200 }
},
{
"id": "end", "type": "end", "name": "Published",
"config": { "outcome": "published" },
"position": { "x": 1000, "y": 200 }
}
],
"edges": [
{ "id": "e1", "source": "start", "target": "fetch-cve", "priority": 0, "isDefault": false },
{ "id": "e2", "source": "fetch-cve", "target": "analyze-impact", "priority": 0, "isDefault": false },
{ "id": "e3", "source": "fetch-cve", "target": "notify-team", "priority": 1, "isDefault": false },
{ "id": "e4", "source": "analyze-impact", "target": "create-report", "priority": 0, "isDefault": false },
{ "id": "e5", "source": "notify-team", "target": "create-report", "priority": 0, "isDefault": false },
{ "id": "e6", "source": "create-report", "target": "end", "priority": 0, "isDefault": false }
]
}
Why this is a valid fork/join
fetch-cveis a fork: it has two outgoing edges (e2,e3), both unconditional (isDefault: false, nocondition). Completingfetch-cveactivates both branches.create-reportis the join: it is the first node reachable from both branches and has two incoming edges (e4,e5). As an AND-join it runs only after bothanalyze-impactandnotify-teamcomplete.- The region is balanced: neither branch reaches
endbeforecreate-report, so it validates with noFORK_WITHOUT_JOINorPARALLEL_BRANCH_REACHES_ENDproblems.
Supported join topology
Join arrivals are tracked by incoming edge ID. Before Java calls any executor, validation checks that each fork branch reaches its join through one distinct incoming edge. The editor validator and simulator apply the same rules; unsupported topology fails simulation before any node is entered.
- Exclusive choices inside a branch are supported when their paths merge before the parallel join,
so either choice reaches the same arrival edge. Separate conditional/default edges directly into the
join are also separate arrival edges and are rejected (
UNBALANCED_PARALLEL). - Nested forks must finish at their own join before reaching the enclosing join. Forks sharing a join,
or sibling branches sharing an arrival edge, are rejected (
UNBALANCED_PARALLEL). - Edges from outside the region into its branch interiors or join are rejected
(
CROSSING_PARALLEL_REGIONS), including conditional paths that bypass the fork. - A completed region may loop back to its fork, and an inner region may repeat while its outer sibling
waits. A branch returning to its own fork before joining is rejected (
PARALLEL_REGION_CYCLE). - A join may also fork into the next balanced region. Ordinary sequential exclusive choices remain supported.
These checks are structural: conditions are not assumed to be always true or mutually exhaustive. Runtime condition failures, missing matching edges, and loops exceeding the transition limit still fail at execution time. Persisted branch and join-arrival fields retain their existing format.
Running it
Starting the workflow activates both branches after fetch-cve. The instance's activeBranches holds
one entry per running branch; currentNodeId is null while both run. create-report fires once both
arrive; its two incoming edge ids accumulate in joinArrivals until the second lands. See
Engine Usage for the resume-by-node API.
Visualizing and simulating it
The Workflow Viewer highlights both active nodes at once and
animates both branch arrivals. In the Visual Editor, fetch-cve
carries a Fork badge and create-report a Join badge, and simulation lets you resume each
blocked branch one at a time.