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.)

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