From 88c25b4645220010b24551a9bad9698f592dc7a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 01:47:10 +0000 Subject: [PATCH 1/3] =?UTF-8?q?feat(spec):=20FLOW=5FFAILED=20carries=20the?= =?UTF-8?q?=20refused=20write's=20inner=20refusal=20=E2=80=94=20Automation?= =?UTF-8?q?Result.userMessage/innerRefusal=20and=20FlowFailureDetailsSchem?= =?UTF-8?q?a?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ruling B' on #24914 (decision batch #379 item 5). Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude --- .../spec/src/api/automation-api.zod.test.ts | 121 +++++++++++++++- packages/spec/src/api/automation-api.zod.ts | 136 ++++++++++++++++++ .../spec/src/contracts/automation-service.ts | 31 ++++ 3 files changed, 287 insertions(+), 1 deletion(-) diff --git a/packages/spec/src/api/automation-api.zod.test.ts b/packages/spec/src/api/automation-api.zod.test.ts index 8d68ce41e1..42e87f0a03 100644 --- a/packages/spec/src/api/automation-api.zod.test.ts +++ b/packages/spec/src/api/automation-api.zod.test.ts @@ -21,9 +21,12 @@ import { AutomationApiErrorCode, AutomationApiContracts, ResumeFailureDetailsSchema, + FlowFailureDetailsSchema, + FlowInnerRefusalSchema, } from './automation-api.zod'; +import { BaseResponseSchema } from './contract.zod'; import * as AutomationApiModule from './automation-api.zod'; -import type { TriggerFlowResponse, ResumeFailureDetails } from './automation-api.zod'; +import type { TriggerFlowResponse, ResumeFailureDetails, FlowFailureDetails } from './automation-api.zod'; import { ExecutionStatus } from '../automation/execution.zod'; import type { AutomationResult } from '../contracts/automation-service'; @@ -50,6 +53,18 @@ type Assert< T extends true > = T; */ export type TriggerFlowDataMatchesContract = Assert< Eq< TriggerFlowResponse['data'], AutomationResult > >; +/** + * #24996 (ruling B′ on #24914) — the inner refusal is declared ONCE: the + * member every door's `400 FLOW_FAILED` details carry is the very type + * `AutomationResult` carries, so a producer relaying one into the other + * cannot drift. Narrow either side alone and this alias reds by name. + * Exported for the same reason as the pin above (TS6196 otherwise). + */ +export type FlowFailureInnerRefusalIsTheContracts = Assert< Eq< + FlowFailureDetails['innerRefusal'], + AutomationResult['innerRefusal'] +> >; + /** * #15221 — the resume door's `400 FLOW_FAILED` details forward the ENGINE's * verdict, so the `status` the details schema declares must be exactly the @@ -529,6 +544,110 @@ describe('ResumeFailureDetailsSchema', () => { }); }); +// ========================================== +// Flow failure details + inner refusal (#24996, ruling B′ on #24914) +// ========================================== + +describe('FlowFailureDetailsSchema — one inner-refusal shape across the four FLOW_FAILED doors', () => { + const INNER = { + code: 'UNIQUE_VIOLATION', + message: "Duplicate record refused on 'crm_account': a record with this name already exists.", + } as const; + const USER_MESSAGE = 'An account with this name already exists.'; + const SUMMARY = { + selected: 0, acted: 0, skipped: 0, unmeasured: 0, + nodes: [{ nodeId: 'create_account', nodeType: 'create_record', status: 'failure', runs: 1, failures: 1 }], + }; + // The four doors, each with its OWN message wording (unchanged by the + // ruling) and, on the resume door alone, the engine's verdict beside the + // shared keys. + const DOORS = [ + { door: 'trigger', message: "Node 'create_account' failed: create_record(crm_account) failed", extra: {} }, + { + door: 'resume', + message: "Node 'create_account' failed: create_record(crm_account) failed", + extra: { runId: 'run_1', status: 'failed', repairable: false }, + }, + { door: '/actions', message: "Flow 'convert_lead' failed: Node 'create_account' failed", extra: {} }, + { door: 'endpoint', message: "Node 'create_account' failed: create_record(crm_account) failed", extra: {} }, + ] as const; + + const envelope = (message: string, details: Record, userMessage?: string) => ({ + success: false, + error: { + code: 'FLOW_FAILED', + message, + ...(userMessage !== undefined ? { userMessage } : {}), + details, + }, + }); + + it.each(DOORS)('$door: an envelope carrying details.innerRefusal and a top-level userMessage validates', ({ door, message, extra }) => { + const body = envelope(message, { errorMessage: 'Could not convert the lead.', summary: SUMMARY, innerRefusal: INNER, ...extra }, USER_MESSAGE); + + const parsedEnvelope = BaseResponseSchema.parse(body); + expect(parsedEnvelope.error?.code).toBe('FLOW_FAILED'); + // `message` is the door's own wording, untouched; `userMessage` rides + // beside it at the top level, never replacing it. + expect(parsedEnvelope.error?.message).toBe(message); + expect(parsedEnvelope.error?.userMessage).toBe(USER_MESSAGE); + + const details = FlowFailureDetailsSchema.parse(parsedEnvelope.error?.details); + expect(details.innerRefusal).toEqual(INNER); + expect(details.errorMessage).toBe('Could not convert the lead.'); + expect(details.summary?.nodes[0]?.nodeId).toBe('create_account'); + + if (door === 'resume') { + // The verdict rides in the SAME details object and parses with its own + // schema; neither schema refuses the other's members. + expect(ResumeFailureDetailsSchema.parse(parsedEnvelope.error?.details)) + .toEqual({ runId: 'run_1', status: 'failed', repairable: false }); + } + }); + + it.each(DOORS)('$door: an envelope WITHOUT innerRefusal or userMessage still validates (optional, additive)', ({ door, message, extra }) => { + for (const details of [{ ...extra }, { errorMessage: 'Could not convert the lead.', summary: SUMMARY, ...extra }]) { + const parsedEnvelope = BaseResponseSchema.parse(envelope(message, details)); + expect(parsedEnvelope.error?.userMessage).toBeUndefined(); + const parsed = FlowFailureDetailsSchema.parse(parsedEnvelope.error?.details); + expect(parsed.innerRefusal).toBeUndefined(); + if (door === 'resume') { + expect(ResumeFailureDetailsSchema.safeParse(parsedEnvelope.error?.details).success).toBe(true); + } + } + }); + + it('declares exactly the three ruled keys — the #9585 ceiling as ruling B′ relaxed it', () => { + expect(Object.keys(FlowFailureDetailsSchema.shape).sort()).toEqual(['errorMessage', 'innerRefusal', 'summary']); + }); + + it('the inner code takes the closed WIRE vocabulary — an unregistered spelling is refused', () => { + expect(FlowInnerRefusalSchema.safeParse(INNER).success).toBe(true); + const refused = FlowInnerRefusalSchema.safeParse({ code: 'ER_DUP_ENTRY', message: INNER.message }); + expect(refused.success).toBe(false); + expect(refused.error?.issues[0]?.path).toEqual(['code']); + expect(FlowFailureDetailsSchema.safeParse({ innerRefusal: { code: 'ER_DUP_ENTRY', message: 'x' } }).success).toBe(false); + }); + + it('the inner refusal is `{ code, message }` — both members required', () => { + expect(FlowInnerRefusalSchema.safeParse({ code: 'UNIQUE_VIOLATION' }).success).toBe(false); + expect(FlowInnerRefusalSchema.safeParse({ message: INNER.message }).success).toBe(false); + }); + + it('AutomationResult mirrors both members on the trigger response data (the #13078 parity)', () => { + const result = TriggerFlowResponseSchema.parse({ + success: true, + data: { success: false, status: 'failed', error: "Node 'create_account' failed", userMessage: USER_MESSAGE, innerRefusal: INNER }, + }); + expect(result.data.userMessage).toBe(USER_MESSAGE); + expect(result.data.innerRefusal).toEqual(INNER); + expect(TriggerFlowResponseSchema.safeParse({ + success: true, + data: { success: false, innerRefusal: { code: 'ER_DUP_ENTRY', message: 'x' } }, + }).success).toBe(false); + }); +}); + // ========================================== // Toggle Flow // ========================================== diff --git a/packages/spec/src/api/automation-api.zod.ts b/packages/spec/src/api/automation-api.zod.ts index 62a1b6b421..ca18979720 100644 --- a/packages/spec/src/api/automation-api.zod.ts +++ b/packages/spec/src/api/automation-api.zod.ts @@ -2,6 +2,7 @@ import { z } from 'zod'; import { BaseResponseSchema } from './contract.zod'; +import { ErrorCode } from './error-code-ledger.zod'; import { FlowSchema } from '../automation/flow.zod'; import { ExecutionLogSchema, ExecutionStatus, FlowRunSummarySchema } from '../automation/execution.zod'; @@ -283,6 +284,68 @@ const screenSpecShape = () => z.object({ ), }); +// ========================================== +// 7a. Inner refusal (AutomationResult.innerRefusal, and every door's 400 FLOW_FAILED details) +// ========================================== + +/** + * The refusal a flow node's write met INSIDE the run — the structured answer + * the data layer gave the node, carried to the caller beside the run's own + * failure instead of being flattened into it (ruling B′ on #24914, the + * maintainer's 「同意」, decision batch #379 item 5; filed as #24996). + * + * ## The problem it solves + * + * A record save that the platform refuses (a duplicate, a validation rule, a + * permission) reaches a record form as a structured envelope — `code`, + * `message`, and the producer-marked `userMessage` — and the form renders it + * by one rule. The same refusal met by a flow's write node reached the caller + * as one string: the node's catch kept the message, the engine prefixed the + * node id, and each flow door served that string as the `FLOW_FAILED` + * `message`. Nothing structured survived, so a flow runner had nothing to + * render but developer prose naming node ids and object API names. This + * member is the declared carrier for what was dropped. + * + * ## Semantics + * + * - **`code` takes the WIRE spelling** — the ADR-0112 vocabulary member a + * record save of the same write answers in `error.code`, not the driver's + * or the engine's internal spelling: a unique refusal is + * `UNIQUE_VIOLATION`, the spelling every route answers since the + * 2026-09-03 unique-violation ruling. It is the same closed set + * `ApiErrorSchema` (`api/contract.zod.ts`)'s `code` parses against, so an unregistered + * spelling fails parse here exactly as it does there. + * - **`message` is the inner refusal's own diagnostic text**, verbatim — + * without the node and operation prefixes the run's failure text adds. + * - **It is a NESTED member, never a replacement.** The run's failure keeps + * its own channels unchanged: `AutomationResult.error` and the door's + * `error.message` keep their wording byte for byte, and the door's + * `error.code` stays `FLOW_FAILED`. `AutomationResult.code` stays the + * closed union the #9378 ruling settled — the inner code rides HERE, so no + * new ledger code is minted for it. + * - **Present only when a refusal was met.** Absent on every other failure + * (a script that threw, an unreachable connector) and on every success: + * a producer never synthesises one from the message text. + * + * The producer-marked end-user text of the same refusal does not ride here: + * it is `userMessage` — on `AutomationResult`, and at the top level of the + * door's error envelope, where `ApiErrorSchema` (`api/contract.zod.ts`) already declares it. + */ +export const FlowInnerRefusalSchema = lazySchema(() => z.object({ + code: ErrorCode.describe( + 'The inner refusal\'s code in its WIRE spelling - the ADR-0112 member a record save of ' + + 'the same write answers in `error.code` (e.g. `UNIQUE_VIOLATION`); the closed set ' + + '`ApiErrorSchema.code` parses against', + ), + message: z.string().describe( + 'The inner refusal\'s own diagnostic text, verbatim, without the node and operation ' + + 'prefixes the run\'s failure text adds', + ), +})); +export type FlowInnerRefusal = z.input; +/** Post-parse shape of {@link FlowInnerRefusal} — defaults applied, transforms run (ADR-0122). */ +export type FlowInnerRefusalParsed = z.infer; + /** * Response after triggering a flow execution. * @@ -362,6 +425,16 @@ export const TriggerFlowResponseSchema = lazySchema(() => BaseResponseSchema.ext errorMessage: z.string().optional().describe( 'Friendly terminal message copied from the flow definition on failure', ), + userMessage: z.string().optional().describe( + 'Producer-marked user-facing refusal text, verbatim, of the refusal a node met inside ' + + 'the run - present exactly when that refusal\'s producer opted in at throw time ' + + '(`ApiErrorSchema.userMessage` semantics). Never replaces `error`', + ), + innerRefusal: FlowInnerRefusalSchema.optional().describe( + 'The refusal a node\'s write met inside the run, structured: `{ code, message }`, `code` ' + + 'in its wire spelling. Set on a failed run only when a refusal was met; `code` above ' + + 'and `error` are unchanged by it', + ), flowLabel: z.string().optional().describe( 'The flow definition\'s authored `label`, copied verbatim so a runner can name the flow ' + '(header, completion toast) and translate it against `flows..label`. Set on every ' @@ -465,6 +538,69 @@ export type ResumeFailureDetails = z.input; /** Post-parse shape of {@link ResumeFailureDetails} — defaults applied, transforms run (ADR-0122). */ export type ResumeFailureDetailsParsed = z.infer; +// ========================================== +// 7c. Flow failure details (every door's 400 FLOW_FAILED: trigger, resume, /actions, endpoint) +// ========================================== + +/** + * The `error.details` every flow door answers on its `400 FLOW_FAILED` arm — + * declared ONCE, so the four doors (`POST /api/v1/automation/:name/trigger`, + * the resume route, `/actions` and a flow-backed endpoint) emit one shape + * rather than four that drift (ruling B′ on #24914, decision batch #379 + * item 5; filed as #24996). + * + * ## The keys, and the ceiling this relaxes + * + * The run's two artefacts — the flow author's `errorMessage` and the + * per-node `summary` — rode these details first at the trigger door, and the + * #9585 ruling (2026-08-19, Option B) brought `/actions` level with it under a + * ceiling: that door carries EXACTLY those two keys into `error.details`, and + * no new schema key. Ruling B′ relaxes that ceiling by one key and names it: + * {@link FlowInnerRefusalSchema}, the refusal a node's write met inside the + * run, as `innerRefusal`. The ceiling is therefore these three keys, at every + * door; a fourth is a contract widening neither ruling covers. + * + * The resume door's verdict (`runId` / `status` / `repairable`, + * {@link ResumeFailureDetailsSchema}) rides beside these keys in the same + * object on that door alone — a client parses the one `details` object with + * both schemas. Like that schema this one is a plain (non-strict) object for + * exactly that reason: each describes its own members and leaves the other's + * alone. + * + * ## What it does not move + * + * - `error.message` keeps each door's wording byte for byte, and + * `error.code` stays `FLOW_FAILED`; the inner refusal is NESTED here, never + * promoted over either. + * - The producer-marked end-user text of the inner refusal is NOT a details + * key: it is the envelope's own top-level `userMessage` + * (`ApiErrorSchema` in `api/contract.zod.ts`), the channel a record save's + * refusal already uses, so a client reads one channel for both. + * - Every key is optional and present only when the run produced it — an + * author who wrote no `errorMessage`, an engine that measured no + * `summary`, a failure that met no refusal: absent means absent, never an + * invented empty. A never-dispatched refusal (`FLOW_DISABLED`, + * `FLOW_NO_START_NODE`, `FLOW_INPUT_SCHEMA_INVALID`) carries none of them. + */ +export const FlowFailureDetailsSchema = lazySchema(() => z.object({ + errorMessage: z.string().optional().describe( + 'The flow author\'s own failure text (`flow.errorMessage`), copied verbatim from ' + + '`AutomationResult.errorMessage`; absent when the author wrote none', + ), + summary: FlowRunSummarySchema.optional().describe( + 'The run\'s per-node accounting (`AutomationResult.summary`) - which node failed; ' + + 'absent when the engine measured none', + ), + innerRefusal: FlowInnerRefusalSchema.optional().describe( + 'The refusal a node\'s write met inside the run (`AutomationResult.innerRefusal`): ' + + '`{ code, message }`, `code` in its wire spelling (e.g. `UNIQUE_VIOLATION`). Absent ' + + 'when the failure met no refusal', + ), +})); +export type FlowFailureDetails = z.input; +/** Post-parse shape of {@link FlowFailureDetails} — defaults applied, transforms run (ADR-0122). */ +export type FlowFailureDetailsParsed = z.infer; + // ========================================== // 8. Toggle Flow (POST /api/v1/automation/:name/toggle) // ========================================== diff --git a/packages/spec/src/contracts/automation-service.ts b/packages/spec/src/contracts/automation-service.ts index c8abbda062..70223cd0d6 100644 --- a/packages/spec/src/contracts/automation-service.ts +++ b/packages/spec/src/contracts/automation-service.ts @@ -20,6 +20,7 @@ import type { ConnectorDescriptor } from '../integration/connector-descriptor'; import type { ConversionNotice, ConversionTodoNotice } from '../conversions/types'; import type { ExecutionContext } from '../kernel/execution-context.zod'; import type { FlowRegistration } from '../api/protocol.zod'; +import type { FlowInnerRefusal } from '../api/automation-api.zod'; /** * Context passed to a flow/script execution @@ -547,6 +548,36 @@ export interface AutomationResult { */ successMessage?: string; errorMessage?: string; + /** + * The producer-marked END-USER text of the refusal a node met inside the + * run, verbatim (ruling B′ on #24914, decision batch #379 item 5; #24996). + * + * The semantics are `ApiErrorSchema.userMessage`'s (`api/contract.zod.ts`) + * and nothing else: presence IS the marking — set exactly when the inner + * refusal's producer (an application hook, a platform refusal that carries + * static guidance) opted in at throw time, read through the one + * `declaredUserMessage` read; absent otherwise, so a runner keeps its own + * substitution for anything unmarked. It never replaces {@link error}: the + * diagnostic channel keeps its own wording, and a door serving this run's + * failure carries this text as its envelope's top-level `userMessage` + * beside an unchanged `message`. + */ + userMessage?: string; + /** + * The refusal a node's write met inside the run, structured — + * `{ code, message }`, `code` in its WIRE spelling (`UNIQUE_VIOLATION` + * for a unique refusal), the shape a record save of the same write + * answers with (ruling B′ on #24914; #24996). + * + * Declared once as `FlowInnerRefusalSchema` (`api/automation-api.zod.ts`), + * and every flow door's `400 FLOW_FAILED` details carry it under the same + * name (`FlowFailureDetailsSchema`). It rides BESIDE the run's failure and + * moves none of it: {@link error} keeps its wording, and {@link code} stays + * the closed union above — the inner code is nested here precisely so no + * new member is minted there. Set on a failed run only when a refusal was + * met; ⛔ never synthesised from the message text. + */ + innerRefusal?: FlowInnerRefusal; /** * The flow definition's authored `label`, copied verbatim the same way as * the two messages above, so a flow runner can name the flow it is running From 27c49842b085a107db51df884da3f19f974d2ade Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 02:05:02 +0000 Subject: [PATCH 2/3] chore(spec): changeset, summary fixture, and the #9585 ceiling docblock relaxed by ruling B' Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude --- .changeset/24996-flow-failed-inner-refusal.md | 16 ++++++++++++++++ packages/runtime/src/action-execution.ts | 9 +++++++-- packages/spec/src/api/automation-api.zod.test.ts | 3 ++- 3 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 .changeset/24996-flow-failed-inner-refusal.md diff --git a/.changeset/24996-flow-failed-inner-refusal.md b/.changeset/24996-flow-failed-inner-refusal.md new file mode 100644 index 0000000000..cb0bc700fc --- /dev/null +++ b/.changeset/24996-flow-failed-inner-refusal.md @@ -0,0 +1,16 @@ +--- +'@objectstack/spec': minor +--- + +feat(spec): a failed flow run can carry the refusal its write node met — `AutomationResult.userMessage` / `AutomationResult.innerRefusal`, and one `FlowFailureDetailsSchema` for every door's `400 FLOW_FAILED` details + +Clause-②: yes (widening) + +The maintainer's 「同意」 on ruling B′ of #24914 (decision batch #379 item 5): the inner refusal of a refused flow write node reaches the client structurally, the way a record save's refusal does, and its code takes the wire spelling. This release is the contract half. The engine and the four doors start emitting the new members when the services and runtime half (#24914) lands; until then nothing on the wire changes. + +- **`AutomationResult` (`@objectstack/spec/contracts`) gains two optional members.** `userMessage` has `ApiErrorSchema.userMessage`'s semantics: it is producer-marked end-user text, present only when the inner refusal's producer opted in at throw time, and it never replaces `error`. `innerRefusal` is `{ code, message }`. Its `code` is the wire spelling a record save of the same write answers in `error.code` (`UNIQUE_VIOLATION` for a unique refusal), parsed against the same closed `ErrorCode` set `ApiErrorSchema.code` uses. `TriggerFlowResponseSchema.data` mirrors both, so the `TriggerFlowResponse['data']` = `AutomationResult` parity pin holds. +- **New `FlowInnerRefusalSchema` and `FlowFailureDetailsSchema` (`@objectstack/spec/api`).** The second one is the `error.details` of the `400 FLOW_FAILED` arm of the trigger, resume, `/actions` and endpoint doors, declared once: `errorMessage?`, `summary?` and `innerRefusal?`. On the resume door, `ResumeFailureDetailsSchema`'s `runId` / `status` / `repairable` sit beside them in the same object, and each schema parses its own members. +- **The #9585 `/actions` ceiling, relaxed by the ruling.** That ceiling was exactly two keys in `error.details` (`errorMessage`, `summary`) and no new schema key. Ruling B′ adds one key and names it, `innerRefusal`, so the ceiling is now the three keys `FlowFailureDetailsSchema` declares, at every door. +- **Unchanged.** `error.message` keeps each door's wording byte for byte, `error.code` stays `FLOW_FAILED`, and `AutomationResult.code` stays the closed union the #9378 ruling settled. The inner code is nested in `innerRefusal`, so no new ledger code is added. Every new member is optional: an envelope without them parses exactly as before. + +**For consumers.** A flow runner can show a failed run's refusal the way a record form shows a save's refusal. Read the envelope's top-level `userMessage` when it is present. Otherwise read `details.innerRefusal` (`code` and `message`), and fall back to `message` last. A client running behind the server ignores the new members and keeps its current behaviour. diff --git a/packages/runtime/src/action-execution.ts b/packages/runtime/src/action-execution.ts index f379ccfb9e..616639b765 100644 --- a/packages/runtime/src/action-execution.ts +++ b/packages/runtime/src/action-execution.ts @@ -1046,8 +1046,13 @@ const FLOW_ACTION_REFUSAL_BRAND = Symbol.for('objectstack.runtime.flowActionRefu * The run artefacts a failed flow dispatch carries beside its status and code * — EXACTLY the two fields the trigger door ships in `error.details` on its * own `400 FLOW_FAILED` arm (`domains/automation.ts`), same names, same - * source (`AutomationResult.errorMessage` / `.summary`). A third key here is - * a contract widening the #9585 ruling does not cover. + * source (`AutomationResult.errorMessage` / `.summary`). The #9585 ruling + * capped this at those two keys; ruling B′ on #24914 (decision batch #379 + * item 5) relaxes the cap by exactly one, `innerRefusal`, and the spec now + * declares the three-key set once for every flow door as + * `FlowFailureDetailsSchema` (`@objectstack/spec/api`). Carrying that third + * key is #24914's emission half and is not done here yet; any key beyond the + * three is a contract widening neither ruling covers. */ export interface FlowActionRunDetails { /** The flow AUTHOR's own failure text (`flow.errorMessage`). */ diff --git a/packages/spec/src/api/automation-api.zod.test.ts b/packages/spec/src/api/automation-api.zod.test.ts index 42e87f0a03..353d0fe8ee 100644 --- a/packages/spec/src/api/automation-api.zod.test.ts +++ b/packages/spec/src/api/automation-api.zod.test.ts @@ -556,7 +556,8 @@ describe('FlowFailureDetailsSchema — one inner-refusal shape across the four F const USER_MESSAGE = 'An account with this name already exists.'; const SUMMARY = { selected: 0, acted: 0, skipped: 0, unmeasured: 0, - nodes: [{ nodeId: 'create_account', nodeType: 'create_record', status: 'failure', runs: 1, failures: 1 }], + nodes: [{ nodeId: 'create_account', nodeType: 'create_record', status: 'failure', runs: 1, failures: 1, skipped: 0 }], + gates: [], }; // The four doors, each with its OWN message wording (unchanged by the // ruling) and, on the resume door alone, the engine's verdict beside the From 22786c992e928af6c43ca807998408adb52add4e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 02:23:27 +0000 Subject: [PATCH 3/3] chore(spec): regenerate api-surface, export-origins, declaration-map, reference docs and strictness counts Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude --- .../docs/references/api/automation-api.mdx | 401 +++++++++++++++++- content/docs/references/index.mdx | 10 +- .../api.md | 2 +- packages/spec/api-surface/api.json | 6 + packages/spec/authorable-surface/api.json | 5 + packages/spec/declaration-map/api.json | 4 + packages/spec/export-origins/api.json | 6 + packages/spec/json-schema.manifest/api.json | 2 + 8 files changed, 428 insertions(+), 8 deletions(-) diff --git a/content/docs/references/api/automation-api.mdx b/content/docs/references/api/automation-api.mdx index 859088d9d8..658c7a805b 100644 --- a/content/docs/references/api/automation-api.mdx +++ b/content/docs/references/api/automation-api.mdx @@ -52,8 +52,8 @@ GET /api/v1/automation/:name/runs/:runId — Get single execution run ## TypeScript Usage ```typescript -import { AutomationApiErrorCode, AutomationFlowPathParamsSchema, AutomationRunPathParamsSchema, CreateFlowRequestSchema, CreateFlowResponseSchema, DeleteFlowRequestSchema, DeleteFlowResponseSchema, GetFlowRequestSchema, GetFlowResponseSchema, GetRunRequestSchema, GetRunResponseSchema, ListRunsRequestSchema, ListRunsResponseSchema, ResumeFailureDetailsSchema, ToggleFlowRequestSchema, ToggleFlowResponseSchema, TriggerFlowRequestSchema, TriggerFlowResponseSchema, UpdateFlowRequestSchema, UpdateFlowResponseSchema } from '@objectstack/spec/api'; -import type { AutomationApiErrorCode, AutomationFlowPathParams, AutomationRunPathParams, CreateFlowRequest, CreateFlowResponse, DeleteFlowRequest, DeleteFlowResponse, GetFlowRequest, GetFlowResponse, GetRunRequest, GetRunResponse, ListRunsRequest, ListRunsResponse, ResumeFailureDetails, ToggleFlowRequest, ToggleFlowResponse, TriggerFlowRequest, TriggerFlowResponse, UpdateFlowRequest, UpdateFlowResponse } from '@objectstack/spec/api'; +import { AutomationApiErrorCode, AutomationFlowPathParamsSchema, AutomationRunPathParamsSchema, CreateFlowRequestSchema, CreateFlowResponseSchema, DeleteFlowRequestSchema, DeleteFlowResponseSchema, FlowFailureDetailsSchema, FlowInnerRefusalSchema, GetFlowRequestSchema, GetFlowResponseSchema, GetRunRequestSchema, GetRunResponseSchema, ListRunsRequestSchema, ListRunsResponseSchema, ResumeFailureDetailsSchema, ToggleFlowRequestSchema, ToggleFlowResponseSchema, TriggerFlowRequestSchema, TriggerFlowResponseSchema, UpdateFlowRequestSchema, UpdateFlowResponseSchema } from '@objectstack/spec/api'; +import type { AutomationApiErrorCode, AutomationFlowPathParams, AutomationRunPathParams, CreateFlowRequest, CreateFlowResponse, DeleteFlowRequest, DeleteFlowResponse, FlowFailureDetails, FlowInnerRefusal, GetFlowRequest, GetFlowResponse, GetRunRequest, GetRunResponse, ListRunsRequest, ListRunsResponse, ResumeFailureDetails, ToggleFlowRequest, ToggleFlowResponse, TriggerFlowRequest, TriggerFlowResponse, UpdateFlowRequest, UpdateFlowResponse } from '@objectstack/spec/api'; // Validate data const result = AutomationApiErrorCode.parse(data); @@ -311,6 +311,401 @@ const result = AutomationApiErrorCode.parse(data); | **deleted** | `boolean` | ✅ | Whether the flow was deleted | +--- + +## FlowFailureDetails + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **errorMessage** | `string` | optional | The flow author's own failure text (`flow.errorMessage`), copied verbatim from `AutomationResult.errorMessage`; absent when the author wrote none | +| **summary** | `{ selected: integer; acted: integer; skipped: integer; unmeasured?: integer; … }` | optional | The run's per-node accounting (`AutomationResult.summary`) - which node failed; absent when the engine measured none | +| **innerRefusal** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>; message: string }` | optional | The refusal a node's write met inside the run (`AutomationResult.innerRefusal`): `{ code, message }`, `code` in its wire spelling (e.g. `UNIQUE_VIOLATION`). Absent when the failure met no refusal | + +### Nested Shape: `FlowFailureDetails.summary` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **selected** | `integer` | ✅ | Total records read by the run | +| **acted** | `integer` | ✅ | Total records written / effects dispatched by the run | +| **skipped** | `integer` | ✅ | Total node executions a closed gate prevented | +| **unmeasured** | `integer` | optional | Total executions that may have caused an effect the platform cannot count. Absent = not tracked (an older run), which is not the same as zero. | +| **failed** | `integer` | optional | Total node executions that failed — a fold of `nodes[].failures`, INCLUDING what a delegating node (`subflow` / `map`) rolled up from a child run that COMPLETED while containing failures: this total answers "what did this run cause", subflows included, so a parent whose child lost rows does not read `failed: 0`. A child that FAILED — whether or not it also contained failures before it failed — counts once, as the delegating step's own failure, and its own `failed` stays on its row (unlike `acted`, which carries a failed child's writes). On a run that completed every one of them was contained (caught by a `try_catch` or routed down a `fault` edge) and the run went on. Absent = not tracked (an older run), which is not the same as zero. | +| **nodes** | `{ nodeId: string; nodeType: string; nodeLabel?: string; status: Enum<'success' \| 'failure' \| 'skipped'>; … }[]` | ✅ | Per-node breakdown, in first-execution order | +| **gates** | `{ nodeId: string; targetNodeId: string; edgeId?: string; label?: string; … }[]` | ✅ | Gates that closed during the run, most-skipped first | +| **detailOmitted** | `boolean` | optional | Set when persistence dropped `nodes`/`gates` to keep the stored row bounded — the totals are still exact. Declared so empty arrays are never mistaken for "nothing ran". | + +### Nested Shape: `FlowFailureDetails.innerRefusal` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | The inner refusal's code in its WIRE spelling - the ADR-0112 member a record save of the same write answers in `error.code` (e.g. `UNIQUE_VIOLATION`); the closed set `ApiErrorSchema.code` parses against | +| **message** | `string` | ✅ | The inner refusal's own diagnostic text, verbatim, without the node and operation prefixes the run's failure text adds | + + +--- + +## FlowInnerRefusal + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +340 more>` | ✅ | The inner refusal's code in its WIRE spelling - the ADR-0112 member a record save of the same write answers in `error.code` (e.g. `UNIQUE_VIOLATION`); the closed set `ApiErrorSchema.code` parses against | +| **message** | `string` | ✅ | The inner refusal's own diagnostic text, verbatim, without the node and operation prefixes the run's failure text adds | + +### Allowed Values: `FlowInnerRefusal.code` + +* `VALIDATION_ERROR` +* `INVALID_FIELD` +* `MISSING_REQUIRED_FIELD` +* `INVALID_FORMAT` +* `VALUE_TOO_LONG` +* `VALUE_TOO_SHORT` +* `VALUE_OUT_OF_RANGE` +* `INVALID_REFERENCE` +* `DUPLICATE_VALUE` +* `INVALID_QUERY` +* `INVALID_FILTER` +* `INVALID_SORT` +* `MAX_RECORDS_EXCEEDED` +* `UNAUTHENTICATED` +* `INVALID_CREDENTIALS` +* `EXPIRED_TOKEN` +* `INVALID_TOKEN` +* `SESSION_EXPIRED` +* `MFA_REQUIRED` +* `EMAIL_NOT_VERIFIED` +* `PERMISSION_DENIED` +* `INSUFFICIENT_PRIVILEGES` +* `FIELD_NOT_ACCESSIBLE` +* `RECORD_NOT_ACCESSIBLE` +* `LICENSE_REQUIRED` +* `IP_RESTRICTED` +* `TIME_RESTRICTED` +* `RESOURCE_NOT_FOUND` +* `OBJECT_NOT_FOUND` +* `RECORD_NOT_FOUND` +* `FIELD_NOT_FOUND` +* `ENDPOINT_NOT_FOUND` +* `RESOURCE_CONFLICT` +* `CONCURRENT_MODIFICATION` +* `DELETE_RESTRICTED` +* `DUPLICATE_RECORD` +* `LOCK_CONFLICT` +* `METHOD_NOT_ALLOWED` +* `PRECONDITION_REQUIRED` +* `RATE_LIMIT_EXCEEDED` +* `QUOTA_EXCEEDED` +* `INTERNAL_ERROR` +* `DATABASE_ERROR` +* `TIMEOUT` +* `SERVICE_UNAVAILABLE` +* `NOT_IMPLEMENTED` +* `EXTERNAL_SERVICE_ERROR` +* `INTEGRATION_ERROR` +* `WEBHOOK_DELIVERY_FAILED` +* `ACCOUNT_LOCKED` +* `ACTION_CONFIRMATION_REQUIRED` +* `ACTION_DISABLED` +* `ALREADY_REVERTED` +* `AMBIGUOUS_MATCH` +* `ANALYTICS_DATE_RANGE_UNRECOGNIZED` +* `ANALYTICS_QUERY_FAILED` +* `API_KEY_EXPIRED` +* `API_KEY_EXPIRY_TOO_LONG` +* `API_KEY_INACTIVE` +* `API_KEY_REVOKED` +* `APPROVAL_ACTIONS_FAILED` +* `APPROVAL_APPROVE_FAILED` +* `APPROVAL_COMMENT_FAILED` +* `APPROVAL_REASSIGN_FAILED` +* `APPROVAL_RECALL_FAILED` +* `APPROVAL_REJECT_FAILED` +* `APPROVAL_REMIND_FAILED` +* `APPROVAL_REQUEST_GET_FAILED` +* `APPROVAL_REQUEST_INFO_FAILED` +* `APPROVAL_REQUEST_LIST_FAILED` +* `APPROVAL_RESUBMIT_FAILED` +* `APPROVAL_REVISE_FAILED` +* `ASYNC_NOT_SUPPORTED` +* `ATTACHMENT_DELETE_DENIED` +* `ATTACHMENT_DOWNLOAD_DENIED` +* `ATTACHMENT_PARENT_ACCESS` +* `AUDIENCE_NOT_ALLOWED` +* `AUDIT_TYPE_NOT_CANONICAL` +* `AUTH_CONFIG_ERROR` +* `AUTH_REQUIRED` +* `AUTOMATION_UNSCOPED_RUN_DATA_ACCESS` +* `BATCH_ABORTED` +* `BATCH_NOT_ATOMIC` +* `BATCH_TOO_LARGE` +* `BATCH_UNRESOLVED_REF` +* `BLANK_MATCH_KEY` +* `CLONE_DISABLED` +* `CLOUD_FETCH_FAILED` +* `CLOUD_UNCONFIGURED` +* `COMMIT_NOT_FOUND` +* `CONCURRENT_UPDATE` +* `CONFLICT` +* `CONFLICTING_MAPPING` +* `CONNECTOR_UPSTREAM_UNAVAILABLE` +* `CREATE_FAILED` +* `CUBE_NOT_FOUND` +* `DATASET_INVALID` +* `DATASOURCE_ADMIN_ERROR` +* `DEFAULT_RECORD_PAGE_CONFLICT` +* `DELEGABLE_SCOPE_FAILED` +* `DELETE_HOOK_RESULT_NOT_WRITE_SHAPE` +* `DELIVERY_NEVER_SENT` +* `DELIVERY_NOT_ELIGIBLE` +* `DESTRUCTIVE_CHANGE` +* `DEVICE_CODE_FAILED` +* `DOMAIN_VERIFICATION_DISABLED` +* `DOMAIN_VERIFICATION_FAILED` +* `DRIVER_UNAVAILABLE` +* `DUPLICATE_ARTIFACT_OBJECT_NAME` +* `DUPLICATE_ARTIFACT_PACKAGE` +* `DUPLICATE_REQUEST` +* `DUPLICATE_SOURCE_NOT_A_BASE` +* `ELIGIBILITY_UNEVALUABLE` +* `EMAIL_DOMAIN_NOT_ALLOWED` +* `EMAIL_SEND_FAILED` +* `EMAIL_SERVICE_REQUIRED` +* `ENQUEUE_FAILED` +* `ENVIRONMENT_BIND_FAILED` +* `ENVIRONMENT_NOT_FOUND` +* `ENV_ACCESS_DENIED` +* `ERR_AUTONUMBER_COLLISION` +* `ERR_BULK_RESULT_MISMATCH` +* `ERR_CROSS_DATASOURCE_TRANSACTION_WRITE` +* `ERR_DATASOURCE_UNAVAILABLE` +* `ERR_DRIVER_CONNECT` +* `ERR_FILE_CONSTRAINT` +* `ERR_FILE_REFERENCE_COPY` +* `ERR_HOOK_TARGET_REBIND` +* `ERR_READONLY_FIELD_REJECTED` +* `ERR_SUMMARY_RECOMPUTE` +* `ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` +* `ERR_TRANSACTION_UNSUPPORTED` +* `EXECUTION_ERROR` +* `EXPIRED_OR_REVOKED` +* `EXPIRY_IN_PAST` +* `EXPIRY_TOO_LONG` +* `EXPLAIN_FAILED` +* `EXPORT_NOT_PERMITTED` +* `EXTERNAL_DATASOURCE_ERROR` +* `EXTERNAL_IMPORT_ERROR` +* `EXTERNAL_SCHEMA_MISMATCH` +* `EXTERNAL_SCHEMA_MODE_VIOLATION` +* `EXTERNAL_WRITE_FORBIDDEN` +* `FEEDS_DISABLED` +* `FIELD_VISIBILITY_UNRESOLVED` +* `FILES_DISABLED` +* `FILE_DOWNLOAD_DENIED` +* `FILE_FIELD_BULK_WRITE_REFUSED` +* `FILE_NOT_FOUND` +* `FILTER_TOKEN_UNKNOWN` +* `FILTER_TOKEN_UNRESOLVED` +* `FIND_HOOK_RESULT_NOT_ARRAY` +* `FIND_ONE_HOOK_RESULT_NOT_RECORD` +* `FLOW_DISABLED` +* `FLOW_FAILED` +* `FLOW_INPUT_SCHEMA_INVALID` +* `FLOW_NO_START_NODE` +* `FORBIDDEN` +* `FORM_NOT_FOUND` +* `FORM_RESOLVE_FAILED` +* `HOOK_UNSCOPED_DATA_ACCESS` +* `IMPORT_JOB_CREATE_FAILED` +* `IMPORT_ROW_FAILED` +* `INSTALL_MODE_NOT_PERMITTED` +* `INTERNAL` +* `INVALID_ARTIFACT_PACKAGES` +* `INVALID_ARTIFACT_PACKAGE_ENTRY` +* `INVALID_EMAIL` +* `INVALID_EXPIRY` +* `INVALID_METADATA` +* `INVALID_OR_EXPIRED` +* `INVALID_PHONE` +* `INVALID_REQUEST` +* `INVALID_RESUME_TOKEN` +* `INVALID_SCREEN_INPUT` +* `INVALID_SIGNAL` +* `INVALID_SIGNATURE` +* `INVALID_STATE` +* `INVITE_EMAIL_FAILED` +* `INVITE_ISSUER_MEMBER_ONLY` +* `INVITE_ISSUER_UNVERIFIED` +* `INVITE_PLACEMENT_OUTSIDE_ISSUER_SCOPE` +* `INVITE_PLACEMENT_UNSUPPORTED` +* `INVITE_REQUIRES_EMAIL` +* `INVITE_ROLE_ABOVE_ISSUER` +* `INVITE_SMS_FAILED` +* `IP_NOT_ALLOWED` +* `ITEM_LOCKED` +* `LAST_LOCAL_CREDENTIAL` +* `LEGACY_ORGANIZATION_ROWS_UNREADABLE` +* `MANIFEST_CONFLICT` +* `MAPPING_FORMAT_MISMATCH` +* `MAPPING_FORMAT_UNSUPPORTED` +* `MAPPING_NOT_FOUND` +* `MAPPING_TARGET_MISMATCH` +* `MARKETPLACE_PROXY_FAILED` +* `MARKETPLACE_STORAGE_FAILED` +* `MARKETPLACE_UNAVAILABLE` +* `MEMORY_MULTI_TENANT_UNSUPPORTED` +* `METADATA_BRANCH` +* `METADATA_CONFLICT` +* `METADATA_NOT_FOUND` +* `METADATA_SCHEMA_INVALID` +* `MIXED_ARTIFACT_COLLECTION_SHAPE` +* `MONGODB_MULTI_TENANT_UNSUPPORTED` +* `MULTI_UPDATE_HOOK_KEY_DIVERGENCE` +* `NAMESPACE_CONFLICT` +* `NAMESPACE_PREFIX` +* `NEEDS_PASSWORD` +* `NODE_FAILURE` +* `NOTHING_TO_PURGE` +* `NOT_ATTEMPTED` +* `NOT_COMPENSABLE` +* `NOT_CREATABLE` +* `NOT_FOUND` +* `NOT_OVERRIDABLE` +* `NOT_UNDOABLE` +* `NO_DRAFT` +* `NO_EXECUTOR` +* `NO_IDENTITY` +* `NO_MATCH` +* `NO_PENDING_VERIFICATION` +* `NO_SUCH_RUN` +* `OAUTH_REGISTER_FAILED` +* `OBJECT_API_DISABLED` +* `OBJECT_API_METHOD_NOT_ALLOWED` +* `OBJECT_OVERLAY_PACKAGE_MISMATCH` +* `OBJECT_OWNERSHIP_CONFLICT` +* `OBJECT_PACKAGE_DISABLED` +* `OPENAPI_UNAVAILABLE` +* `ORGANIZATION_CREATION_DISABLED` +* `ORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED` +* `ORGANIZATION_SLUG_LOCKED_BY_ACTIVE_ENVIRONMENTS` +* `OS_PROTOCOL_INCOMPATIBLE` +* `PACKAGE_DELETE_FAILED` +* `PACKAGE_DELETE_PARTIAL` +* `PACKAGE_MANIFEST_INVALID` +* `PACKAGE_PUBLISH_FAILED` +* `PASSWORD_ALREADY_SET` +* `PASSWORD_EXPIRED` +* `PASSWORD_POLICY_VIOLATION` +* `PASSWORD_RESET_REQUIRED` +* `PASSWORD_REUSE` +* `PAYLOAD_TOO_LARGE` +* `PERMISSION_NOT_ALLOWED` +* `PHONE_NOT_ENABLED` +* `PLAN_CHANGED` +* `PLUGIN_CONTRACT_VIOLATION` +* `PLUGIN_INSTALL_FAILED` +* `PLUGIN_MANIFEST_INVALID` +* `PLUGIN_REGISTER_FAILED` +* `PLUGIN_UI_REQUIRED_KEY_MISSING` +* `PREFLIGHT_FAILED` +* `PROJECT_MEMBERSHIP_REQUIRED` +* `PROJECT_NOT_FOUND` +* `PROJECT_PROVISIONING` +* `PROJECT_PROVISIONING_FAILED` +* `QUERY_OBJECT_MISMATCH` +* `RAW_SQL_UNSUPPORTED` +* `READ_BACK_FAILED` +* `READ_SCOPE_COMPILE_FAILED` +* `RECORD_GONE` +* `RECORD_LOCKED` +* `RECORD_NOT_ELIGIBLE` +* `REGISTRY_TYPE_NOT_CANONICAL` +* `REQUEST_NOT_FOUND` +* `RESEED_NO_ROWS` +* `RESEED_SKIPPED` +* `RESUME_FAILED` +* `RESUME_IN_PROGRESS` +* `RESUME_TARGET_LOST` +* `ROLE_NOT_FOUND` +* `ROLLED_BACK` +* `ROUTE_NOT_FOUND` +* `RULE_DEFINE_FAILED` +* `RULE_DELETE_FAILED` +* `RULE_EVALUATE_FAILED` +* `RULE_GET_FAILED` +* `RULE_LIST_FAILED` +* `RULE_NOT_FOUND` +* `RUN_NOT_FOUND` +* `SAML_REGISTER_FAILED` +* `SELF_REGISTRATION_CLOSED` +* `SERVICE_NOT_REGISTERED` +* `SETTINGS_ACTION_FAILED` +* `SETTINGS_CRYPTO_UNAVAILABLE` +* `SETTINGS_ENGINE_NOT_BOUND` +* `SETTINGS_FORBIDDEN` +* `SETTINGS_LOCKED` +* `SETTINGS_UNKNOWN_KEY` +* `SETTINGS_UNKNOWN_NAMESPACE` +* `SETTINGS_VALIDATION` +* `SHARES_LIST_FAILED` +* `SHARE_GRANT_FAILED` +* `SHARE_REVOKE_FAILED` +* `SHARING_NOT_ENABLED` +* `SIGN_IN_REQUIRED` +* `SMS_SERVICE_REQUIRED` +* `SQL_DIALECT_EMISSION_UNSUPPORTED` +* `SSO_REGISTER_FAILED` +* `SSO_REGISTER_FORBIDDEN` +* `STACK_CAPABILITY_UNKNOWN` +* `STACK_COMPOSE_ACTION_KEY_COLLISION` +* `STACK_COMPOSE_COLLECTION_CONFLICT` +* `STACK_COMPOSE_FUNCTIONS_SHAPE_CONFLICT` +* `STACK_COMPOSE_FUNCTION_CONFLICT` +* `STACK_COMPOSE_KEY_CONFLICT` +* `STACK_COMPOSE_OBJECT_CONFLICT` +* `STACK_CROSS_REFERENCE_INVALID` +* `STACK_HIERARCHY_SCOPE_CAPABILITY_REQUIRED` +* `STACK_NAMESPACE_PREFIX_INVALID` +* `STACK_PROVENANCE_MISSING` +* `STACK_SCHEMA_INVALID` +* `STACK_SINGLE_APP_VIOLATION` +* `STACK_TRIGGER_CAPABILITY_REQUIRED` +* `STACK_UNMANAGED_INSTALL_UNCOPYABLE` +* `STORED_TYPE_NOT_CANONICAL` +* `STORE_UNAVAILABLE` +* `SUGGESTION_CONFIRM_FAILED` +* `SUGGESTION_DISMISS_FAILED` +* `SUGGESTION_LIST_FAILED` +* `SUGGESTION_NOT_FOUND` +* `SUGGESTION_STATE` +* `SUMMARY_RECOMPUTE_FAILED` +* `THROTTLED` +* `UNAUTHORIZED` +* `UNIQUE_SCOPE_CONFIRMATION_REQUIRED` +* `UNIQUE_VIOLATION` +* `UNKNOWN_KEY` +* `UNKNOWN_NAMESPACE` +* `UNMANAGED_INSTALL_UNCOPYABLE` +* `UNMANAGED_INSTALL_WALLED_POSTURE` +* `UNSUPPORTED` +* `UNSUPPORTED_QUERY_PARAM` +* `UNSUPPORTED_TRANSFORM` +* `UPDATE_HOOK_RESULT_NOT_WRITE_SHAPE` +* `UPDATE_ID_MISMATCH` +* `UPLOAD_SESSION_EXPIRED` +* `UPLOAD_SESSION_NOT_FOUND` +* `USER_ALREADY_EXISTS` +* `USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL` +* `VALIDATION_FAILED` +* `VERSION_NOT_FOUND` +* `VERSION_NOT_RESTORABLE` +* `WALLED_MEMBERSHIP_POLICY_UNDECLARED` +* `WRITABLE_PACKAGE_REQUIRED` +* `WRONG_PASSWORD` + + --- ## GetFlowRequest @@ -650,6 +1045,8 @@ const result = AutomationApiErrorCode.parse(data); | **screen** | `{ nodeId: string; title?: string; description?: string; fields: object[]; … }` | optional | The screen to render - set when the run paused at a `screen` node awaiting user input. The client collects values for `screen.fields` and resumes the run with them. | | **successMessage** | `string` | optional | Friendly terminal message copied from the flow definition on terminal success, so a screen-flow runner can show a meaningful toast | | **errorMessage** | `string` | optional | Friendly terminal message copied from the flow definition on failure | +| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim, of the refusal a node met inside the run - present exactly when that refusal's producer opted in at throw time (`ApiErrorSchema.userMessage` semantics). Never replaces `error` | +| **innerRefusal** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>; message: string }` | optional | The refusal a node's write met inside the run, structured: `{ code, message }`, `code` in its wire spelling. Set on a failed run only when a refusal was met; `code` above and `error` are unchanged by it | | **flowLabel** | `string` | optional | The flow definition's authored `label`, copied verbatim so a runner can name the flow (header, completion toast) and translate it against `flows..label`. Set on every result of an evaluation of a registered flow (paused and terminal alike); absent on a refusal carrying `code`. For a subflow chain it is the addressed (parent) run's flow. Never defaulted to the API name | | **refusalMessage** | `string` | optional | Rendered refusal, set when `status` is `refused` - the `end` node's `message` template interpolated against the run's variables, so it names the record. Authored per-record text (not a flow-level copy like the two above); absent on every other status. A runner shows it with Close only | | **summary** | `{ selected: integer; acted: integer; skipped: integer; unmeasured?: integer; … }` | optional | What the run did - records selected / acted on, gate skips, per-node status. Set on a TERMINAL result (a paused run has not finished doing it yet). | diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index 7dad531e93..1aa68c2a4e 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,7 +1,7 @@ --- title: Protocol reference — every schema by module navTitle: Protocol Reference -description: Every schema published by @objectstack/spec — 1542 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1544 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -21,7 +21,7 @@ counts are sums of the rows they head. Regenerate with | Module | Pages | Schemas | Description | | :--- | ---: | ---: | :--- | | [AI Protocol](/docs/references/ai) | 12 | 68 | Agents, tools, skills, RAG and knowledge sources, model registry, conversations. | -| [API Protocol](/docs/references/api) | 32 | 438 | REST contracts, endpoints, routing, realtime, batch, discovery. | +| [API Protocol](/docs/references/api) | 32 | 440 | REST contracts, endpoints, routing, realtime, batch, discovery. | | [Automation Protocol](/docs/references/automation) | 13 | 71 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. | | [Data Protocol](/docs/references/data) | 29 | 176 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. | | [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. | @@ -34,7 +34,7 @@ counts are sums of the rows they head. Regenerate with | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 36 | 285 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | | [UI Protocol](/docs/references/ui) | 16 | 169 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **197** | **1542** | 14 protocol modules | +| **Total** | **197** | **1544** | 14 protocol modules | --- @@ -63,7 +63,7 @@ Agents, tools, skills, RAG and knowledge sources, model registry, conversations. ## API Protocol -**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **32 pages, 438 schemas** +**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **32 pages, 440 schemas** REST contracts, endpoints, routing, realtime, batch, discovery. @@ -72,7 +72,7 @@ REST contracts, endpoints, routing, realtime, batch, discovery. | [`analytics.zod.ts`](/docs/references/api/analytics) | `AnalyticsEndpoint`, `AnalyticsMetadataResponse`, `AnalyticsQueryRequest`, `AnalyticsResultResponse`, `AnalyticsSqlResponse`, `DatasetCompareTo`, `DatasetSelection`, `DatasetTotals`, `GetAnalyticsMetaRequest` | | [`auth.zod.ts`](/docs/references/api/auth) | `AuthProvider`, `LoginRequest`, `LoginType`, `RefreshTokenRequest`, `RegisterRequest`, `Session`, `SessionResponse`, `SessionUser`, `UserProfileResponse` | | [`auth-endpoints.zod.ts`](/docs/references/api/auth-endpoints) | `AuthEndpoint`, `AuthFeaturesConfig`, `AuthProviderInfo`, `DeviceRequestResponse`, `DeviceTokenResponse`, `EmailPasswordConfigPublic`, `GetAuthConfigResponse` | -| [`automation-api.zod.ts`](/docs/references/api/automation-api) | `AutomationApiErrorCode`, `AutomationFlowPathParams`, `AutomationRunPathParams`, `CreateFlowRequest`, `CreateFlowResponse`, `DeleteFlowRequest`, `DeleteFlowResponse`, `GetFlowRequest`, `GetFlowResponse`, `GetRunRequest`, `GetRunResponse`, `ListRunsRequest`, `ListRunsResponse`, `ResumeFailureDetails`, `ToggleFlowRequest`, `ToggleFlowResponse`, `TriggerFlowRequest`, `TriggerFlowResponse`, `UpdateFlowRequest`, `UpdateFlowResponse` | +| [`automation-api.zod.ts`](/docs/references/api/automation-api) | `AutomationApiErrorCode`, `AutomationFlowPathParams`, `AutomationRunPathParams`, `CreateFlowRequest`, `CreateFlowResponse`, `DeleteFlowRequest`, `DeleteFlowResponse`, `FlowFailureDetails`, `FlowInnerRefusal`, `GetFlowRequest`, `GetFlowResponse`, `GetRunRequest`, `GetRunResponse`, `ListRunsRequest`, `ListRunsResponse`, `ResumeFailureDetails`, `ToggleFlowRequest`, `ToggleFlowResponse`, `TriggerFlowRequest`, `TriggerFlowResponse`, `UpdateFlowRequest`, `UpdateFlowResponse` | | [`batch.zod.ts`](/docs/references/api/batch) | `BatchConfig`, `BatchOperationResult`, `BatchOperationType`, `BatchOptions`, `BatchRecord`, `BatchUpdateRequest`, `BatchUpdateResponse`, `CrossObjectBatchDroppedFields`, `CrossObjectBatchOperation`, `CrossObjectBatchRequest`, `CrossObjectBatchResponse`, `DeleteManyRequest`, `UpdateManyRecord`, `UpdateManyRequest` | | [`contract.zod.ts`](/docs/references/api/contract) | `ApiError`, `BaseResponse`, `BatchLoadingStrategy`, `BulkRequest`, `BulkResponse`, `CreateRequest`, `DataLoaderConfig`, `DeleteResponse`, `ExportRequest`, `IdRequest`, `ListRecordResponse`, `ModificationResult`, `QueryOptimizationConfig`, `RecordData`, `SingleRecordResponse`, `UpdateRequest` | | [`discovery.zod.ts`](/docs/references/api/discovery) | `ApiRoutes`, `AuthFamilies`, `CapabilityDescriptor`, `Discovery`, `DiscoveryEnvironment`, `EnvironmentType`, `RouteHealthEntry`, `RouteHealthReport`, `ServiceInfo`, `ServiceSelfInfo`, `ServiceStatus`, `WellKnownCapabilities` | diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md index 80c986c933..7b162852b5 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md @@ -19,4 +19,4 @@ hand-patch a number here** — fix the code or the verdict and regenerate. | Dir | Sites | |---|---| -| `api/` | 446 | +| `api/` | 448 | diff --git a/packages/spec/api-surface/api.json b/packages/spec/api-surface/api.json index 4980634e58..d1ef98db19 100644 --- a/packages/spec/api-surface/api.json +++ b/packages/spec/api-surface/api.json @@ -405,6 +405,12 @@ "FindReferencesToMetaResponse (type)", "FindReferencesToMetaResponseParsed (type)", "FindReferencesToMetaResponseSchema (const)", + "FlowFailureDetails (type)", + "FlowFailureDetailsParsed (type)", + "FlowFailureDetailsSchema (const)", + "FlowInnerRefusal (type)", + "FlowInnerRefusalParsed (type)", + "FlowInnerRefusalSchema (const)", "FlowRegistration (type)", "FlowRegistrationRefusal (type)", "FlowRegistrationRefusalSchema (const)", diff --git a/packages/spec/authorable-surface/api.json b/packages/spec/authorable-surface/api.json index c17a2b4fc4..d30150bb3e 100644 --- a/packages/spec/authorable-surface/api.json +++ b/packages/spec/authorable-surface/api.json @@ -689,6 +689,11 @@ "api/FindDataResponse:records", "api/FindDataResponse:total", "api/FindReferencesToMetaResponse:references", + "api/FlowFailureDetails:errorMessage", + "api/FlowFailureDetails:innerRefusal", + "api/FlowFailureDetails:summary", + "api/FlowInnerRefusal:code", + "api/FlowInnerRefusal:message", "api/FlowRegistrationRefusal:code", "api/FlowRegistrationRefusal:message", "api/GeneratedApiDocumentation:generatedAt", diff --git a/packages/spec/declaration-map/api.json b/packages/spec/declaration-map/api.json index 16edaaacd3..93ff2f315e 100644 --- a/packages/spec/declaration-map/api.json +++ b/packages/spec/declaration-map/api.json @@ -289,6 +289,10 @@ "FindDataResponseSchema": "api/FindDataResponse", "FindReferencesToMetaResponse": "api/FindReferencesToMetaResponse", "FindReferencesToMetaResponseSchema": "api/FindReferencesToMetaResponse", + "FlowFailureDetails": "api/FlowFailureDetails", + "FlowFailureDetailsSchema": "api/FlowFailureDetails", + "FlowInnerRefusal": "api/FlowInnerRefusal", + "FlowInnerRefusalSchema": "api/FlowInnerRefusal", "FlowRegistration": "api/FlowRegistration", "FlowRegistrationRefusal": "api/FlowRegistrationRefusal", "FlowRegistrationRefusalSchema": "api/FlowRegistrationRefusal", diff --git a/packages/spec/export-origins/api.json b/packages/spec/export-origins/api.json index 96c8fd0337..9816ec22dd 100644 --- a/packages/spec/export-origins/api.json +++ b/packages/spec/export-origins/api.json @@ -384,6 +384,12 @@ "FindReferencesToMetaResponse": "src/api/protocol.zod.ts#FindReferencesToMetaResponse (type)", "FindReferencesToMetaResponseParsed": "src/api/protocol.zod.ts#FindReferencesToMetaResponseParsed (type)", "FindReferencesToMetaResponseSchema": "src/api/protocol.zod.ts#FindReferencesToMetaResponseSchema (const)", + "FlowFailureDetails": "src/api/automation-api.zod.ts#FlowFailureDetails (type)", + "FlowFailureDetailsParsed": "src/api/automation-api.zod.ts#FlowFailureDetailsParsed (type)", + "FlowFailureDetailsSchema": "src/api/automation-api.zod.ts#FlowFailureDetailsSchema (const)", + "FlowInnerRefusal": "src/api/automation-api.zod.ts#FlowInnerRefusal (type)", + "FlowInnerRefusalParsed": "src/api/automation-api.zod.ts#FlowInnerRefusalParsed (type)", + "FlowInnerRefusalSchema": "src/api/automation-api.zod.ts#FlowInnerRefusalSchema (const)", "FlowRegistration": "src/api/protocol.zod.ts#FlowRegistration (type)", "FlowRegistrationRefusal": "src/api/protocol.zod.ts#FlowRegistrationRefusal (type)", "FlowRegistrationRefusalSchema": "src/api/protocol.zod.ts#FlowRegistrationRefusalSchema (const)", diff --git a/packages/spec/json-schema.manifest/api.json b/packages/spec/json-schema.manifest/api.json index 923f98997a..8f8c67bfc4 100644 --- a/packages/spec/json-schema.manifest/api.json +++ b/packages/spec/json-schema.manifest/api.json @@ -159,6 +159,8 @@ "api/FindDataRequest", "api/FindDataResponse", "api/FindReferencesToMetaResponse", + "api/FlowFailureDetails", + "api/FlowInnerRefusal", "api/FlowRegistration", "api/FlowRegistrationRefusal", "api/GeneratedApiDocumentation",