Skip to content

A Flow is a finite, directed acyclic graph of steps that orchestrates the execution of declared operations under explicit persona control. Flows do not compute -- they sequence. All business logic remains in rules and operations.

DSL Syntax

hcl
flow <FlowId> {
  snapshot: at_initiation
  entry:    <StepId>

  steps: {
    <StepId>: <StepType> {
      ...
    }
    ...
  }
}

Fields

FieldRequiredDescription
snapshotYesAlways at_initiation in v1.0. The verdict set is frozen at flow start.
entryYesThe step where execution begins. Must exist in the steps map.
stepsYesA map of step ids to step definitions.

The Six Step Types

1. OperationStep

Executes a declared operation and routes on the outcome:

hcl
step_confirm: OperationStep {
  op:      confirm_delivery
  persona: seller
  outcomes: {
    confirmed: step_check_threshold
  }
  on_failure: Terminate(outcome: failure)
}
FieldRequiredDescription
opYesReferences a declared operation by id.
personaYesThe persona executing the operation. Must be in the operation's allowed_personas.
outcomesYesMap from outcome labels to next steps or terminals. Keys must exactly match the operation's declared outcomes (exhaustive).
on_failureYesA FailureHandler (Terminate, Compensate, or Escalate).

2. BranchStep

Evaluates a predicate against the frozen snapshot and routes on the result:

hcl
step_check_threshold: BranchStep {
  condition: verdict_present(within_threshold)
  persona:   escrow_agent
  if_true:   step_auto_release
  if_false:  step_handoff_compliance
}
FieldRequiredDescription
conditionYesA predicate expression evaluated against the frozen snapshot.
personaYesThe persona evaluating the condition.
if_trueYesNext step or terminal when the condition is true.
if_falseYesNext step or terminal when the condition is false.

3. HandoffStep

Transfers authority from one persona to another. The flow pauses until the new persona acts:

hcl
step_handoff_compliance: HandoffStep {
  from_persona: escrow_agent
  to_persona:   compliance_officer
  next:         step_compliance_release
}
FieldRequiredDescription
from_personaYesThe persona giving up control.
to_personaYesThe persona receiving control.
nextYesThe step that executes under the new persona.

4. SubFlowStep

Invokes another flow as a nested step:

hcl
step_run_appeal: SubFlowStep {
  flow:       appeal_process
  persona:    appeals_board
  on_success: step_overturn
  on_failure: Terminate(outcome: appeal_denied)
}
FieldRequiredDescription
flowYesReferences another declared flow by id.
personaYesThe persona initiating the sub-flow.
on_successYesNext step or terminal on sub-flow success.
on_failureYesA FailureHandler. Required.

Sub-flows inherit the invoking flow's snapshot. They do NOT take an independent snapshot (obligation E5).

5. ParallelStep

Executes multiple branches concurrently. See Parallel Execution for the full reference.

hcl
step_parallel_inspect: ParallelStep {
  branches: [
    Branch {
      id:    branch_quality
      entry: step_quality
      steps: { ... }
    },
    Branch {
      id:    branch_compliance
      entry: step_compliance
      steps: { ... }
    }
  ]
  join: JoinPolicy {
    on_all_success:  step_release_decision
    on_any_failure:  Terminate(outcome: inspection_failed)
    on_all_complete: null
  }
}

6. Terminal

Marks the end of a path with a named outcome:

hcl
Terminal(outcome: "success")
Terminal(outcome: "failure")
Terminal(outcome: "escalation")

Terminal outcomes are one of: "success", "failure", or "escalation".

Frozen Snapshot Semantics

Within a flow, the ResolvedVerdictSet is computed once at flow initiation and is never recomputed after intermediate operation execution. Operations within a flow do not see entity state changes produced by preceding steps in the same flow.

This is a fundamental semantic commitment: flows are pure decision graphs over a stable logical universe.

