OpenWOP openwop.dev
RFC details authors, dates, what it affects, review history
FieldValue
RFC0257
TitleA run in which no node failed and every terminal node was skipped ends completed, and says so on run.completed
StatusActive
Author(s)David Tufts (@davidscotttufts)
Created2026-11-02
Updated2026-11-02 — Draft → Active the same day, comment window waived by the maintainer (confirmed in the openwop-be session, 2026-11-02: "Yes, waive it", after openwop-app-14 relayed the instruction "skip the 7 day comment window and move to active"). STEWARD OVERRIDE of RFC 0147 §A.6 (§A.4 binds replay), recorded in MAINTAINERS.md. UQ 1–8 decided and R1/R6 dispositioned (maintainer-approved); see §Decisions. Lands the runCompleted.skippedTerminals rule, the execution.md §subWorkflow Output line, the EdgeCondition.left resolution pin, v2-all-terminals-skipped and three fixtures in suite 2.46.41. Earlier 2026-11-02 — filed Draft. Registers only; no spec, schema or suite change yet. The RFC 0001 §5 cross-organization approval rule is not yet active (GOVERNANCE.md §"Sole-steward operation").
Affectsschemas/v2/run-event-payloads.schema.json $defs/runCompleted (one optional property; its description is §A's normative home) · spec/v2/core/execution.md §subWorkflow (one Output sentence) · schemas/v2/workflow-definition.schema.json and workflow-chain-pack-manifest.schema.json EdgeCondition.left (description: the resolution pin) · spec/v2/surface-baseline.json (regenerated) · conformance: one new scenario, two new fixtures, one fixture variant · conformance/fixtures.md · CHANGELOG. v1 is frozen and is not touched.
Compatibilityadditive per COMPATIBILITY.md §4 ("new normative requirement on a previously undefined behavior") and §2.4 (an optional property on a closed v2 object). It changes observed behavior on one tier-1 host; see §Compatibility.
Supersedes—
Superseded by—

Summary

The v2 core says a run's log ends in exactly one of run.completed, run.failed or run.cancelled, but never says which one a run gets when it runs out of work. This RFC settles one arm of that question: when no node failed and every terminal node (a node with no outgoing edge) was skipped, the run ends completed, and run.completed carries a new optional skippedTerminals list naming those terminals. A core.subWorkflow parent receives the same list. The decision is a pure function of the pinned definition and the recorded node outcomes, so replay and fork reproduce it. The two steward hosts disagree on this state today (one fails it, one completes it with nothing to say so), and the RFC picks completion because a failed run with no failed node reports a failure that nothing produced.

Motivation

The state

A run reaches it when nothing is ready, running or suspended, no node's latest outcome is node.failed, and no terminal node completed. Two mechanisms produce it, and neither needs a defect:

  1. An edge condition routes nothing (RFC 0134 operators and the original five). A guard declines (guard.ok equals true is false), an "only if" step's truthy edge is false, or an equals router's value matches no branch. Every path to every terminal is skipped.
  2. A trigger rule is never satisfied, with no condition anywhere. A → B where the only edge carries triggerRule: "any_failed" (an error handler). When A succeeds, B is skipped, and B is the only terminal. The workflow did exactly what its author wanted, and nothing was conditioned.

The second case matters for the decision: "every terminal was skipped" does not imply "the author's intent was violated".

What the corpus says today

Nothing normative. Measured at openwop cd40f264:

  • spec/v2/core/events.md §"The terminal event" requires exactly one terminal run event and says nothing about which.
  • spec/v2/core/runs.md lists completed and failed as statuses, with no rule that selects between them except for specific causes (run_timeout, approval_rejected in interrupt.md §Rejection, recursion_limit_exceeded).
  • RFC 0134 and spec/v1/workflow-chain-packs.md (line 169) define how a skipped source evaluates truthy/falsy. They do not define a run status.
  • node.skipped is a registered event (spec/v2/event-codemap.json, payload nodeSkipped), but no v2 rule requires a host to emit it.
  • "Terminal node" is defined only in RFC 0065 (outputRole), informally: "those with no outgoing edge".
  • RFC 0125's motivating case says an all_complete terminal lets a run "complete cleanly", which presumes a completion rule the core never states.

What the hosts do (code reads, not executions, except where noted)

HostEvery terminal skipped, no node failedEvidence
openwop-app (tier 1, production)run.failed, error.code all_terminals_skipped (v2 wire: vendor-namespaced openwop-app.all_terminals_skipped), details.skippedTerminals, no failedNodeId. The failure is dead-lettered and raises a user notification. Before openwop-app #5443 (merged 2026-11-01) it was internal_error "unknown node failure".backend/typescript/src/executor/scheduler.ts inspectDisposition; executor.ts failed branch and emitTerminalFailure (dead letter, notification); run-confirmed on the career.apply chain with a declining guard (test/cs70wf-38-all-terminals-skipped.test.ts)
v2 reference host (openwop-examples f28b78d)run.completed with outputs: {} and no marker. It emits node.skipped for the terminal. It does not evaluate edge condition at run time at all, so it reaches this state only through trigger rules.examples/hosts/v2-reference/src/executor.ts fires, the run loop, runOutputs

openwop-app emits no node.skipped event (its skips are debug log lines only), so on that host the skipped terminals are visible on the wire only in the failure's details.

The two conforming hosts therefore disagree, and a client cannot tell from either wire whether a completed run did its work or a failed run had a failure.

Who hits it

openwop-app's steward write-up (docs/steward/workflows-assessment/2026-10-31-engine-skipped-terminal.md, finding CS70WF-38) lists 9 shipped chain packs whose every terminal is reached only through conditional edges with no complementary pair that guarantees one completes. One (career.apply) is run-confirmed; the other eight come from a static scan and are candidates, not confirmed failures. A further 32 chains are condition-gated but carry a truthy/falsy pair, so one terminal always completes.

On that host each such run currently:

  • reports failed, so it counts in failure rates and alerts;
  • is dead-lettered (runs.md §"Dead letters"), and with a run-level retry policy would be retried first. The retry re-runs every non-terminal node, including side effects that already succeeded, and declines again deterministically;
  • as the child of a core.subWorkflow with the default onChildFailure: fail-parent, fails its parent.

Why the spec is the right place

The terminal event type is wire: childStatus (execution.md §subWorkflow), dead-lettering, cancelRun's run_terminal, retry policy and every client's success test key on it. Two hosts already disagree, and the disagreement is visible to every consumer.

Proposal

Choosing the outcome

Three outcomes were weighed (full trade-offs in §"Alternatives considered"):

ConcernA. completed + marker (proposed)B. failed + registered codeC. author declares it
Replay and forkDeterministic: a function of the definition and recorded outcomesSameSame, if the declaration is read from the pinned definition, never host config
outputs and outputRoleoutputs is an object as today; the marker tells tooling there is no deliverableNo outputs; the code tells tooling whyDepends on the declared branch
core.subWorkflow parentContinues; it can route on childSkippedTerminalsFails by default (fail-parent) on a by-design declineAuthor-dependent, per child
Retries, dead letters, alerts keyed on statusNot retried or dead-lettered; alerts must read the marker to see itRetried, dead-lettered, alerted, including the trigger-rule case where nothing was conditionedAuthor-dependent
Routing defects (a router matches nothing by mistake)Silent unless the marker is readLoudLoud only where the author opted in
Backward compatibilityFlips openwop-app; adds a marker to the reference hostFlips the reference host; openwop-app swaps a vendor code for a registered oneBoth hosts change; new wire surface in two schemas
What it claimsNo node failed, and the run says it completed with no terminalA failure that no node producedWhatever the author declared

The decisive reason: a run status is a claim about the run, and failed here would claim a failure that no node produced. openwop-app had to invent a code and leave failedNodeId empty because there was nothing to name. Each consumer of failed then acts on that missing failure: the dead-letter sink, the default fail-parent, retry policies and alerts. The trigger-rule case (A → B over any_failed) shows that the state is reached by ordinary graphs with no conditions at all, so "every terminal skipped" is not evidence of a broken intent.

The strongest counter-argument: silent success hides routing defects. If a model node renames its output key, every equals edge on the old key goes false, and every run "completes" having done nothing while a dashboard keyed on status stays green. Prior art splits the same way. Airflow, Argo and GitHub Actions treat a run whose remaining steps were skipped by a guard as successful. AWS Step Functions (States.NoChoiceMatched) and BPMN 2.0 (an exclusive gateway with no true condition and no default flow) raise an error. (These descriptions are from the author's knowledge of those systems and were not re-checked against their current documentation for this draft.) In both of the failing systems, though, the error is raised at the routing node, not as a run status with no cause. This RFC keeps that division:

  • a run with no failed node is not a failed run;
  • an author who needs "nothing routed" to be a failure says so at the routing point, with primitives that exist today: a core.fail terminal on the complementary (falsy) edge or on a router's default branch;
  • the marker makes the no-terminal outcome visible to every consumer that wants to alert on it.

Whether to add an author declaration (option C) as well is Unresolved question 1.

Definitions

  • Terminal node. A node of the run's definition, after chain expansion, with no outgoing edge. A node whose only outgoing edges close a cycle is not terminal. A disabled: true node counts like any other node. (RFC 0065's informal definition, made normative.) A definition with no terminal node never reaches this RFC's state.
  • Quiescent. No node of the run is ready, running or suspended, and the host will schedule none without new input.
  • A node's latest outcome is the last of node.completed, node.failed or node.skipped the host recorded for it, or the equivalent internal state on a host that does not emit node.skipped.

§A — Disposition (home: run-event-payloads.schema.json $defs/runCompleted.skippedTerminals)

When a run is quiescent, its definition has at least one terminal node, no terminal node's latest outcome is completed, no node's latest outcome is failed, and no rule that ends the run on its own cause applies (cancellation, run_timeout, a breached cap, recursion_limit_exceeded):

  1. The host MUST end the run with run.completed, and the run's status MUST be completed. It MUST NOT emit run.failed for this state.
  2. run.completed MUST carry skippedTerminals: the id of every terminal node of the definition, each once, in the order the definition's nodes[] lists them.
  3. A run.completed for a run in which at least one terminal node completed MUST NOT carry skippedTerminals.
  4. The disposition MUST be a function only of the run's pinned definition and its recorded node outcomes. It MUST NOT depend on host configuration, the clock, or which host instance evaluates it. A replay fork that reproduces the source's node outcomes MUST end with the same terminal event type and the same skippedTerminals.
  5. outputs remains governed by events.md §Payloads (an object, {} valid). This RFC adds no rule on its contents.

The state where a node failed and every terminal was skipped is not governed by this RFC (see §"Open spec gaps", row 2).

§B — core.subWorkflow parent (spec/v2/core/execution.md §subWorkflow, Output)

When the child's terminal event is run.completed carrying skippedTerminals:

  1. The parent node's node.completed MUST carry outputs.childStatus: "completed" and MUST carry outputs.childSkippedTerminals, equal to the child's skippedTerminals.
  2. onChildFailure does not apply; the child did not fail.
  3. outputMapping applies as for any completed child: a mapped child variable that is unset arrives undefined (the existing rule).

A parent that must stop when its child reached no terminal routes on it, for example { "type": "truthy", "left": "childSkippedTerminals" } to a core.fail node.

§C — Authoring guidance (non-normative)

  • To make a decline an ordinary completion with a deliverable, give the declining branch its own terminal (a falsy complement edge to a core.noop or a node that records the reason). openwop-app's Campaign Studio channel chains did this (its ADR 1020 P1).
  • To make "nothing routed" a failure, add a core.fail terminal on the complement edge or the router's default label.
  • Tooling that uses outputRole (RFC 0065) to pick a primary deliverable should present "no deliverable" when skippedTerminals is present, rather than fall back to another node's output.

Wire shape changes

schemas/v2/run-event-payloads.schema.json $defs/runCompleted, one optional property:

   "properties": {
     "outputs": { "$ref": "#/$defs/_outputsObject" },
     "durationMs": { "type": "integer", "minimum": 0 },
+    "skippedTerminals": {
+      "type": "array",
+      "description": "RFC 0257 §A — present only when the run completed with no terminal node completed and no node failed: every terminal node's id (a node with no outgoing edge), once each, in nodes[] order. Absent when any terminal completed.",
+      "items": { "$ref": "ids.schema.json#/$defs/nodeId" },
+      "minItems": 1,
+      "uniqueItems": true
+    }
   },

outputs.childSkippedTerminals on the core.subWorkflow node needs no schema change: _outputsObject is open and execution.md already lets the node "add fields". The field is named in prose.

No change to RunSnapshot (Unresolved question 3), the error registry, the event registry, api/v2/openapi.yaml or api/v2/asyncapi.yaml (the AsyncAPI message references the payload schema). The CloudEvents and webhook renderings are generated from the same definition and change with it.

Version axes: no eventLogSchemaVersion change. Whether run.completed's per-event schemaVersion changes for an added optional property is Unresolved question 6.

Examples

Positive — a guard declined; the only terminal session was skipped:

{ "type": "run.completed", "payload": { "outputs": {}, "skippedTerminals": ["session"], "durationMs": 412 } }

Positive — a terminal completed, so no marker:

{ "type": "run.completed", "payload": { "outputs": { "payload": { "ok": true } }, "durationMs": 380 } }

Negative — fails validation (minItems: 1):

{ "type": "run.completed", "payload": { "outputs": {}, "skippedTerminals": [] } }

Negative — validates, but violates §A.1 (a failure with no failed node, for this state):

{ "type": "run.failed", "payload": { "error": { "code": "openwop-app.all_terminals_skipped", "message": "…", "details": { "skippedTerminals": ["session"] } } } }

Security considerations

  • Threat models. SECURITY/threat-model-prompt-injection.md: a model or injected content that steers a guard to decline now yields a completed run. A consumer that reads completed as "the action happened" would be wrong. That was already true of a host that completes the run, as the reference host does; the marker makes it detectable. SECURITY/threat-model-replay.md: §A.4 forbids configuration-dependent dispositions, which would otherwise let a replay under a different configuration report a different outcome for the same history.
  • Payload. skippedTerminals carries node ids from the run's definition, which node.* events already carry. No credential, input or output material. No redaction recipe needed.
  • Invariants. None added. The disposition is not a security property, and SECURITY/invariants.yaml is reserved for those.
  • Audit and OTel. No new audit event. v2 core defines no run-status OTel attribute (the v1 openwop.run_status attribute is in the frozen spec/v1/observability.md); whether to add a no-terminal attribute is Unresolved question 5.
  • External audit. No surface the external audit scoped changes (SECURITY/external-audit-engagement.md).
  • Embargo. Not a safety fix; no embargo path.

Open spec gaps (what this RFC does not cover)

  1. The general run-completion predicate. The v2 core never says when a run is completed versus failed beyond named causes. This RFC settles the "no node failed, every terminal skipped" arm only.
  2. A failed node with every terminal skipped. Out of scope. By code read, openwop-app fails with the failed node's error; the reference host completes when the failure was routed over an all_complete/any_failed edge.
  3. Trigger-rule semantics over a skipped source. Not defined at v2. By code read the hosts differ on all_success with one completed and one skipped source (openwop-app fires the target; the reference host skips it). This changes which graphs reach this RFC's state, not the rule.
  4. Edge-condition evaluation is optional at v2. RFC 0134 binds only "a host that evaluates EdgeCondition", and the reference host does not.
  5. node.skipped emission is not required, and openwop-app emits none.
  6. ~~left path resolution~~ — pinned by this RFC (G2): a host that evaluates EdgeCondition resolves left as a dot path into the source node's node.completed outputs; a missing segment is undefined. openwop-app already does this; the reference host does not evaluate conditions.
  7. ~~A definition with no terminal node and disabled: true nodes~~ — decided (UQ 8): the first never reaches this state; a disabled node counts like any other.

Compatibility

Classification: additive.

  • COMPATIBILITY.md §4: "New normative requirement on a previously undefined behavior — Yes, additive (the spec was previously silent)." No v2 text selects a terminal event for this state.
  • §2.4: adding an optional property to the closed runCompleted object is additive. A consumer validating against an older v2 schema version is validating against that version and SHOULD pin it (versioning.md §4). A consumer that validates new events against an old pinned closed schema will reject a run.completed carrying the marker; that is the known cost of v2 closure, accepted by §2.4 and its precedents (RFCs 0183, 0186, 0188).
  • §2.2: no required field changes, no event type changes shape, no endpoint contract changes, no MUST is relaxed. No registered error code changes meaning. openwop-app.all_terminals_skipped is a vendor code the host owns, and it stops being emitted for this state.

Forward-compatibility clauses. skippedTerminals is optional with no default and is absent from every run.completed where a terminal completed. Clients that do not know it ignore it, as events.md requires for unknown members. Hosts that never reach the state never emit it.

Honest caveat. "Additive" is a statement about the corpus, not about users. On openwop-app, runs that end failed today will end completed:

  • failure counts drop;
  • dead letters and failure notifications stop for these runs;
  • a core.subWorkflow parent stops failing on a child that reached no terminal, and continues (Risk R6).

Persisted runs keep their recorded run.failed. A replay fork whose fromSeq precedes the terminal event re-executes under current code, reaches run.completed instead, and emits replay.diverged as replay.md §Divergence requires. That is the documented meaning of replay under current code, not a regression.

On the reference host the status is unchanged; it gains the marker and, under §B, the parent field.

No migration tooling is required (additive). Under GOVERNANCE.md §"Sole-steward operation" there is no outside host to coordinate with; the two steward hosts are listed in §"Implementation notes".

Conformance

Existing coverage

  • v2-run-completed-outputs.test.ts asserts run.completed carries outputs (on conformance-noop).
  • v2-terminal-event-once.test.ts asserts exactly one terminal run event.
  • conformance-approval-reject-routed (with v2-approval-reject-disposition) is the nearest fixture: a terminal is skipped while another completes, and the run completes.
  • edge-condition-truthy-falsy (v1) checks the operator shape. No v2 scenario drives an edge condition at run time.
  • subworkflow.test.ts, v2-subworkflow-linkage.test.ts and v2-sub-workflow-advertisement.test.ts cover core.subWorkflow; none covers a child that reached no terminal.

No existing scenario reaches the state this RFC governs.

New scenario (planned): v2-all-terminals-skipped.test.ts

Targets major 2 (row in conformance/scenario-majors.json). Top-of-file docstring cites spec/v2/core/events.md §"The terminal event" and execution.md §subWorkflow. Each fixture is advertised through discovery fixtures[]; a fixture the host does not list records inapplicable for its leg (the existing fixture convention: a blocked row on a conforming host is a release blocker), never a pass.

LegFixture and inputAsserts
0257.trigger-rule-skip-completesconformance-error-route-only: start (core.noop) → on-fail (core.noop) over triggerRule: "any_failed". No input.run.completed; skippedTerminals deep-equals ["on-fail"]; no run.failed in the log. Needs no condition evaluation, so it runs on both steward hosts.
0257.condition-skip-completesconformance-all-terminals-skipped: check (core.identity) → act (core.noop) over { "type": "truthy", "left": "payload.ok" }. Input payload: { ok: false }.run.completed; skippedTerminals deep-equals ["act"]; no run.failed.
0257.terminal-completed-no-markerSame fixture, input payload: { ok: true }.run.completed without skippedTerminals. This is the positive control: a host that ignores the condition runs act on both inputs and fails the previous leg rather than passing both.
0257.replay-same-dispositionGated on replay advertising modes ⊇ replay. Fork the conformance-error-route-only run with mode: "replay" from its first node.started (no condition evaluation needed, so both steward hosts can run it).The fork ends run.completed with the same skippedTerminals.
0257.subworkflow-child-skippedGated on subWorkflow. conformance-subworkflow-skip-parent → child conformance-error-route-only.Parent run.completed; the parent node's node.completed.outputs.childStatus === "completed" and childSkippedTerminals deep-equals ["on-fail"].

Server-free (0257.shape): runCompleted validates with and without skippedTerminals; an empty list and a repeated id fail.

Shipped 2026-11-02 in suite 2.46.41, with judges in lib/skipped-terminals-witness.ts (five self-tests). Before merge every host leg ran against a fake host: it passes a conforming one, and each sabotage (a run.failed, a missing marker, an ignored condition, a marker beside a completed terminal, a replay that changes the disposition) fails the leg it targets.

Fixtures

New conformance/fixtures/conformance-error-route-only.json, conformance-all-terminals-skipped.json and conformance-subworkflow-skip-parent.json, each with a row in the conformance/fixtures.md catalog table and a per-fixture contract, using only typeIds existing fixtures already use (core.noop, core.identity, core.subWorkflow). The truthy fixture's left: "payload.ok" resolves into core.identity's outputs.payload under the pin above (gap G2, closed).

Capability gating

No new capability flag. The disposition is core run behavior, like the terminal-event rule beside it. Legs gate only on existing facets (replay, subWorkflow) and on fixture advertisement.

Reference-host coverage and INTEROP-MATRIX

  • openwop-app (tier 1) is expected to witness every leg, including 0257.condition-skip-completes, which needs run-time condition evaluation.
  • The v2 reference host can witness 0257.trigger-rule-skip-completes, 0257.replay-same-disposition and 0257.subworkflow-child-skipped once it emits the marker. It cannot witness the condition legs until it evaluates conditions (Open spec gap 4).
  • No new profile or column in INTEROP-MATRIX.md.

Suite vs spec

The legs assert only what §A and §B state. Nothing is stricter than the spec text.

Falsifiability — one row per normative requirement

RequirementObservable — what an outside party seesWho can cause the conditionVerdict
§A.1 ends completed, never run.failedthe terminal event type in the logthe suite, by running an advertised fixture (v2-all-terminals-skipped.test.ts)witnessable — unaided (fixture-advertised; an unlisted fixture is inapplicable)
§A.2 skippedTerminals lists every terminal, once, in nodes[] orderthe run.completed payloadthe suite, same fixtureswitnessable — unaided (fixture-advertised)
§A.3 absent when a terminal completedthe run.completed payload on the ok: true inputthe suitewitnessable — unaided (fixture-advertised)
§A.4 replay reproduces the dispositionthe fork's terminal event and markerthe suite, via forkRunwitnessable — gated (replay)
§A.4 does not depend on host configuration, clock or instance—only the operator can change a host's configurationunwitnessable — the suite cannot vary host configuration; the replay leg witnesses only that a re-derivation agrees with the recorded one
§B.1 parent carries childStatus: "completed" and childSkippedTerminalsthe parent node's node.completed outputsthe suite, via the parent fixturewitnessable — gated (subWorkflow)
G2 left resolves into the source's outputsthe condition legs (ok: false declines, ok: true fires)the suitewitnessable — unaided (fixture-advertised), on a host that evaluates conditions

Alternatives considered

B. Keep failed, register a standard code. Register all_terminals_skipped (422, not retriable, details: { skippedTerminals: nodeId[] }), and require run.failed with no failedNodeId for this state. It is loud: routing defects cannot hide behind a green status. openwop-app already behaves this way under a vendor code, so it would only rename the code. Rejected because:

  • it reports a failure no node produced;
  • it fails the trigger-rule case (A → B over any_failed), where nothing was conditioned and every node did what it was told;
  • it dead-letters, retries and alerts on deterministic outcomes, and a retry re-runs side effects that succeeded;
  • it fails core.subWorkflow parents by default on a child's by-design decline;
  • it flips the reference host the other way.

The loudness it buys is available more precisely at the routing node (core.fail on the complement edge or default branch).

C. An author declaration. For example, WorkflowSettings.onNoTerminal: "complete" | "fail", or a per-terminal "skippable" flag. It states intent exactly where the executor cannot infer it, and is the only option where a multi-branch router with no default can be made fail-closed in one line. Not proposed now because:

  • it adds surface to two closed schemas (the definition and the chain manifest);
  • it gives two ways to say what a core.fail or core.noop terminal already says (overview.md Axiom 2, "one name per thing");
  • it still needs a default, and the default is exactly the A-versus-B question.

It can be added later as an additive optional setting whose default is this RFC's rule (Unresolved question 1).

D. A new run status (skipped, or completed-empty). runs.md lets the status vocabulary grow by overview.md §0. Rejected because:

  • a consumer must accept an unknown status and "MUST NOT act on it", so existing clients would see a terminal run they cannot classify;
  • childStatus (completed | failed | cancelled) and the three terminal event types are closed sets that would all have to grow;
  • cancelRun's run_terminal and isTerminal on poll would need the new member too.

A marker on run.completed carries the same information without that cost.

E. Refuse such graphs at registration. Require every definition to have a terminal that is reachable on every input. Undecidable in general (conditions test run-time values), and a conservative check would refuse the 9 legitimate chains and the any_failed handler pattern. Rejected.

F. Do nothing. Leaves the two steward hosts disagreeing on the terminal event type for the same graph, with no marker on either wire that would let a client tell the difference. Every consumer of childStatus, dead letters and retry policy keeps acting on whichever host it happens to talk to. Rejected; the disagreement is the reason this RFC exists.

Decisions

Decided 2026-11-02 at Active, approved by the maintainer in the openwop-be session.

  1. No author declaration (option C) now. A core.fail terminal at the routing point says it; a setting can be added later, defaulting to this rule.
  2. skippedTerminals, a node id list. Not a token.
  3. No RunSnapshot projection. The marker lives on run.completed only.
  4. outputs stays open (§A.5). openwop-app measured {} for this state (2026-11-02).
  5. No metrics or OTel signal. Operators alert on the marker.
  6. No per-event schemaVersion bump for an added optional property.
  7. Homes. §A lives in the runCompleted.skippedTerminals description; §B is one sentence in execution.md §subWorkflow (core 36,795 / 36,800).
  8. No terminal node: not this state. disabled nodes count like any other.

Risks. R1 (silent success hides routing defects) is accepted: the marker and the core.fail guidance are the mitigations. R6 (parents stop failing on a child that reached no terminal) is accepted: no outside host exists (GOVERNANCE.md §"Sole-steward operation"), and openwop-app measured zero production runs in this state (G10), so no deployed parent changes behavior.

Unresolved questions (as filed)

  1. Add option C now? Should the spec also add an author declaration that makes "no terminal completed" a failure, defaulting to this RFC's rule? The proposal says not yet; reviewers who expect many routers without a default branch may disagree.
  2. Marker name and shape. skippedTerminals (a node id list) or a closed outcome token such as terminalOutcome: "none-completed"? The list is more useful to a person reading a run. The token extends better if partial-skip outcomes are ever reported.
  3. Project it onto RunSnapshot? A client that only reads getRun cannot see the marker. Adding an optional snapshot property is additive, but the snapshot is a fold of the log, and a second copy is a second thing to keep consistent.
  4. outputs contents. Should outputs be {} when no terminal completed? The reference host already emits {}. What openwop-app would put there has not been measured, and the core never defines how outputs is derived.
  5. An OTel or metrics signal. Should a host expose the count of runs that completed with no terminal, so operators can alert on routing defects without reading payloads? v2 core has no run-status attribute to extend.
  6. Per-event schemaVersion. Does adding an optional payload property bump run.completed's schemaVersion? No precedent was found in this draft.
  7. Core word budget. spec/v2/core is at 36,784 of 36,800 words (scripts/check-core-budget.mjs). §A and §B need roughly 120 words. Which words are freed, or does the rule live outside core/?
  8. A definition with no terminal node, and disabled nodes. Should a definition with no terminal node be refused at registration? Is a disabled: true node terminal-eligible?

Implementation notes (non-normative)

  • openwop-app. scheduler.ts inspectDisposition: the skippedTerminals branch returns completed instead of failed, and executor.ts's completion path stamps skippedTerminals on run.completed. The all_terminals_skipped failure branch and its vendor-code mapping are deleted. Note that scheduler.ts today also returns failed for a quiescent run where nothing completed and some node failed (Open spec gap 2), which this RFC leaves alone. subWorkflowDispatcher.ts adds childSkippedTerminals. Its cs70wf-38-all-terminals-skipped.test.ts flips its expectation. Before the flip, measure how many production runs per day end all_terminals_skipped, and which chains produce them, so the change to failure counts is expected rather than discovered.
  • v2 reference host. executor.ts computes the terminal set (nodes not in links(def).map(from), the same set runOutputs uses) and adds skippedTerminals when none has a node.completed. Evaluating edge conditions at run time is a separate change (Open spec gap 4).
  • Sequencing. Spec text and schema → scenario and fixtures (suite minor) → both hosts → certified cuts. The core budget question (UQ 7) must be answered before the spec text can merge.
  • Effort. Small on both hosts. The conformance scenario is the largest piece.
  • SDKs. openwop-sdks regenerates the RunCompleted payload type with the optional field.

Acceptance criteria

  • [x] Spec text merged (the runCompleted.skippedTerminals description, execution.md §subWorkflow) within the core word budget. (2026-11-02.)
  • [x] schemas/v2/run-event-payloads.schema.json $defs/runCompleted.skippedTerminals added; spec/v2/surface-baseline.json regenerated; check-v2-surface-monotone.mjs passes.
  • [x] v2-all-terminals-skipped.test.ts with its three fixtures and catalog rows, in a suite release (2.46.41).
  • [x] CHANGELOG entries (corpus and conformance/CHANGELOG.md).
  • [ ] openwop-app passes every leg on a certified production cut, including 0257.condition-skip-completes and its positive control.
  • [ ] The v2 reference host passes 0257.trigger-rule-skip-completes, or the RFC records why it does not.
  • [ ] Register sweep: every gap and risk row closed, transferred or carried.

References

  • openwop-app docs/steward/workflows-assessment/2026-10-31-engine-skipped-terminal.md (finding CS70WF-38), openwop-app #5443 (all_terminals_skipped), backend/typescript/test/cs70wf-38-all-terminals-skipped.test.ts.
  • openwop-app backend/typescript/src/executor/scheduler.ts (inspectDisposition, evaluateTrigger), executor.ts (failed branch, emitTerminalFailure), middleware/protocolVersion.ts (v2EventErrorCode), at fc536023f.
  • openwop-examples examples/hosts/v2-reference/src/executor.ts (fires, the run loop, runOutputs), at f28b78d.
  • RFC 0013 (workflow-chain packs), RFC 0053 (dead letters), RFC 0065 (outputRole, terminal node), RFC 0125 (triggerRule on fragment edges), RFC 0133 (sub-chains), RFC 0134 (truthy/falsy), RFC 0194 (terminal event ends forward execution), RFC 0213 (stating outcomes the core left unstated), RFC 0223 (approval rejection disposition).
  • Spec: spec/v2/core/events.md, runs.md, execution.md, replay.md, interrupt.md §Rejection, workflow-chain-packs.md, errors.md; spec/v1/workflow-chain-packs.md (frozen).
  • Prior art (author's knowledge, not re-verified for this draft): Apache Airflow DAG-run state with skipped leaves (ShortCircuitOperator); Argo Workflows when; GitHub Actions job if; AWS Step Functions Choice / States.NoChoiceMatched; BPMN 2.0 exclusive gateway default flow.
  • Registers: registers/0257-run-status-when-every-terminal-is-skipped.gaps.md, registers/0257-run-status-when-every-terminal-is-skipped.risks.md.

More proposals.

Edit this page on GitHub ↗