Filed by the domain:services execution seat from a tier contract review of PR #14948 (#14419), which found it. Unassigned; domain:*, type and priority are triage's — this seat does not produce them.
Named reader: the domain:spec execution seat. It lands in packages/spec, which is single-owner, so this card exists rather than a rider on #14948.
The divergence
packages/spec/src/automation/control-flow.zod.ts declares TryCatchErrorValueSchema — by its own docblock, "the value a try_catch binds to errorVariable … declared here so the catch region's author, the engine and the run log share ONE shape" — with keys nodeId, message, iteration, item.
PR #14948 (#14419) makes the engine bind a fifth key, code, carrying the platform's classified error code so a flow's try_catch can tell "the row is already there" from "the store is down". That is the whole point of the card and it is ruled.
Measured by the reviewer on built spec: TryCatchErrorValueSchema.parse({ nodeId, message, code: 'DUPLICATE_RECORD' }) strips code — it is a plain z.object. So the declared shared shape no longer describes what the engine binds, and the schema actively discards the key if anything round-trips through it.
Where it surfaces
⛔ #14948 does not touch any of the above. Per that card's disposition it declares the gap in its changeset and names this card, and updates only the hand-written content/docs/automation/flows.mdx row ({$error.nodeId} / {$error.message} → plus {$error.code}).
Suggested shape (for triage and the spec seat, not a ruling)
Add code: z.string().optional().describe(…) to TryCatchErrorValueSchema, extend the errorVariable .describe() so the authorable surface documents it, and regenerate the derived artefacts (gen:authorable-surface-base, the json-schema manifest, and build-docs for control-flow.mdx). A @objectstack/spec changeset is owed.
⚠️ string, not a closed enum — and this is the part worth not getting wrong. ADR-0112 D3/D4 (with the #9106 amendment) makes the code vocabulary StandardErrorCode ∪ registered ledger codes ∪ tenant-authored codes, and NodeExecutor is third-party-registrable, so a closed type would be a lie the moment anyone registers an executor. The same reasoning is why #14948 typed the field string rather than StandardErrorCode. StandardErrorCode | (string & {}) is an option purely for IDE hints, not for narrowing.
Why the mechanical Clause-② rule applies here too
The reading that made #14948 Clause-② yes — a new payload key on a published surface — applies identically to adding code here. Expect Clause-② yes and a contract review on the spec-side PR.
Refs: PR #14948 / #14419 (the engine half, and the review that found this) · ADR-0112 (the code vocabulary) · #9106 (its amendment)
Generated by Claude Code
Filed by the
domain:servicesexecution seat from a tier contract review of PR #14948 (#14419), which found it. Unassigned;domain:*, type and priority are triage's — this seat does not produce them.Named reader: the
domain:specexecution seat. It lands inpackages/spec, which is single-owner, so this card exists rather than a rider on #14948.The divergence
packages/spec/src/automation/control-flow.zod.tsdeclaresTryCatchErrorValueSchema— by its own docblock, "the value atry_catchbinds toerrorVariable… declared here so the catch region's author, the engine and the run log share ONE shape" — with keysnodeId,message,iteration,item.PR #14948 (#14419) makes the engine bind a fifth key,
code, carrying the platform's classified error code so a flow'stry_catchcan tell "the row is already there" from "the store is down". That is the whole point of the card and it is ruled.Measured by the reviewer on built spec:
TryCatchErrorValueSchema.parse({ nodeId, message, code: 'DUPLICATE_RECORD' })stripscode— it is a plainz.object. So the declared shared shape no longer describes what the engine binds, and the schema actively discards the key if anything round-trips through it.Where it surfaces
packages/spec/authorable-surface/automation.jsonandapi-surface/automation.jsonboth list the schema.content/docs/references/automation/control-flow.mdxrenders it (auto-generated from the describe — it moves only when the spec moves, which is why this card exists and why fix(automation): create_record surfaces the engine's DUPLICATE_RECORD code #14948 cannot fix it).⛔ #14948 does not touch any of the above. Per that card's disposition it declares the gap in its changeset and names this card, and updates only the hand-written
content/docs/automation/flows.mdxrow ({$error.nodeId}/{$error.message}→ plus{$error.code}).Suggested shape (for triage and the spec seat, not a ruling)
Add
code: z.string().optional().describe(…)toTryCatchErrorValueSchema, extend theerrorVariable.describe()so the authorable surface documents it, and regenerate the derived artefacts (gen:authorable-surface-base, the json-schema manifest, andbuild-docsforcontrol-flow.mdx). A@objectstack/specchangeset is owed.string, not a closed enum — and this is the part worth not getting wrong. ADR-0112 D3/D4 (with the #9106 amendment) makes the code vocabularyStandardErrorCode ∪ registered ledger codes ∪ tenant-authored codes, andNodeExecutoris third-party-registrable, so a closed type would be a lie the moment anyone registers an executor. The same reasoning is why #14948 typed the fieldstringrather thanStandardErrorCode.StandardErrorCode | (string & {})is an option purely for IDE hints, not for narrowing.Why the mechanical Clause-② rule applies here too
The reading that made #14948
Clause-② yes— a new payload key on a published surface — applies identically to addingcodehere. ExpectClause-② yesand a contract review on the spec-side PR.Refs: PR #14948 / #14419 (the engine half, and the review that found this) · ADR-0112 (the code vocabulary) · #9106 (its amendment)
Generated by Claude Code