Why this matters: Without frozen verdicts, a race condition is possible. Imagine the escrow amount is $9,000 at flow initiation (under the $10,000 threshold), but by the time a branch evaluates, it has changed to $11,000. Without freezing, the flow would route using the old decision but the amount would now exceed the threshold. Frozen verdicts eliminate this class of bug by construction.

If you need fresh verdicts, start a new flow.

Failure Handling

Every OperationStep and SubFlowStep must declare a FailureHandler. Three options:

Terminate

Ends the flow with a terminal outcome:

hcl
on_failure: Terminate(outcome: failure)

Compensate

Runs compensation operations to undo partial work, then terminates:

hcl
on_failure: Compensate(
  steps: [
    {
      op:         revert_delivery_confirmation
      persona:    escrow_agent
      on_failure: Terminal(failure)
    }
  ]
  then: Terminal(failure)
)

Compensation steps are Terminal-only on failure -- no nested compensation. Each compensation step names an operation and persona.

Escalate

Transfers control to another persona:

hcl
on_failure: Escalate(
  to_persona: compliance_officer
  next:       step_manual_review
)

S6 -- Flow Path Enumeration

Static analysis S6 proves that every possible path through a flow reaches a terminal. For each flow, the analyzer derives: all possible execution paths, all personas at each step, all operation outcomes at each OperationStep, and all terminal outcomes.

