Skip to content

The fallbackNodeId tombstone prescribes condition: 'fault' — the migration it hands authors does not draw a fault edge #6094

Description

@os-zhuang

Found by the triage seat while re-checking the residuals on #3863 (findings triage round, 2026-08-07). Filed unassigned. Baseline: origin/main @ 1eb13a0.

Fact

packages/spec/src/automation/flow.zod.ts:715 — the retiredKey() tombstone for flow.errorHandling.fallbackNodeId tells the author how to migrate:

flow.errorHandling.fallbackNodeId was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — the engine routes unrecoverable node errors via per-node fault edges (an edge with condition 'fault'), and never read this key: a fallback configured here silently did not exist. Delete the key and draw a fault edge from the failing node to the handler node instead.

The fault semantic does not live in condition. It lives in type:

flow.zod.ts:440   type: z.enum(['default', 'fault', 'conditional', 'back']).default('default')
flow.zod.ts:438   condition: ExpressionInputSchema.optional()  // CEL predicate

Why this bites, and which direction it fails

An author who follows the prescription literally writes { source, target, condition: 'fault' }. That edge:

  1. parses cleantype is .default('default'), so omitting it is legal, and condition accepts any expression input. No error is raised at publish time;
  2. is a normal edge, not a fault edge — executeNode routes failures on type === 'fault', so the handler is never reached on failure;
  3. carries 'fault' as a CEL predicate — a bare identifier where a boolean was expected, so the edge is not reliably traversed on the success path either.

Net: the author deletes a key that "silently did not exist", follows the prescribed replacement, and lands a second thing that silently does not exist. The tombstone exists precisely to stop that, which is why this is worth a one-line fix rather than a note.

This is fail-open in the authoring sense (nothing rejects it), though not a data-safety hole: an un-drawn fault edge means failures keep aborting the run, which is the safe default. No stored data is at risk; no current user is known to have hit it.

The repo already knows this exact mistake by name

  • content/docs/automation/flows.mdx:869 — "type: 'fault' is what routes — a label is not."
  • content/docs/releases/v17.mdx:1365 names the failure mode in the release notes: "label: 'error' written where type: 'fault' was meant".

So a documented, named author error is being prescribed by the spec's own migration message — in a different slot (condition instead of label), but the same shape. #3881 added the docs half; the tombstone was not swept with it.

Scope — deliberately one line

flow.zod.ts:715: an edge with condition 'fault'an edge with type: 'fault'.

⛔ Do not widen this into an errorHandling prose pass, and ⛔ do not touch the fallbackNodeId retirement itself — the retirement is correct and #3896 is closed. The trailing sentence ("draw a fault edge from the failing node to the handler node") is already right and stays.

Verification notes for whoever takes it

  • :715 is the only occurrence of the wrong spelling repo-widegit grep "condition 'fault'" origin/main returns exactly this one line. Positive control that the scan surface works: type: 'fault' hits flows.mdx:864, bpmn-mapping.ts:269, flow.test.ts:1354/:1378.
  • The string is not mirrored into any generated doc under content/docs today (git grep "an edge with condition" hits only the source line), so this is a single-file change. Whether the reference generator will pick it up later is [docs-gen] 生成的 reference 把 retiredKey() 墓碑渲染成 any —— 嵌套两层时连 [REMOVED] 处方都没有,退役键读起来像自由槽 #5606's territory, not this issue's — that issue is about tombstones rendering as any, a different defect on the same machinery, and the two do not collide (it changes the renderer, this changes one string).
  • Acceptance is stateable in one sentence: the tombstone names the slot that actually routes.

Related: #3863 (fault-edge residuals — where this was found), #3881 (fault-edge v1 + docs), #3896 (the retirement this tombstone records), #5606 (tombstone rendering, distinct).

本单来自分诊座位 Routine(#5474 试点),不构成认领。 (Filed by the triage-seat Routine; not a claim. English per the language policy effective with #6029.)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions