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
flow <FlowId> {
snapshot: at_initiation
entry: <StepId>
steps: {
<StepId>: <StepType> {
...
}
...
}
}Fields
| Field | Required | Description |
|---|---|---|
snapshot | Yes | Always at_initiation in v1.0. The verdict set is frozen at flow start. |
entry | Yes | The step where execution begins. Must exist in the steps map. |
steps | Yes | A map of step ids to step definitions. |
The Six Step Types
1. OperationStep
Executes a declared operation and routes on the outcome:
step_confirm: OperationStep {
op: confirm_delivery
persona: seller
outcomes: {
confirmed: step_check_threshold
}
on_failure: Terminate(outcome: failure)
}| Field | Required | Description |
|---|---|---|
op | Yes | References a declared operation by id. |
persona | Yes | The persona executing the operation. Must be in the operation's allowed_personas. |
outcomes | Yes | Map from outcome labels to next steps or terminals. Keys must exactly match the operation's declared outcomes (exhaustive). |
on_failure | Yes | A FailureHandler (Terminate, Compensate, or Escalate). |
2. BranchStep
Evaluates a predicate against the frozen snapshot and routes on the result:
step_check_threshold: BranchStep {
condition: verdict_present(within_threshold)
persona: escrow_agent
if_true: step_auto_release
if_false: step_handoff_compliance
}| Field | Required | Description |
|---|---|---|
condition | Yes | A predicate expression evaluated against the frozen snapshot. |
persona | Yes | The persona evaluating the condition. |
if_true | Yes | Next step or terminal when the condition is true. |
if_false | Yes | Next 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:
step_handoff_compliance: HandoffStep {
from_persona: escrow_agent
to_persona: compliance_officer
next: step_compliance_release
}| Field | Required | Description |
|---|---|---|
from_persona | Yes | The persona giving up control. |
to_persona | Yes | The persona receiving control. |
next | Yes | The step that executes under the new persona. |
4. SubFlowStep
Invokes another flow as a nested step:
step_run_appeal: SubFlowStep {
flow: appeal_process
persona: appeals_board
on_success: step_overturn
on_failure: Terminate(outcome: appeal_denied)
}| Field | Required | Description |
|---|---|---|
flow | Yes | References another declared flow by id. |
persona | Yes | The persona initiating the sub-flow. |
on_success | Yes | Next step or terminal on sub-flow success. |
on_failure | Yes | A 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.
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:
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:
on_failure: Terminate(outcome: failure)Compensate
Runs compensation operations to undo partial work, then terminates:
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:
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 flowsEvery 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:
// --- 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:
| Path | Steps | Terminal |
|---|---|---|
| 1 | confirm -> check threshold (true) -> auto release -> success | success |
| 2 | confirm -> check threshold (false) -> handoff -> compliance release -> success | success |
| 3 | confirm -> failure | failure |
| 4 | confirm -> check threshold (true) -> auto release fails -> compensate -> failure | failure |
| 5 | confirm -> check threshold (false) -> handoff -> compliance release fails -> compensate -> failure | failure |
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
outcomesmap 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_successmerge 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.