Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/33402-inner-refusal-describe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@objectstack/spec': patch
---

docs(spec): `innerRefusal` (on `AutomationResult`, `FlowFailureDetailsSchema` and `FlowInnerRefusalSchema`) is now described as the refusal a node met inside the run, from a data node's read or write or a `script` node's function resolution, instead of only a node's write.

Clause-②: no
Metadata-protocol: none
4 changes: 2 additions & 2 deletions content/docs/references/api/automation-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ const result = AutomationApiErrorCode.parse(data);
| :--- | :--- | :--- | :--- |
| **errorMessage** | `string` | optional | The flow author's own failure text (`flow.errorMessage`, rendered by the engine), 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 |
| **innerRefusal** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>; message: string }` | optional | The refusal a node met inside the run (a data node's read or write, or a `script` node's function resolution; `AutomationResult.innerRefusal`): `{ code, message }`, `code` in its wire spelling (e.g. `UNIQUE_VIOLATION`). Absent when the failure met no refusal |

### Nested Shape: `FlowFailureDetails.summary`

Expand Down Expand Up @@ -1137,7 +1137,7 @@ const result = AutomationApiErrorCode.parse(data);
| **successMessage** | `string` | optional | Friendly terminal message rendered from the flow definition on terminal success (its `{{ }}` holes filled over the run's variables, in the run's locale), so a screen-flow runner can show a meaningful toast |
| **errorMessage** | `string` | optional | Friendly terminal message rendered from the flow definition on failure (its `{{ }}` holes filled over the variables at the failure point, in the run's locale) |
| **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 |
| **innerRefusal** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>; message: string }` | optional | The refusal a node met inside the run (a data node's read or write, or a `script` node's function resolution), 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.<flow>.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). |
Expand Down
21 changes: 13 additions & 8 deletions packages/spec/src/api/automation-api.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,10 +316,12 @@ const screenSpecShape = () => z.object({
// ==========================================

/**
* 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 refusal a flow node met INSIDE the run (a data node's read or write, or
* a `script` node's function resolution) — the structured answer the data
* layer (or the function resolver) 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
*
Expand Down Expand Up @@ -461,7 +463,8 @@ export const TriggerFlowResponseSchema = lazySchema(() => BaseResponseSchema.ext
+ '(`ApiErrorSchema.userMessage` semantics). Never replaces `error`',
),
innerRefusal: FlowInnerRefusalSchema.optional().describe(
'The refusal a node\'s write met inside the run, structured: `{ code, message }`, `code` '
'The refusal a node met inside the run (a data node\'s read or write, or a `script` '
+ 'node\'s function resolution), 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',
),
Expand Down Expand Up @@ -602,8 +605,9 @@ export type ResumeFailureDetailsParsed = z.infer<typeof ResumeFailureDetailsSche
* #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
* {@link FlowInnerRefusalSchema}, the refusal a node met inside the run (a
* data node's read or write, or a `script` node's function resolution), 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`,
Expand Down Expand Up @@ -638,7 +642,8 @@ export const FlowFailureDetailsSchema = lazySchema(() => z.object({
+ 'absent when the engine measured none',
),
innerRefusal: FlowInnerRefusalSchema.optional().describe(
'The refusal a node\'s write met inside the run (`AutomationResult.innerRefusal`): '
'The refusal a node met inside the run (a data node\'s read or write, or a `script` '
+ 'node\'s function resolution; `AutomationResult.innerRefusal`): '
+ '`{ code, message }`, `code` in its wire spelling (e.g. `UNIQUE_VIOLATION`). Absent '
+ 'when the failure met no refusal',
),
Expand Down
3 changes: 2 additions & 1 deletion packages/spec/src/contracts/automation-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,8 @@ export interface AutomationResult {
*/
userMessage?: string;
/**
* The refusal a node's write met inside the run, structured —
* The refusal a node met inside the run (a data node's read or write, or a
* `script` node's function resolution), 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).
Expand Down
Loading