Skip to content

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

hcl
<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:

FieldRequiredDescription
idYesUnique identifier for the branch.
entryYesThe first step of the branch.
stepsYesA 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:

FieldRequiredDescription
on_all_successYesNext step or terminal when ALL branches succeed.
on_any_failureYesFailureHandler when ANY branch fails.
on_all_completeNoNext 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.

hcl
// 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 error

This is why the supply chain inspection pattern uses separate entities for parallel inspections:

hcl
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:

hcl
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:

hcl
// --- 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:

PathRouteTerminal
1begin -> parallel (both pass) -> clearance approved -> releaseshipment_cleared
2begin -> parallel (both pass) -> clearance not approved -> holdshipment_held
3begin -> parallel (any fails)inspection_failed
4begin -> failsinspection_blocked
5begin -> parallel (both pass) -> hold fails -> compensate -> revertinspection_reverted
6begin -> parallel (both pass) -> hold fails -> compensate failsrevert_failed
7begin -> parallel (both pass) -> release failsrelease_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_success merge 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.