A ParallelStep executes multiple branches concurrently within a flow. Each branch is an independent sub-DAG with its own entry step and step map. Parallel branches must have non-overlapping entity effect sets, all branches complete before the join evaluates, and compensation handles failure after partial success.
DSL Syntax
<step_id>: ParallelStep {
branches: [
Branch {
id: <BranchId>
entry: <StepId>
steps: {
<StepId>: <Step>
...
}
},
...
]
join: JoinPolicy {
on_all_success: <StepId> | Terminal(<outcome>)
on_any_failure: <FailureHandler>
on_all_complete: <StepId> | Terminal(<outcome>) | null
}
}Branch Definition
Each branch is an independent sub-DAG:
| Field | Required | Description |
|---|---|---|
id | Yes | Unique identifier for the branch. |
entry | Yes | The first step of the branch. |
steps | Yes | A map of step ids to step definitions (same step types as flow-level steps). |
JoinPolicy
The join policy determines what happens after all branches complete:
| Field | Required | Description |
|---|---|---|
on_all_success | Yes | Next step or terminal when ALL branches succeed. |
on_any_failure | Yes | FailureHandler when ANY branch fails. |
on_all_complete | No | Next step or terminal when all branches complete regardless of individual success/failure. Set to null if unused. |
Permitted merge conditions: all_success, any_failure, all_complete. The first_success merge policy is NOT supported -- all branches run to completion.
Non-Overlapping Entity Effects
Parallel branches must have disjoint entity effect sets. If branch A transitions entity QualityLot and branch B also transitions QualityLot, the parallel step is rejected at elaboration time.
This constraint is verified by transitively resolving all operation effects across all branches. Two branches cannot simultaneously modify the same entity type. This prevents non-deterministic final states that would depend on branch execution order.
// VALID: different entities per branch
// Branch 1 affects QualityLot
// Branch 2 affects ComplianceLot
// No overlap
// INVALID: both branches affect the same entity
// This is a Pass 5 errorThis is why the supply chain inspection pattern uses separate entities for parallel inspections:
entity QualityLot {
states: [pending, in_progress, passed, failed]
initial: pending
transitions: [
(pending, in_progress),
(in_progress, passed),
(in_progress, failed)
]
}
entity ComplianceLot {
states: [pending, in_progress, passed, failed]
initial: pending
transitions: [
(pending, in_progress),
(in_progress, passed),
(in_progress, failed)
]
}QualityLot and ComplianceLot are separate entities by spec requirement, not by modeling preference. Separate entities make the branches truly independent, and the elaborator verifies this statically.
All Branches Complete
All branches run to completion before the join evaluates. Branch execution order is implementation-defined. The join outcome is a function of the set of branch terminal outcomes, not their order.
There is no early termination on first failure. If branch A fails, branches B and C continue to their terminal states. Only after all branches complete does the join policy evaluate.
Frozen Snapshot in Parallel Branches
Parallel branches execute under the parent flow's frozen snapshot. No branch sees entity state changes produced by another branch during execution. This is guaranteed by:
- Disjoint entity effects -- branches cannot modify the same entities.
- Frozen verdict semantics -- all branches evaluate predicates against the same snapshot.
- Branch isolation (E8) -- the executor enforces that no branch observes another branch's state changes.
Compensation on Failure
When some branches succeed and others fail, the successful branches may have committed entity state changes that need to be rolled back. The Compensate failure handler declares how to undo completed work:
on_failure: Compensate(
steps: [
{
op: revert_quality_inspection
persona: customs_officer
on_failure: Terminal(revert_failed)
},
{
op: revert_compliance_inspection
persona: customs_officer
on_failure: Terminal(revert_failed)
}
]
then: Terminal(inspection_reverted)
)Compensation rules:
- Each compensation step names an operation and persona.
- Compensation step failure handlers are Terminal only -- no nested compensation.
- Compensation is not error handling. It is rollback of committed state.
Full Working Example: Supply Chain Inspection
A complete contract with parallel inspection branches, compensation, and post-join routing:
// --- Personas ---
persona customs_officer
persona quality_inspector
persona port_authority
// --- Entities (separate for parallel safety) ---
entity Shipment {
states: [arrived, inspecting, cleared, held]
initial: arrived
transitions: [
(arrived, inspecting),
(inspecting, cleared),
(inspecting, held)
]
}
entity QualityLot {
states: [pending, in_progress, passed, failed]
initial: pending
transitions: [
(pending, in_progress),
(in_progress, passed),
(in_progress, failed),
(passed, pending)
]
}
entity ComplianceLot {
states: [pending, in_progress, passed, failed]
initial: pending
transitions: [
(pending, in_progress),
(in_progress, passed),
(in_progress, failed),
(passed, pending)
]
}
// --- Facts ---
fact cargo_weight_kg {
type: Int(min: 0, max: 1000000)
source: "cargo_service.total_weight_kg"
}
fact documentation_complete {
type: Bool
source: "customs_service.docs_verified"
default: false
}
// --- Rules ---
rule weight_acceptable {
stratum: 0
when: cargo_weight_kg <= 50000
produce: verdict weight_ok { payload: Bool = true }
}
rule docs_complete {
stratum: 0
when: documentation_complete = true
produce: verdict docs_ok { payload: Bool = true }
}
rule clearance_approved {
stratum: 1
when: verdict_present(weight_ok) and verdict_present(docs_ok)
produce: verdict clearance_approved { payload: Bool = true }
}
// --- Operations ---
operation begin_inspection {
personas: [customs_officer]
require: true
effects: [Shipment: arrived -> inspecting]
outcomes: [started]
}
operation start_quality_check {
personas: [quality_inspector]
require: true
effects: [QualityLot: pending -> in_progress]
outcomes: [started]
}
operation record_quality_pass {
personas: [quality_inspector]
require: verdict_present(weight_ok)
effects: [QualityLot: in_progress -> passed]
outcomes: [passed]
}
operation start_compliance_check {
personas: [customs_officer]
require: true
effects: [ComplianceLot: pending -> in_progress]
outcomes: [started]
}
operation record_compliance_pass {
personas: [customs_officer]
require: verdict_present(docs_ok)
effects: [ComplianceLot: in_progress -> passed]
outcomes: [passed]
}
operation release_shipment {
personas: [port_authority]
require: verdict_present(clearance_approved)
effects: [Shipment: inspecting -> cleared]
outcomes: [cleared]
}
operation hold_shipment {
personas: [customs_officer]
require: true
effects: [Shipment: inspecting -> held]
outcomes: [held]
}
operation revert_quality {
personas: [customs_officer]
require: true
effects: [QualityLot: passed -> pending]
outcomes: [reverted]
}
// --- Flow with parallel branches ---
flow inspection_flow {
snapshot: at_initiation
entry: step_begin
steps: {
step_begin: OperationStep {
op: begin_inspection
persona: customs_officer
outcomes: { started: step_parallel_inspect }
on_failure: Terminate(outcome: inspection_blocked)
}
step_parallel_inspect: ParallelStep {
branches: [
Branch {
id: branch_quality
entry: step_start_quality
steps: {
step_start_quality: OperationStep {
op: start_quality_check
persona: quality_inspector
outcomes: { started: step_quality_result }
on_failure: Terminate(outcome: quality_failed)
}
step_quality_result: OperationStep {
op: record_quality_pass
persona: quality_inspector
outcomes: { passed: Terminal(quality_cleared) }
on_failure: Terminate(outcome: quality_failed)
}
}
},
Branch {
id: branch_compliance
entry: step_start_compliance
steps: {
step_start_compliance: OperationStep {
op: start_compliance_check
persona: customs_officer
outcomes: { started: step_compliance_result }
on_failure: Terminate(outcome: compliance_failed)
}
step_compliance_result: OperationStep {
op: record_compliance_pass
persona: customs_officer
outcomes: { passed: Terminal(compliance_cleared) }
on_failure: Terminate(outcome: compliance_failed)
}
}
}
]
join: JoinPolicy {
on_all_success: step_release_decision
on_any_failure: Terminate(outcome: inspection_failed)
on_all_complete: null
}
}
step_release_decision: BranchStep {
condition: verdict_present(clearance_approved)
persona: port_authority
if_true: step_release
if_false: step_hold
}
step_release: OperationStep {
op: release_shipment
persona: port_authority
outcomes: { cleared: Terminal(shipment_cleared) }
on_failure: Terminate(outcome: release_failed)
}
step_hold: OperationStep {
op: hold_shipment
persona: customs_officer
outcomes: { held: Terminal(shipment_held) }
on_failure: Compensate(
steps: [{
op: revert_quality
persona: customs_officer
on_failure: Terminal(revert_failed)
}]
then: Terminal(inspection_reverted)
)
}
}
}Path enumeration for this flow:
| Path | Route | Terminal |
|---|---|---|
| 1 | begin -> parallel (both pass) -> clearance approved -> release | shipment_cleared |
| 2 | begin -> parallel (both pass) -> clearance not approved -> hold | shipment_held |
| 3 | begin -> parallel (any fails) | inspection_failed |
| 4 | begin -> fails | inspection_blocked |
| 5 | begin -> parallel (both pass) -> hold fails -> compensate -> revert | inspection_reverted |
| 6 | begin -> parallel (both pass) -> hold fails -> compensate fails | revert_failed |
| 7 | begin -> parallel (both pass) -> release fails | release_failed |
Every path terminates. The elaborator proves this via S6.
Constraints
- Branch sub-DAGs must be acyclic.
- No overlapping entity effect sets across branches. Verified at contract load time by transitively resolving all operation effects across all branches.
- All branches run to completion before the join evaluates. Branch execution order is implementation-defined.
- Parallel branches execute under the parent flow's frozen snapshot -- no branch sees entity state changes from another branch (E8).
first_successmerge policy is not supported.- Compensation step failure handlers are Terminal only -- no nested compensation.
Common Mistakes
Same entity in multiple branches. Two branches that both transition Order will be rejected. Use separate entity types (e.g., QualityLot and ComplianceLot) for independent parallel work.
Expecting early termination. All branches complete before the join evaluates. There is no "cancel remaining branches on first failure" behavior.
Nested compensation. Compensation steps can only have Terminal failure handlers. You cannot compensate a compensation.
Assuming branch ordering. Branch execution order is implementation-defined. The join outcome depends on the set of branch outcomes, not their order.
How Parallel Steps Connect to Other Constructs
- Operations are executed within branch steps. Each branch's operations affect disjoint entity sets.
- Entities must be partitioned across branches. The elaborator enforces this statically.
- Personas appear at every step within branches, just like in non-parallel flow steps.
- Rules provide the verdicts that branch steps evaluate (via the frozen snapshot).
- Compensation invokes operations to undo partial work when the parallel step fails.
Static Analysis Properties
- S6 (Flow path enumeration) includes all parallel branch paths in its exhaustive enumeration. Both-succeed, any-failure, and compensation paths are all covered.
- The elaborator transitively resolves all operation effects across all branches to verify disjoint entity sets.
- Parallel steps do not increase the computational complexity class of evaluation -- the bound is the product of branch path counts.