Because OperationStep outcome handling is exhaustive (outcome map keys must exactly equal the operation's declared outcomes), the set of possible paths through an OperationStep is exactly the set of declared outcomes.

Running tenor check:

Flow Paths (S6): 6 total paths across 1 flows

Every path -- including both parallel branch outcomes, compensation paths, and escalation paths -- reaches a terminal.

Full Working Example

The escrow release flow from the Tenor conformance suite:

hcl
// --- Personas ---
persona buyer
persona seller
persona escrow_agent
persona compliance_officer

// --- Entity ---
entity EscrowAccount {
  states:  [held, released, refunded, disputed]
  initial: held
  transitions: [
    (held, released),
    (held, refunded),
    (held, disputed),
    (disputed, released),
    (disputed, refunded)
  ]
}

// --- Facts ---
fact escrow_amount {
  type:   Money(currency: "USD")
  source: "escrow_service.balance"
}

fact compliance_threshold {
  type:    Money(currency: "USD")
  source:  "compliance_db.threshold"
  default: 10000.00
}

fact delivery_confirmed {
  type:    Bool
  source:  "tracking_service.delivered"
  default: false
}

// --- Rules ---
rule within_threshold {
  stratum: 0
  when:    escrow_amount <= compliance_threshold
  produce: verdict within_threshold { payload: Bool = true }
}

rule delivery_ok {
  stratum: 0
  when:    delivery_confirmed = true
  produce: verdict delivery_ok { payload: Bool = true }
}

// --- Operations ---
operation confirm_delivery {
  personas: [seller]
  require:  verdict_present(delivery_ok)
  effects:  [EscrowAccount: held -> held]
  outcomes: [confirmed]
}

operation release_escrow {
  personas: [escrow_agent]
  require:  verdict_present(within_threshold)
  effects:  [EscrowAccount: held -> released]
  outcomes: [released]
}

operation release_escrow_with_compliance {
  personas: [compliance_officer]
  require:  verdict_present(delivery_ok)
  effects:  [EscrowAccount: held -> released]
  outcomes: [released]
}

operation revert_delivery_confirmation {
  personas: [escrow_agent]
  require:  true
  effects:  [EscrowAccount: held -> held]
  outcomes: [reverted]
}

// --- Flow ---
flow standard_release {
  snapshot: at_initiation
  entry:    step_confirm

  steps: {
    step_confirm: OperationStep {
      op:      confirm_delivery
      persona: seller
      outcomes: { confirmed: step_check_threshold }
      on_failure: Terminate(outcome: failure)
    }

    step_check_threshold: BranchStep {
      condition: verdict_present(within_threshold)
      persona:   escrow_agent
      if_true:   step_auto_release
      if_false:  step_handoff_compliance
    }

    step_auto_release: OperationStep {
      op:      release_escrow
      persona: escrow_agent
      outcomes: { released: Terminal(success) }
      on_failure: Compensate(
        steps: [{
          op:         revert_delivery_confirmation
          persona:    escrow_agent
          on_failure: Terminal(failure)
        }]
        then: Terminal(failure)
      )
    }

    step_handoff_compliance: HandoffStep {
      from_persona: escrow_agent
      to_persona:   compliance_officer
      next:         step_compliance_release
    }

    step_compliance_release: OperationStep {
      op:      release_escrow_with_compliance
      persona: compliance_officer
      outcomes: { released: Terminal(success) }
      on_failure: Compensate(
        steps: [{
          op:         revert_delivery_confirmation
          persona:    escrow_agent
          on_failure: Terminal(failure)
        }]
        then: Terminal(failure)
      )
    }
  }
}

Path enumeration for this flow:

PathStepsTerminal
1confirm -> check threshold (true) -> auto release -> successsuccess
2confirm -> check threshold (false) -> handoff -> compliance release -> successsuccess
3confirm -> failurefailure
4confirm -> check threshold (true) -> auto release fails -> compensate -> failurefailure
5confirm -> check threshold (false) -> handoff -> compliance release fails -> compensate -> failurefailure

Every path terminates. The frozen snapshot ensures consistent decisions.

Constraints

  • Step graph must be acyclic. Verified at load time via topological sort.
  • All StepIds referenced must exist in the steps map. Violations: "entry step '<id>' is not declared in steps".
  • All OperationIds referenced must exist in the contract.
  • All PersonaIds must resolve to declared Persona constructs. Violations: "undeclared persona '<id>'".
  • Flow reference graph (SubFlowStep references) must be acyclic across all contract files.
  • Sub-flows inherit the invoking flow's snapshot (E5).
  • OperationStep outcome routing must be exhaustive: the keys of the outcomes map must exactly equal the declared outcome set of the referenced operation. Missing outcomes are Pass 5 errors.
  • Every OperationStep and SubFlowStep must declare a FailureHandler. Violations: "OperationStep must declare a FailureHandler".
  • Compensation failure handlers are Terminal only -- no nested compensation.
  • first_success merge policy is not supported for parallel steps.

Common Mistakes

Assuming verdicts are re-evaluated between steps. The verdict snapshot is frozen at initiation. Mid-flow entity state changes do not cause verdict re-evaluation.

Missing failure handlers. Every OperationStep and SubFlowStep requires on_failure. The elaborator rejects steps without it.

Non-exhaustive outcome handling. If an operation declares outcomes [approved, rejected], the OperationStep must handle both. Missing rejected is a Pass 5 error.

Circular step references. Flows must be DAGs. A step referencing an earlier step creates a cycle, which is rejected.

Handoff without persona. A HandoffStep must name both from_persona and to_persona. Omitting either is an error.

How Flows Connect to Other Constructs

  • Operations are invoked by OperationSteps. The flow routes on operation outcomes.
  • Rules produce the verdicts that BranchSteps and OperationStep preconditions evaluate.
  • Personas control who executes each step and who receives handoffs.
  • Entities are transitioned by operations within the flow. Parallel branches must have disjoint entity effects.
  • Systems can trigger flows across contracts via cross-contract triggers.

Static Analysis Properties

  • S6 (Flow path enumeration) proves every path terminates, enumerates all possible execution paths, and computes all reachable entity states via the flow.
  • S4 (Authority topology) is evaluated in the context of flow steps -- which persona acts at which step.
  • S7 (Evaluation complexity bounds) covers predicate evaluation in BranchSteps and OperationStep preconditions.