Skip to content

Commit b9ea7d4

Browse files
chore(spec): regenerate the automation surface, docs references and ledgers for the end-node outcome (wip)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf
1 parent fe4ba76 commit b9ea7d4

13 files changed

Lines changed: 53 additions & 22 deletions

File tree

content/docs/automation/flows.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ Each node performs a specific action in the flow.
9999
| Type | Description |
100100
| :--- | :--- |
101101
| `start` | Flow entry point |
102-
| `end` | Flow termination — `config.outcome` says how: `completed` (default) or `refused` with an interpolated `message` ([below](#end-refuse-the-run-with-per-record-text)) |
102+
| `end` | Flow termination — `config.outcome` says how: `completed` (default) or `refused` with an interpolated `message` ([below](#end-node-outcome)) |
103103
| `decision` | Conditional branching (if/else) |
104104
| `assignment` | Set variable values |
105105
| `loop` | Structured iteration **container** — runs a body region once per item (ADR-0031) |
@@ -426,7 +426,7 @@ bound to `config.idVariable` so a later step can reference it.
426426
This is how a single flow walks the user through several full object forms in
427427
sequence (e.g. lead → account → opportunity), each step saving its own record.
428428

429-
**End (refuse the run with per-record text):** [#end-refuse-the-run-with-per-record-text]
429+
### Ending a run `completed` or `refused` [#end-node-outcome]
430430

431431
Every `end` used to mean "completed". The terminal node now declares its
432432
**outcome**, so a flow can say *refuse this, and here is why, for this record*

content/docs/permissions/system-context.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ assuming `isSystem` covers it is a documented source of bugs.
193193

194194
| Assumption | Reality | Anchor |
195195
|:---|:---|:---|
196-
| "It suppresses triggers / record-change automation" | **No.** Only `skipTriggers` does. A bare `{ isSystem: true }` on a seed write re-fired automation on freshly seeded rows and wedged first boot | `metadata-protocol/src/seed-loader.ts:2032` (rationale at `:1942``1944`, #3760), `flow.zod.ts:702` |
196+
| "It suppresses triggers / record-change automation" | **No.** Only `skipTriggers` does. A bare `{ isSystem: true }` on a seed write re-fired automation on freshly seeded rows and wedged first boot | `metadata-protocol/src/seed-loader.ts:2032` (rationale at `:1942``1944`, #3760), `flow.zod.ts:743` |
197197
| "It skips the state machine" | **No.** That is `skipStateMachine`, carried by seed replay and by `treatAsHistorical` imports | `objectql/src/engine.ts` FSM gate; see [State Machine](/docs/protocol/objectql/state-machine) |
198198
| "It skips validation rules" | **No.** Field shape, `format`, `script` and the rest still run. The `readonly` strip runs *before* validation precisely so a discarded value is not judged | `objectql/src/engine.ts:10166``10183` |
199199
| "It preserves a supplied `updated_at` / `updated_by`" | **No.** That is `preserveAudit`, a separate opt-in — and an UPDATE-path exemption only | `field.zod.ts:1580` (#3493 / #6640) |

content/docs/references/api/automation-api.mdx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ const result = AutomationApiErrorCode.parse(data);
399399
| **flowName** | `string` || Machine name of the executed flow |
400400
| **flowVersion** | `integer` | optional | Version of the flow that was executed |
401401
| **status** | `Enum<'pending' \| 'running' \| 'paused' \| 'completed' \| 'failed' \| 'cancelled' \| …>` || Current execution status |
402+
| **refusalMessage** | `string` | optional | Rendered `end` node `message` when `status` is `refused` — the per-record text the flow refused with. Absent on every other status. |
402403
| **trigger** | `{ type: string; recordId?: string; object?: string; userId?: string; … }` || What triggered this execution |
403404
| **steps** | `{ nodeId: string; nodeType: string; nodeLabel?: string; status: Enum<'success' \| 'failure' \| 'skipped'>; … }[]` || Ordered list of executed steps |
404405
| **summary** | `{ selected: integer; acted: integer; skipped: integer; unmeasured?: integer; … }` | optional | Per-run rollup: records selected / acted on, gate skips, per-node status |
@@ -469,7 +470,7 @@ const result = AutomationApiErrorCode.parse(data);
469470
| Property | Type | Required | Description |
470471
| :--- | :--- | :--- | :--- |
471472
| **name** | `string` || Flow machine name (snake_case) |
472-
| **status** | `Enum<'pending' \| 'running' \| 'paused' \| 'completed' \| 'failed' \| 'cancelled' \| 'timed_out' \| 'retrying'>` | optional | Filter by execution status |
473+
| **status** | `Enum<'pending' \| 'running' \| 'paused' \| 'completed' \| 'failed' \| 'cancelled' \| 'timed_out' \| 'retrying' \| 'refused'>` | optional | Filter by execution status |
473474
| **limit** | `integer` | optional (default: `20`) | Maximum number of runs to return |
474475
| **cursor** | `string` | optional | Cursor for pagination |
475476

@@ -607,11 +608,12 @@ const result = AutomationApiErrorCode.parse(data);
607608
| **error** | `string` | optional | Error message if execution failed |
608609
| **durationMs** | `number` | optional | Execution duration in milliseconds |
609610
| **code** | `Enum<'PERMISSION_DENIED' \| 'INVALID_SIGNAL' \| 'RUN_NOT_FOUND' \| 'STORE_UNAVAILABLE' \| …>` | optional | Machine-readable failure classification, set alongside `error` when the caller must distinguish WHY it failed. A closed union - the members and their transport mappings are documented on the contract (`AutomationResult.code`, contracts/automation-service.ts). |
610-
| **status** | `Enum<'completed' \| 'paused' \| 'failed' \| 'stranded'>` | optional | Lifecycle status. `paused` means the run suspended at a node and can be continued with the resume route. Absent or `completed`/`failed`/`stranded` means the run reached a terminal state. `stranded` is the terminally-failed-but-repairable run: a resume consumed the suspension and a downstream node threw, so the run is recorded as failed and can be re-armed only by an explicit operator verb - never by the resume route, which answers RUN_NOT_FOUND for it. |
611+
| **status** | `Enum<'completed' \| 'paused' \| 'failed' \| 'stranded' \| 'refused'>` | optional | Lifecycle status. `paused` means the run suspended at a node and can be continued with the resume route. Absent or `completed`/`failed`/`stranded`/`refused` means the run reached a terminal state. `refused` is a first-class refusal: the flow reached an `end` node declaring `outcome: 'refused'` — a successful evaluation that said no, so `success` is true, `successMessage` is absent and the per-record reason is on `refusalMessage`; a runner shows it with Close only. `stranded` is the terminally-failed-but-repairable run: a resume consumed the suspension and a downstream node threw, so the run is recorded as failed and can be re-armed only by an explicit operator verb - never by the resume route, which answers RUN_NOT_FOUND for it. |
611612
| **runId** | `string` | optional | Run id - set when `status` is `paused`, so callers can resume it |
612613
| **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. |
613614
| **successMessage** | `string` | optional | Friendly terminal message copied from the flow definition on terminal success, so a screen-flow runner can show a meaningful toast |
614615
| **errorMessage** | `string` | optional | Friendly terminal message copied from the flow definition on failure |
616+
| **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 |
615617
| **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). |
616618

617619

content/docs/references/automation/builtin-node-config.mdx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ description: Builtin Node Config protocol schemas
77

88
Config contracts for the remaining flat builtins — the CRUD quartet
99
(`get_record` / `create_record` / `update_record` / `delete_record`),
10-
`screen`, `map` (#4045) and, since #14149, `assignment`'s value contract.
10+
`screen`, `map` (#4045), since #14149 `assignment`'s value contract and,
11+
since #14945, the structural `end` node's outcome (`EndConfigSchema`, the
12+
one contract here the FLOW PARSE applies rather than an executor).
1113
Sibling of `io-node-config.zod.ts` (notify / http) and `control-flow.zod.ts`
1214
(loop / parallel / try_catch).
1315

@@ -84,8 +86,8 @@ Deliberately absent:
8486
## TypeScript Usage
8587

8688
```typescript
87-
import { AssignmentConfigSchema, AssignmentExpressionValueSchema, AssignmentValueSchema, CreateRecordConfigSchema, DeleteRecordConfigSchema, GetRecordConfigSchema, MapConfigSchema, ScreenConfigSchema, ScreenFieldConfigSchema, UpdateRecordConfigSchema } from '@objectstack/spec/automation';
88-
import type { AssignmentConfig, AssignmentExpressionValue, AssignmentValue, CreateRecordConfig, DeleteRecordConfig, GetRecordConfig, MapConfig, ScreenConfig, ScreenFieldConfig, UpdateRecordConfig } from '@objectstack/spec/automation';
89+
import { AssignmentConfigSchema, AssignmentExpressionValueSchema, AssignmentValueSchema, CreateRecordConfigSchema, DeleteRecordConfigSchema, EndConfigSchema, GetRecordConfigSchema, MapConfigSchema, ScreenConfigSchema, ScreenFieldConfigSchema, UpdateRecordConfigSchema } from '@objectstack/spec/automation';
90+
import type { AssignmentConfig, AssignmentExpressionValue, AssignmentValue, CreateRecordConfig, DeleteRecordConfig, EndConfig, GetRecordConfig, MapConfig, ScreenConfig, ScreenFieldConfig, UpdateRecordConfig } from '@objectstack/spec/automation';
8991

9092
// Validate data
9193
const result = AssignmentConfigSchema.parse(data);
@@ -151,6 +153,18 @@ Value the variable takes: a string (`{token}` flow interpolation — a sole toke
151153
| **multi** | `boolean` | optional | Declare bulk intent: delete every row the filter matches (default false — a predicate delete without it is refused by the engine) |
152154

153155

156+
---
157+
158+
## EndConfig
159+
160+
### Properties
161+
162+
| Property | Type | Required | Description |
163+
| :--- | :--- | :--- | :--- |
164+
| **outcome** | `Enum<'completed' \| 'refused'>` | optional (default: `"completed"`) | How the run ends when it reaches this node. `completed` (the default) is the ordinary terminal. `refused` is a first-class refusal: the run records `refused` — distinct from `failed`, a refusal is a successful evaluation that says no — carries the rendered `message`, is never resumed, and a runner shows the message with Close only: no Submit, no completion toast. |
165+
| **message** | `string` | optional | Why the run was refused, as a `{token}` template interpolated at run time exactly like a screen `description` (`{record.name}` etc.), so the text names the record. Required when `outcome` is `refused`; refused when it is `completed` — a completion renders nothing, so the key would be a silent no-op. |
166+
167+
154168
---
155169

156170
## GetRecordConfig

content/docs/references/automation/execution.mdx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ const result = CheckpointSchema.parse(data);
103103
| **id** | `string` || Execution instance ID |
104104
| **flowName** | `string` || Machine name of the executed flow |
105105
| **flowVersion** | `integer` | optional | Version of the flow that was executed |
106-
| **status** | `Enum<'pending' \| 'running' \| 'paused' \| 'completed' \| 'failed' \| 'cancelled' \| 'timed_out' \| 'retrying'>` || Current execution status |
106+
| **status** | `Enum<'pending' \| 'running' \| 'paused' \| 'completed' \| 'failed' \| 'cancelled' \| 'timed_out' \| 'retrying' \| 'refused'>` || Current execution status |
107+
| **refusalMessage** | `string` | optional | Rendered `end` node `message` when `status` is `refused` — the per-record text the flow refused with. Absent on every other status. |
107108
| **trigger** | `{ type: string; recordId?: string; object?: string; userId?: string; … }` || What triggered this execution |
108109
| **steps** | `{ nodeId: string; nodeType: string; nodeLabel?: string; status: Enum<'success' \| 'failure' \| 'skipped'>; … }[]` || Ordered list of executed steps |
109110
| **summary** | `{ selected: integer; acted: integer; skipped: integer; unmeasured?: integer; … }` | optional | Per-run rollup: records selected / acted on, gate skips, per-node status |
@@ -174,6 +175,7 @@ const result = CheckpointSchema.parse(data);
174175
* `cancelled`
175176
* `timed_out`
176177
* `retrying`
178+
* `refused`
177179

178180

179181
---
@@ -347,7 +349,7 @@ const result = CheckpointSchema.parse(data);
347349
| **nextRunAt** | `string` | optional | Next scheduled execution timestamp |
348350
| **lastRunAt** | `string` | optional | Last execution timestamp |
349351
| **lastExecutionId** | `string` | optional | Execution ID of the last run |
350-
| **lastRunStatus** | `Enum<'pending' \| 'running' \| 'paused' \| 'completed' \| 'failed' \| 'cancelled' \| 'timed_out' \| 'retrying'>` | optional | Status of the last run |
352+
| **lastRunStatus** | `Enum<'pending' \| 'running' \| 'paused' \| 'completed' \| 'failed' \| 'cancelled' \| 'timed_out' \| 'retrying' \| 'refused'>` | optional | Status of the last run |
351353
| **totalRuns** | `integer` | optional (default: `0`) | Total number of executions |
352354
| **consecutiveFailures** | `integer` | optional (default: `0`) | Consecutive failed executions |
353355
| **startDate** | `string` | optional | Schedule effective start date |

content/docs/references/index.mdx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Protocol Reference
3-
description: Every schema published by @objectstack/spec — 1589 schemas across 14 protocol modules
3+
description: Every schema published by @objectstack/spec — 1590 schemas across 14 protocol modules
44
---
55

66
{/* ⚠️ 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
2121
| :--- | ---: | ---: | :--- |
2222
| [AI Protocol](/docs/references/ai) | 11 | 66 | Agents, tools, skills, RAG and knowledge sources, model registry, conversations. |
2323
| [API Protocol](/docs/references/api) | 31 | 436 | REST contracts, endpoints, routing, realtime, batch, discovery. |
24-
| [Automation Protocol](/docs/references/automation) | 13 | 72 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. |
24+
| [Automation Protocol](/docs/references/automation) | 13 | 73 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. |
2525
| [Cloud Protocol](/docs/references/cloud) | 11 | 94 | Environments, packages and versions, marketplace, developer portal, tenancy. |
2626
| [Data Protocol](/docs/references/data) | 29 | 166 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. |
2727
| [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. |
@@ -33,7 +33,7 @@ counts are sums of the rows they head. Regenerate with
3333
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
3434
| [System Protocol](/docs/references/system) | 36 | 291 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
3535
| [UI Protocol](/docs/references/ui) | 16 | 153 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
36-
| **Total** | **200** | **1589** | 14 protocol modules |
36+
| **Total** | **200** | **1590** | 14 protocol modules |
3737

3838
---
3939

@@ -103,15 +103,15 @@ REST contracts, endpoints, routing, realtime, batch, discovery.
103103

104104
## Automation Protocol
105105

106-
**Source:** `packages/spec/src/automation/` · **Import:** `@objectstack/spec/automation` · **13 pages, 72 schemas**
106+
**Source:** `packages/spec/src/automation/` · **Import:** `@objectstack/spec/automation` · **13 pages, 73 schemas**
107107

108108
Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records.
109109

110110
| File | Schemas |
111111
| :--- | :--- |
112112
| [`approval.zod.ts`](/docs/references/automation/approval) | `ApprovalDecision`, `ApprovalEscalation`, `ApprovalNodeApprover`, `ApprovalNodeConfig`, `ApproverType`, `DecisionOutputDef` |
113113
| [`bpmn-interop.zod.ts`](/docs/references/automation/bpmn-interop) | `BpmnDiagnostic`, `BpmnElementMapping`, `BpmnExportOptions`, `BpmnImportOptions`, `BpmnInteropResult`, `BpmnUnmappedStrategy`, `BpmnVersion` |
114-
| [`builtin-node-config.zod.ts`](/docs/references/automation/builtin-node-config) | `AssignmentConfig`, `AssignmentExpressionValue`, `AssignmentValue`, `CreateRecordConfig`, `DeleteRecordConfig`, `GetRecordConfig`, `MapConfig`, `ScreenConfig`, `ScreenFieldConfig`, `UpdateRecordConfig` |
114+
| [`builtin-node-config.zod.ts`](/docs/references/automation/builtin-node-config) | `AssignmentConfig`, `AssignmentExpressionValue`, `AssignmentValue`, `CreateRecordConfig`, `DeleteRecordConfig`, `EndConfig`, `GetRecordConfig`, `MapConfig`, `ScreenConfig`, `ScreenFieldConfig`, `UpdateRecordConfig` |
115115
| [`control-flow.zod.ts`](/docs/references/automation/control-flow) | `FlowRegion`, `LoopConfig`, `ParallelBranch`, `ParallelConfig`, `RetryPolicy`, `TryCatchConfig`, `TryCatchErrorValue` |
116116
| [`execution.zod.ts`](/docs/references/automation/execution) | `Checkpoint`, `ConcurrencyPolicy`, `ExecutionError`, `ExecutionErrorSeverity`, `ExecutionLog`, `ExecutionStatus`, `ExecutionStepLog`, `ExecutionStepMetrics`, `ExecutionStepSkipReason`, `FlowRunGateSummary`, `FlowRunNodeSummary`, `FlowRunSummary`, `ScheduleState` |
117117
| [`flow.zod.ts`](/docs/references/automation/flow) | `Flow`, `FlowEdge`, `FlowNode`, `FlowNodeAction`, `FlowVariable`, `FlowVersionHistory` |

docs/audits/2026-07-unknown-key-strictness-ledger.counts.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ regenerate.
2121
| Measure | Value |
2222
|---|---|
2323
| Triaged directories | 5 |
24-
| Object sites in them | 440 |
24+
| Object sites in them | 441 |
2525
| Still-open (strip) sites | 124 |
2626
| Files carrying at least one | 22 |
2727

@@ -46,10 +46,10 @@ The `strict` column is the one the campaign schedules against; it counts both th
4646
|---|---|---|---|---|---|
4747
| `ui/` | 169 | 157 | 5 | 0 | 7 |
4848
| `data/` | 157 | 76 | 1 | 0 | 80 |
49-
| `automation/` | 67 | 42 | 0 | 1 | 24 |
49+
| `automation/` | 68 | 43 | 0 | 1 | 24 |
5050
| `security/` | 20 | 7 | 0 | 0 | 13 |
5151
| `studio/` | 27 | 27 | 0 | 0 | 0 |
52-
| **total** | **440** | **309** | **6** | **1** | **124** |
52+
| **total** | **441** | **310** | **6** | **1** | **124** |
5353

5454
## File-level triage — site counts
5555

@@ -115,7 +115,7 @@ classify and is not listed (it becomes reportable the day it grows its first sit
115115
|---|---|
116116
| `approval.zod.ts` | 4 |
117117
| `bpmn-interop.zod.ts` | 5 |
118-
| `builtin-node-config.zod.ts` | 9 |
118+
| `builtin-node-config.zod.ts` | 10 |
119119
| `control-flow.zod.ts` | 6 |
120120
| `execution.zod.ts` | 13 |
121121
| `flow-function.zod.ts` | 1 |
@@ -126,7 +126,7 @@ classify and is not listed (it becomes reportable the day it grows its first sit
126126
| `state-machine.zod.ts` | 6 |
127127
| `time-relative-trigger.zod.ts` | 1 |
128128
| `webhook.zod.ts` | 1 |
129-
| **total** | **67** |
129+
| **total** | **68** |
130130

131131
### `security/` — sites
132132

@@ -204,7 +204,7 @@ over it is here.
204204

205205
### `automation/` — open
206206

207-
**24 strip of 67**, in 5 file(s).
207+
**24 strip of 68**, in 5 file(s).
208208

209209
| File | Strip | Sites |
210210
|---|---|---|
@@ -213,7 +213,7 @@ over it is here.
213213
| `execution.zod.ts` | 13 | 13 |
214214
| `flow.zod.ts` | 1 | 11 |
215215
| `node-executor.zod.ts` | 4 | 4 |
216-
| **total** | **24** | **67** |
216+
| **total** | **24** | **68** |
217217

218218
| Bucket | Sites |
219219
|---|---|

packages/spec/api-surface/automation.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@
8787
"DeleteRecordConfig (type)",
8888
"DeleteRecordConfigParsed (type)",
8989
"DeleteRecordConfigSchema (const)",
90+
"EndConfig (type)",
91+
"EndConfigParsed (type)",
92+
"EndConfigSchema (const)",
9093
"ExecutionError (type)",
9194
"ExecutionErrorParsed (type)",
9295
"ExecutionErrorSchema (const)",

packages/spec/authorable-defaults/automation.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"automation/ConcurrencyPolicy:lockScope = \"global\"",
3636
"automation/ConcurrencyPolicy:maxConcurrent = 1",
3737
"automation/ConcurrencyPolicy:onConflict = \"queue\"",
38+
"automation/EndConfig:outcome = \"completed\"",
3839
"automation/ExecutionError:retryable = false",
3940
"automation/Flow:runAs = \"user\"",
4041
"automation/Flow:status = \"draft\"",

0 commit comments

Comments
 (0)