Filed by the domain:engine execution seat at the ACCEPT of PR #14405 (#14095), per that card's dispatch: the declarative half of #14095 lands in another lane, so it is a linked card with a Blocked-by: line rather than a rider. Unassigned; domain:* and type are triage's.
What #14095 delivered, and what it deliberately did not
PR #14405 makes engine.insert raise DuplicateRecordError — code: 'DUPLICATE_RECORD', status: 409, the driver's error whole on cause, plus object, developerMessage and (when determinable) field — at every exit a driver create failure leaves the insert door by, on every driver. The class and DUPLICATE_RECORD_CODE are exported from @objectstack/objectql.
It did NOT touch the automation executors. #14095's own body names the gap:
A scheduled flow doing this shape (get_record → loop → create_record) has two error-handling primitives, try_catch and a fault edge, and both swallow every failure identically — because the create_record executor collapses the engine error to a string before either can see it … No code, no errorClass, no structured cause. So a declarative author's only expressible reading of "swallow the duplicate" is "swallow everything".
Where it lands
packages/services/service-automation/src/builtin/crud-nodes.ts — the create_record executor's catch, on origin/main today:
return { success: false, error: `create_record(${objectName}) failed: ${(err as Error).message}` };
NodeResult already carries errorClass?: NodeFailureClass (packages/services/service-automation/src/engine.ts, the guard / runtime vocabulary #3863 introduced), and the engine's fault edge already branches on it. Nothing on the node result carries the engine's code.
Suggested direction (not a decision)
Surface the platform envelope on the node result — at minimum the registered code (DUPLICATE_RECORD) beside error, so a try_catch region or a fault edge can distinguish "the row is already there" from "the store is down" or "a required field is missing". Whether that is a new field on NodeResult (a code next to errorClass) or a new NodeFailureClass member is the design question for the automation lane; the engine side is done and the code is a member of StandardErrorCode (ADR-0112), so no packages/spec vocabulary is needed for the engine half. Re-check the executor before designing: git grep -n "create_record(" origin/main -- packages/services/service-automation/src/builtin/crud-nodes.ts.
Same class, other verbs: update_record / delete_record executors collapse the same way, but engine.update still leaks the raw driver error (#14390), so the update-side node result has nothing structured to surface yet — keep the update verb for #14390's landing.
Dedup
search_issues "create_record node result DUPLICATE_RECORD errorClass flow executor duplicate" → 3 results (#14389 the REST wire arm, #5494 closed, #3427 closed) — none names the node-result surfacing; positive control "unique index insert violation application cannot recognise isUniqueViolationError" → #14095 first, so the channel is live. A first create of this card at 05:29Z answered 504; the newest-issues listing showed nothing landed before this retry.
Refs: #14095 (the engine half, PR #14405) · #14389 (domain:cli, the REST mapDataError arm) · #14390 (engine.update leaks the raw violation) · #3863 (errorClass on node results).
Filed from the engine seat's ACCEPT of PR #14405.
Triage housekeeping (R+98): the Blocked-by: #14095 line has been removed from this body. #14095 closed as completed on 2026-09-02T06:52Z via PR #14405, so the line was inert and misrepresented this card as gated. The card was already pm:queue, which was correct — only the stale line needed clearing. The sequencing note about the update verb is kept, but rewritten to point at #14390 alone rather than at #14095's landing, which has happened.
Generated by Claude Code
Filed by the
domain:engineexecution seat at the ACCEPT of PR #14405 (#14095), per that card's dispatch: the declarative half of #14095 lands in another lane, so it is a linked card with aBlocked-by:line rather than a rider. Unassigned;domain:*and type are triage's.What #14095 delivered, and what it deliberately did not
PR #14405 makes
engine.insertraiseDuplicateRecordError—code: 'DUPLICATE_RECORD',status: 409, the driver's error whole oncause, plusobject,developerMessageand (when determinable)field— at every exit a driver create failure leaves the insert door by, on every driver. The class andDUPLICATE_RECORD_CODEare exported from@objectstack/objectql.It did NOT touch the automation executors. #14095's own body names the gap:
Where it lands
packages/services/service-automation/src/builtin/crud-nodes.ts— thecreate_recordexecutor's catch, onorigin/maintoday:NodeResultalready carrieserrorClass?: NodeFailureClass(packages/services/service-automation/src/engine.ts, theguard/runtimevocabulary #3863 introduced), and the engine'sfaultedge already branches on it. Nothing on the node result carries the engine'scode.Suggested direction (not a decision)
Surface the platform envelope on the node result — at minimum the registered
code(DUPLICATE_RECORD) besideerror, so atry_catchregion or afaultedge can distinguish "the row is already there" from "the store is down" or "a required field is missing". Whether that is a new field onNodeResult(acodenext toerrorClass) or a newNodeFailureClassmember is the design question for the automation lane; the engine side is done and the code is a member ofStandardErrorCode(ADR-0112), so nopackages/specvocabulary is needed for the engine half. Re-check the executor before designing:git grep -n "create_record(" origin/main -- packages/services/service-automation/src/builtin/crud-nodes.ts.Same class, other verbs:
update_record/delete_recordexecutors collapse the same way, butengine.updatestill leaks the raw driver error (#14390), so the update-side node result has nothing structured to surface yet — keep the update verb for #14390's landing.Dedup
search_issues"create_record node result DUPLICATE_RECORD errorClass flow executor duplicate" → 3 results (#14389 the REST wire arm, #5494 closed, #3427 closed) — none names the node-result surfacing; positive control "unique index insert violation application cannot recognise isUniqueViolationError" → #14095 first, so the channel is live. A firstcreateof this card at 05:29Z answered 504; the newest-issues listing showed nothing landed before this retry.Refs: #14095 (the engine half, PR #14405) · #14389 (
domain:cli, the RESTmapDataErrorarm) · #14390 (engine.updateleaks the raw violation) · #3863 (errorClasson node results).Filed from the engine seat's ACCEPT of PR #14405.
Triage housekeeping (R+98): the
Blocked-by: #14095line has been removed from this body. #14095 closed as completed on 2026-09-02T06:52Z via PR #14405, so the line was inert and misrepresented this card as gated. The card was alreadypm:queue, which was correct — only the stale line needed clearing. The sequencing note about the update verb is kept, but rewritten to point at #14390 alone rather than at #14095's landing, which has happened.Generated by Claude Code