5. Execution Semantics#
This chapter defines what a conforming runtime must do. Where it is silent, implementations are free — and are expected to differ.
5.1 The execution instance#
Executing a harness creates an execution instance: an identity, a state for every node, and a set of resolved port values.
A runtime MUST NOT execute a document that fails validation (chapter 13). Validation is a gate, not advice.
5.2 Starting#
Execution begins by moving the entry set to READY:
- if
@entryis present, that single node; - otherwise, every node with no incoming edge of any type.
A node reachable only by an error or compensation edge is a handler, not a start — see §2.5.
5.3 Readiness#
A node in PENDING becomes READY when its join condition over incoming edges is satisfied. Only control, data and dependency edges participate.
An incoming edge is satisfied when:
| edge type | satisfied when the source is |
|---|---|
control | SUCCEEDED or SKIPPED |
data | SUCCEEDED or SKIPPED, and the bound value is available |
dependency | in any terminal state — including FAILED and CANCELLED |
An edge carrying a condition that evaluates false is not satisfied, and never will be. It is resolved-negative: it neither blocks forever nor counts toward a join.
5.3.1 Join policies#
joinPolicy on the target node decides how satisfied edges combine:
| policy | ready when |
|---|---|
all (default) | every incoming edge is satisfied or resolved-negative, and at least one is satisfied |
any | the first incoming edge is satisfied |
quorum | at least @quorum incoming edges are satisfied |
quorum is required when joinPolicy="quorum", and MUST NOT exceed the number of incoming edges (HX-2401, HX-2402).
The "at least one satisfied" clause on all matters: if every incoming edge resolved negative, no path reached the node, and it remains PENDING (§6.4).
Not reached is not the same as skipped.
SKIPPEDmeans reached, and its guard was false — a terminal success whose control successors still run.PENDINGmeans never reached, so its successors are not reached either. An earlier draft of this section said an all-negative join yieldedSKIPPED, which contradicted §6.4 and made the untaken branch of every decision look like it had run and succeeded — with its downstream consumers then failingHX-4101for a value nobody ever intended to produce.
5.3.2 Cancellation under any#
When a node with joinPolicy="any" becomes ready, sibling branches still running that could only have reached this node MUST be moved to CANCELLED.
A runtime MUST NOT cancel a node that is RUNNING and declared idempotent="false" — it must let it finish and then discard the result. A half-completed non-idempotent action is exactly the state the format exists to avoid: interrupting a payment mid-flight leaves a question nobody can answer.
5.3.3 Unreachability propagates#
A node that no path will reach makes its own outgoing edges resolved-negative, recursively.
Without this rule a join downstream of a branch that was not taken waits forever: the edge from the untaken branch is neither satisfied nor negative, so an all join never resolves and the instance deadlocks. A runtime MUST propagate unreachability so that such a join can resolve on the branch that was taken.
A node is unreachable when any of the following holds:
- it is named by a
case/@toand every decision that could route to it hasrouted elsewhere;
- every incoming forward edge is resolved-negative;
- an incoming data edge feeding a required input with no
defaultcomes from an unreachable source — the node can never obtain a value it declared it needs.
5.4 Scheduling#
A runtime MAY execute any READY nodes in any order, and any number of them concurrently — subject to:
- Loop concurrency. A loop's
maxConcurrencybounds concurrent iterations. - Declared exclusivity. A vendor extension MAY constrain further; the core
specification has no global concurrency control.
- Determinism where it is observable. Decision cases evaluate in document
order; loop iterations with
maxConcurrency="1"execute in sequence order.
Everything else is an implementation decision: which ready node to pick, how to distribute work, how to recover from a crash. Runtimes should compete here.
A workflow whose result depends on which ready node ran first has a race, and the specification will not paper over it. If ordering matters, express it with an edge.
5.5 Executing a node#
For a node moving from READY to RUNNING:
- Evaluate the guard, if present. If false, the node becomes
SKIPPED—a terminal successful state — and its control successors proceed.
- Resolve inputs. Each input takes the value from its incoming data edge, or
from
value, or fromdefault. A required input with no value is a runtime error (HX-4101). - Resolve resources. Each
resourceRefis resolved, credentials fetchedfrom their declared store. Failure to resolve is
HX-4102. - Execute via
impl, subject to<timeout>. - Bind outputs. Declared outputs become available to outgoing data edges.
- Transition to
SUCCEEDEDorFAILED.
Steps 1–3 happen before execution begins, so that a node which cannot run fails without side effects.
5.6 Data availability#
An output value is available to downstream nodes once its producer reaches SUCCEEDED.
If a producer is SKIPPED, its outputs are unavailable. A downstream required input bound to an unavailable output is a runtime error (HX-4101) unless the input declares a default.
This catches a real and common design mistake: guarding a node while leaving a required downstream consumer wired to its output. The document is statically valid; the workflow is wrong. A validator SHOULD warn about the pattern, and the runtime MUST fail rather than pass a null downstream — silently substituting null is how a workflow proceeds with data nobody produced.
Values are immutable once bound. There is no shared mutable state; a value travels along an edge and cannot be modified in place by its consumer.
5.7 Completion#
An execution instance is complete when no node is PENDING, READY, RUNNING or RETRYING.
| outcome | when |
|---|---|
| succeeded | every reached node is SUCCEEDED or SKIPPED |
| failed | at least one node is FAILED with no outgoing error edge |
| compensated | a failure triggered unwinding and compensation completed |
| cancelled | terminated externally |
Nodes never reached remain PENDING at completion. That is normal, not an error: a decision took one branch, so the other branch's nodes were never reached.
5.8 Failure propagation#
When a node reaches FAILED and its retry policy is exhausted:
- If it has outgoing
erroredges, all are traversed. The failure ishandled and the workflow continues.
- If it has none, the failure propagates: the runtime begins unwinding, and
compensation runs per chapter 8.
5.9 What is deliberately unspecified#
Naming these prevents an implementation from assuming a guarantee that was never made:
Persistence and recovery. What happens when the runtime process dies mid-execution is out of scope in v1.0. Implementations differ enormously here and none of it changes what a document means. It is the question implementers ask most, and a candidate for a future version — see the roadmap.
Distribution. Whether nodes run in one process, many, or across machines.
Queueing and prioritisation. Which ready node runs first when capacity is limited.
Observability format. Traces are RECOMMENDED and their content is constrained by chapter 12, but the wire format is an implementation choice.
Resource pooling. Whether two nodes referencing the same resource share a connection.