Each node row is written by a single writer (the orchestration that owns it). We keep changes minimal, and we never materialize implicit states (skipped, cancelled): they are derived at read time. This avoids extra writes, write contention, and conflict resolution.
An execution_id is the sub-orchestration path from the root to the orchestration that last wrote a node, with an execution number per segment:
root:3:loopA:4:branchC:1
- A segment is added only at a sub-orchestration boundary — a loop iteration, or a join/race branch. Plain nodes (SQL, IF, sequencing, …) inherit their owning orchestration's
execution_id; the path is therefore coarser than the full graph. - Execution numbers: 1 for join/race branches; 1..N for loop iterations (one per
continue_as_new); the root counts root-level loop iterations. - Two
execution_ids that can ever write the same node share identical segment identities and length — they differ only in the numbers. This makes them totally ordered: at the first segment whose number differs, the larger number is more recent (“supersedes”).
Add status_detail to df.nodes. On every status write, the owning orchestration stamps the node's current execution_id there.
(Incidental cleanup, not core to this proposal: status_detail lets us drop the unused error column and stop overloading result for non-result data.)
The status update becomes a fenced conditional write: apply only if the incoming execution_id is not superseded by the one already stored (incoming >= stored). A stale writer — e.g. a race loser still draining, or a previous loop iteration — cannot clobber a newer execution's state, and the row converges to the most-recent execution regardless of arrival order. The write stays single-writer-effective and order-independent; the orchestration never branches on whether the write landed.
With status + status_detail, plus what we already store (the IF decision in result, and the race winner on the race node), a single SELECT of an instance's nodes supports both directions:
- Leaf → root: climb a node's ancestors; the first ancestor whose
execution_idis not a prefix of the node's tells you the node's effective state (ancestor failed ⇒ skipped; running ⇒ pending in a new execution; completed ⇒ branch/iteration not taken ⇒ skipped). - Root → leaves: descend carrying the live lineage; decisions at IF/RACE/LOOP paint whole sub-graphs at once (not-taken ⇒ skipped, race loser ⇒ cancelled, under-failure ⇒ skipped, unreached ⇒ pending).
Intuitively: structure tells you what could run, and execution_id tells you which execution each node's status belongs to — together enough to color the current state of every sub-graph, with no materialized transitions.
- Loop boundary: requires loops to run as a sub-orchestration rooted at the loop node so iterations get a clean
execution_idsegment — see separate note. - Race winner: the root→leaves walk needs the race node to record its winning branch (small add if not already present).
- Representation & upgrade:
execution_idordering can be stored in a comparable form (implementation detail);status_detailis a new nullable column and the fencedUPDATEis compatible with pre-existing rows.