Design spec: parallel execution — fork/join (concurrent branches) (#88) - #94
Conversation
Design-only deliverable for the concurrent-branches epic. Defines the fork/join model (inferred from edge shape, no new node types), wait-for-all AND-join semantics, fail-fast branch failure, flat last-write-wins context, the active-branch (token) state model and token-based advance loop, new validation rules for structured parallelism, viewer/simulation impact, and a phased delivery plan maintaining Java/TypeScript parity.
EricWittmann
left a comment
There was a problem hiding this comment.
Review: Parallel Execution Fork/Join — Design Spec
This PR adds a single design document (docs/superpowers/specs/2026-09-04-parallel-execution-fork-join-design.md, 344 lines) defining fork/join semantics for concurrent branches. It is design-only with no code changes, so this review focuses on internal consistency, accuracy against the current codebase, and design risks worth capturing before implementation.
I verified the spec's factual claims about the existing code and they all hold:
WorkflowInstanceis an immutablerecordwith a singlecurrentNodeId(engine/.../model/WorkflowInstance.java) and TScurrentNodeId: string(ui/src/types/instance.ts).advance()is a single-cursorwhile(true)loop guarded byMAX_TRANSITIONS, usinghasEnteredCurrentNode/completeCurrentHistoryEntryandselectEdgeexactly as described (WorkflowEngine.java).UNCONDITIONAL_MULTIPLE_EDGESexists today as a warning (validateWorkflow.ts:250) — accurately characterized.HistoryEntryhas nobranchIdtoday (both languages) — accurate.
Overall this is a thorough, well-structured spec with clear decisions, an explicit non-goals list, a sensible phased plan, and a migration note. It is in good shape. The notes below are suggestions/nitpicks to strengthen it — none block merging a design doc.
Suggestions
-
currentNodeIdas a "derived, back-compat accessor" on an immutable record needs a concrete plan.WorkflowInstanceis a Javarecord, socurrentNodeId()is an auto-generated component accessor and the field is also a Jackson-serialized wire component (@JsonFormatsiblings,Map.copyOf/List.copyOfinbuild()). Making it "derived" (returns the sole active node ornull) requires either removing the record component and adding a computed getter, or keeping a synchronized field — each with wire/serialization consequences. The spec says "wire parity" is intended; it would help to state explicitly whethercurrentNodeIdremains a serialized field (populated for single-branch,nullfor multi-branch) or becomes computed-only, since that affects deserialization of persisted instances and any Jackson round-trips. Worth nailing down in Phase 1 rather than "an implementation detail." -
Returning
nullfromcurrentNodeIdwhen >1 branch is active is an NPE hazard for existing consumers. The engine itself dereferencesinstance.currentNodeId()in many places (getHumanTaskInfo,getReceiveEventInfo,getWaitInfo,getActionInfo,matchesEvent,advance). The spec's back-compat argument ("only affects workflows that actually fork") is reasonable, but the transition of these call sites to the active-branch set is exactly the risky part and deserves a callout in the Phase 1 scope (the spec listsadvance()but not these accessor/matchesEventmethods). -
MAX_TRANSITIONS = 100shared across all branches may be too tight under fan-out. The spec says the guard "counts total steps across all branches." With multiple concurrent branches each taking several steps, a legitimate parallel workflow will consume this shared budget much faster than a linear one, risking false "possible infinite loop" failures. Consider whether the bound should scale (e.g., per-branch, orMAX_TRANSITIONS * activeBranchCount) and note it in the design. -
Resume API rename. The spec introduces
completeNode(instance, nodeId, output)for resume-by-node, but the current public API iscompleteCurrentNode(workflow, instance, result). Whether this is an additive method, a rename with a deprecated shim, or a breaking change matters for consumers and should be stated (the spec's back-compat emphasis elsewhere makes the silence here notable). -
Fail-fast + parked external work. Fail-fast "cancels siblings," but a sibling parked on a
HUMAN_TASK/RECEIVE_EVENTrepresents work owned by an external system. The engine is stateless and cannot recall it. A one-line acknowledgment that cancellation is logical (marks branch cancelled) rather than a real-world recall would set correct expectations. -
Edge
prioritysemantics inside a fork. For an exclusive-choice node, edgepriority(and the existingDUPLICATE_EDGE_PRIORITYwarning) is meaningful; for a fork (all-unconditional) it is not. It would be worth stating thatpriorityis ignored on fork edges and confirmingDUPLICATE_EDGE_PRIORITYshould not fire on a valid fork, alongside the other retired/adjusted rules.
Nitpicks
- Line 209: typo — "namespate by branch purpose" should be "namespace by branch purpose."
- Decisions table, "Graph modeling" row (line 66): the rationale reads "Reinterpret multiple edges (no new node types) | Minimal additions to
NodeTypeand both node-component registries..." This is mildly self-contradictory — the chosen option requires noNodeTypeadditions at all. Consider rewording to "No additions toNodeType..." to avoid confusion with the rejectedFORK/JOINalternative. - The spec references updating
docs/user-guide/validation.mdwhen retiringUNCONDITIONAL_MULTIPLE_EDGES(good — that file does document the rule set); Phase 5 already captures this.
Assessment
Good to merge as a design deliverable. The document is accurate, internally consistent, and appropriately scoped, with the semantic behavior change (fork-and-run-all) clearly flagged in the migration note. The suggestions above are refinements to fold into the Phase 1 plan — particularly the currentNodeId derivation/serialization strategy, the null-current call-site migration, and the MAX_TRANSITIONS budget under fan-out — rather than blockers for approving the design.
Summary
Design-only deliverable for the concurrent-branches epic (#88). Adds
docs/superpowers/specs/2026-09-04-parallel-execution-fork-join-design.md. No code changes.The spec commits to the decisions made for this epic and works out the semantics they imply:
are all unconditional (no condition, no default); anything with a condition/default remains
exclusive-choice; mixing the two is a new validation error. A join is the multi-incoming
convergence node, paired to its fork by static analysis.
structured/balanced-parallelism validation guarantees this equals "all branches converged."
ENDterminates the whole workflow,WAITINGwhen any branch is parked.
documented rather than engineered away.
Key structural elements defined: an active-branch (token) set replacing the single
currentNodeId(kept as a derived back-compat accessor), a token-based
advance()loop,branchIdonHistoryEntry, six new validation codes plus retiringUNCONDITIONAL_MULTIPLE_EDGES, multi-activehighlighting in the viewer/simulation, and a 5-phase delivery plan maintaining Java/TypeScript
parity.
Migration note
Definitions that today have ≥2 unconditional outgoing edges (previously the
UNCONDITIONAL_MULTIPLE_EDGESwarning, "take one") will, after Phase 1, fork and run all branches.This is the intended semantic upgrade; authors who meant exclusive choice must add conditions and/or a
default edge.
Scope
Design/spec only — no implementation. Once the design is approved, the companion phased plan and Phase 1
(Java engine + validator) follow.
Relates to #88 (design phase). Implementation tracked as follow-on phases.