Skip to content

Commit bdb4e8d

Browse files
os-zhuangclaude
andauthored
docs(automation,releases,plugins): back-fill the accuracy audit #4161 never got (#4219)
The drift comment on #4161 was computed by the mapper bug #4206 fixed: the service-automation change collapsed to `packages/services`, so these four docs were attributed to the wrong package — and a service-automation-only diff would have reported none of them. Re-derived with the fixed mapper and audited against the implementation. flows.mdx carries the #4161 axis itself: a `runAs:'user'` run that resolved no trigger user has its data operations refused. Also documents `node.type` as an open string checked against the live registry (ADR-0018), the script executor as naming a callable with logger-backed `email`/`slack` markers, the `.strict()` shells, and six previously undocumented node keys. implementation-status.mdx gains the real `/api/v1` route prefixes, the memory driver's actual `InMemoryDriver.supports` matrix, and the ADR-0090 D3 `Role` -> `Position` rename — which ratchets the role-word baseline down by one, so that gate-mandated update ships here too. Gates: docs build, check:doc-authoring, check:role-word, check:nul-bytes, check:release-notes, check:skill-examples (198 prose examples). Claude-Session: https://claude.ai/code/session_01J2x8Tie9WT1VgWFifqMKrR Co-authored-by: Claude <noreply@anthropic.com>
1 parent 19365b7 commit bdb4e8d

6 files changed

Lines changed: 256 additions & 151 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
---
3+
4+
Docs only — no package changes, nothing to release.
5+
6+
Back-fills the implementation-accuracy audit that #4161 never got. Its drift
7+
comment was computed by the mapper bug fixed in #4206: the change to
8+
`packages/services/service-automation` collapsed to the container directory, so
9+
the four docs that actually reference `@objectstack/service-automation` were
10+
either attributed to the wrong package or (on a service-automation-only diff)
11+
not reported at all. Re-derived with the fixed mapper and audited:
12+
`automation/flows.mdx`, `plugins/packages.mdx`, `releases/implementation-status.mdx`,
13+
`releases/v9.mdx`.
14+
15+
44 evidence-backed fixes, 5 repaired by the adversarial verifier. The
16+
substantive ones:
17+
18+
- **`flows.mdx`**`runAs` now states that a `'user'` run which resolved no
19+
trigger user has its data operations refused (the axis #4161 tightened);
20+
`node.type` is documented as an open `string` checked against the live action
21+
registry at `registerFlow()` (ADR-0018), not a closed enum; the `script`
22+
executor is described as naming a callable, with `'email'` / `'slack'` called
23+
out as logger-backed markers that record intent without delivering; the
24+
`.strict()` flow/node/edge/variable shells and the previously undocumented
25+
`timeoutMs`, `inputSchema`, `waitEventConfig`, `boundaryConfig`,
26+
`successMessage`, `errorMessage` keys are added.
27+
- **`implementation-status.mdx`** — REST rows carry their real `/api/v1` prefix;
28+
the memory driver's capability list is replaced with what `InMemoryDriver.supports`
29+
actually declares; Cache/Encryption/Dataset/Sorting statuses corrected;
30+
the `msw` column dropped (not a workspace package); `Role``Position`
31+
(ADR-0090 D3), which ratchets `scripts/role-word-baseline.json` down by one —
32+
that gate fails on a *decrease* too, so the baseline update ships with it.
33+
- **`packages.mdx`** — package inventory and the `create*` plugin-factory claim
34+
corrected against the real exports.
35+
- **`v9.mdx`** — notes that `os package publish --visibility` has since shipped
36+
(`private` / `org` / `marketplace`, default `org`).
37+
38+
18 residual items the agents could not resolve without a code-owner decision are
39+
recorded in the PR body rather than guessed at; two are genuine code/doc
40+
contradictions worth their own issues.

content/docs/automation/flows.mdx

Lines changed: 86 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,13 @@ const approvalFlow = {
7373
| `version` | `number` | optional | Version number (defaults to `1`) |
7474
| `status` | `enum` | optional | `'draft'`, `'active'`, `'obsolete'`, `'invalid'` (defaults to `'draft'`) |
7575
| `type` | `FlowType` || Flow trigger type (see below) |
76-
| `runAs` | `enum` | optional | `'system'` or `'user'` execution context (defaults to `'user'`) |
76+
| `runAs` | `enum` | optional | `'system'` (elevated, bypasses RLS) or `'user'` (the triggering user, RLS-respecting; the default). A `'user'` run that resolved **no** trigger user has nothing to scope to, so its data operations are refused — declare `'system'` for schedule / time-relative / API triggers and for record-change flows fired by a write that carried no user |
7777
| `variables` | `FlowVariable[]` | optional | Input/output variables |
7878
| `nodes` | `FlowNode[]` || Flow nodes |
7979
| `edges` | `FlowEdge[]` || Connections between nodes |
8080
| `errorHandling` | `object` | optional | Error handling strategy |
81+
| `successMessage` | `string` | optional | Toast a screen-flow runner shows on terminal success (plain string — `{var}` is **not** interpolated) |
82+
| `errorMessage` | `string` | optional | Toast a screen-flow runner shows on failure, instead of the raw error |
8183

8284
### Flow Types
8385

@@ -110,7 +112,7 @@ Each node performs a specific action in the flow.
110112
| `get_record` | Query records |
111113
| `http` | Make an HTTP API call |
112114
| `notify` | Send an outbound notification via the messaging service |
113-
| `script` | Run a custom script action (dispatched by `config.actionType`) |
115+
| `script` | Call a named callable — a registered function (`config.function`) or a built-in side-effect marker (`config.actionType`) |
114116
| `screen` | Display a user form/screen (durable pause) |
115117
| `wait` | Pause for a timer or named signal (durable pause; timers auto-resume) |
116118
| `subflow` | Invoke another flow — a pause inside the child suspends both runs as a linked chain |
@@ -134,11 +136,21 @@ Each node performs a specific action in the flow.
134136
| Property | Type | Required | Description |
135137
| :--- | :--- | :--- | :--- |
136138
| `id` | `string` || Unique node identifier |
137-
| `type` | `FlowNodeAction` || Node type |
139+
| `type` | `string` || Node type — a built-in id from the table above **or** a plugin-registered one. Per ADR-0018 the spec does not gate this with a closed enum; it is checked against the live action registry at `registerFlow()` |
138140
| `label` | `string` || Display label |
139-
| `config` | `object` | optional | Type-specific configuration |
140-
| `connectorConfig` | `object` | optional | External connector settings |
141+
| `config` | `object` | optional | Type-specific configuration (an open record — the registered executor's `configSchema` owns its shape) |
142+
| `connectorConfig` | `object` | optional | `{ connectorId, actionId, input }` for a `connector_action` node |
141143
| `position` | `{ x, y }` | optional | Visual position on canvas |
144+
| `timeoutMs` | `number` | optional | Per-node execution timeout |
145+
| `inputSchema` | `object` | optional | Declared input parameter types, for Studio form generation and runtime validation |
146+
| `waitEventConfig` | `object` | optional | `wait`-node event descriptor (`eventType`, `timerDuration`, `signalName`, `timeoutMs`, `onTimeout`) |
147+
| `boundaryConfig` | `object` | optional | BPMN boundary-event descriptor (interop) |
148+
149+
<Callout type="info">
150+
The flow, node, edge, and variable **shells are `.strict()`** — a key they do not
151+
declare is a parse error naming the likely intended key, not a silent strip. (A
152+
node's `config` stays open, so plugin node types keep their own vocabulary.)
153+
</Callout>
142154

143155
### Node Examples
144156

@@ -193,18 +205,36 @@ Each node performs a specific action in the flow.
193205

194206
**Script:**
195207

196-
The built-in `script` executor dispatches on `config.actionType` (e.g. `email`)
197-
rather than evaluating an arbitrary JavaScript string:
208+
The built-in `script` executor never evaluates an arbitrary JavaScript string —
209+
it **names a callable**. Two forms:
210+
211+
- **`config.function`** — a function registered through
212+
`defineStack({ functions })`. `config.inputs` is `{var}`-interpolated and
213+
handed to it; `config.outputVariable` binds the returned value as a flow
214+
variable, so a later declarative node persists it. This is the supported way
215+
to run server logic.
216+
- **`config.actionType`** — one of the two built-in side-effect markers,
217+
`'email'` or `'slack'`. These are **logger-backed**: they record the intent
218+
and succeed, they do not deliver anything — reach for a `notify` node when you
219+
want real delivery. Any other `actionType` value is treated as a
220+
registered-function name — except the marker `'invoke_function'`, which means
221+
"call the function named in `config.function`" and errors if that key is
222+
missing.
223+
224+
Inline `config.script` (a JS source body) is *recognized* but **not executed**
225+
the built-in runtime has no server-side JS sandbox, so such a node warns and
226+
no-ops. A script node that names neither a built-in action nor a registered
227+
function fails the step loudly rather than passing silently.
198228

199229
```typescript
200230
{
201-
id: 'send_welcome',
231+
id: 'score_lead',
202232
type: 'script',
203-
label: 'Send Welcome Email',
233+
label: 'Score Lead',
204234
config: {
205-
actionType: 'email',
206-
template: 'welcome',
207-
recipients: '{record.email}',
235+
function: 'scoreLead', // registered via defineStack({ functions })
236+
inputs: { rating: '{record.rating}' }, // {var} templates resolve against flow variables
237+
outputVariable: 'leadScore', // later: fields: { score: '{leadScore}' }
208238
},
209239
}
210240
```
@@ -247,8 +277,11 @@ Two things worth knowing:
247277
value at all, which counts as unanswered — without `defaultValue: false` the
248278
user cannot express "no" by leaving it clear.
249279
- **A broken predicate fails open** (the field stays visible) rather than hiding
250-
an input the flow may be waiting on. Nothing validates the expression at
251-
author time yet, so a typo shows up as a field that never hides.
280+
an input the flow may be waiting on. `registerFlow()` does check the predicate
281+
as bare CEL — a `{var}` template or a syntax error is a loud registration
282+
failure that quotes the source and locates it as
283+
`config.fields[N].visibleWhen` — but a predicate that *parses* and simply
284+
names the wrong field still shows up as a field that never hides.
252285

253286
**Screen (object form):**
254287

@@ -561,8 +594,9 @@ Edges connect nodes and define the execution path:
561594
| `source` | `string` || Source node ID |
562595
| `target` | `string` || Target node ID |
563596
| `type` | `enum` | optional | `'default'` (success), `'fault'` (error), `'conditional'` (expression-guarded), or `'back'` (declared back-edge, ADR-0044); defaults to `'default'` |
564-
| `condition` | `string` | optional | Boolean expression for branching |
597+
| `condition` | `string` | optional | Boolean CEL predicate for branching (a bare string is stored as `{ dialect: 'cel', source }`) |
565598
| `label` | `string` | optional | Label displayed on the connector — cosmetic only. It does **not** select a path except on a branching node (`decision` / `approval`), which picks its out-edge by label. |
599+
| `isDefault` | `boolean` | optional | BPMN default-flow marker (interop). Accepted by the schema, but **the engine does not read it** — traversal selects by `condition` and by `label`. On a `decision` node, the fallback is the out-edge labelled `default`: when no `conditions[]` entry matches, the node emits `branchLabel: 'default'` |
566600

567601
### Fault edges — handling a failed node
568602

@@ -582,7 +616,7 @@ edges: [
582616
the type leaves an ordinary edge, and every unconditional out-edge is traversed
583617
(in parallel) on **success**. The handler then runs whenever the node
584618
*succeeds*, never when it fails, and the run still aborts on failure. Both
585-
halves are silent, so `objectstack validate` reports this as
619+
halves are silent, so `os validate` reports this as
586620
`flow-error-label-not-fault`.
587621
</Callout>
588622

@@ -621,7 +655,7 @@ The split is deliberate. A dropped filter condition does not narrow a query, it
621655
widens it — so if guards were routable, one `fault` edge on a `delete_record`
622656
would turn off the protection against emptying the object while the run still
623657
reported success. Re-running changes nothing either: the fix is to correct the
624-
metadata, which `objectstack validate` will point at.
658+
metadata, which `os validate` will point at.
625659
</Callout>
626660

627661
#### Fault edges vs. `errorHandling.retry`
@@ -673,9 +707,12 @@ errorHandling: {
673707

674708
| Property | Type | Description |
675709
| :--- | :--- | :--- |
676-
| `strategy` | `enum` | `'fail'` (stop), `'retry'` (retry), `'continue'` (skip) |
710+
| `strategy` | `enum` | `'fail'` (stop) or `'retry'` (re-run the whole flow). `'continue'` parses but the engine branches only on `'retry'`, so it behaves exactly like `'fail'` — use a `fault` edge to keep going past a failed node |
677711
| `maxRetries` | `number` | Maximum retry attempts (0-10) |
678712
| `retryDelayMs` | `number` | Delay between retries (ms) |
713+
| `backoffMultiplier` | `number` | Exponential backoff multiplier (default `1`) |
714+
| `maxRetryDelayMs` | `number` | Ceiling on the backed-off delay (default `30000`) |
715+
| `jitter` | `boolean` | Randomize the delay to avoid a thundering herd (default `false`) |
679716

680717
## Discovery & Registration
681718

@@ -782,30 +819,43 @@ credentials.
782819

783820
<Callout type="warn">
784821
**`success: true` doesn't always mean "it ran".** If the start condition isn't
785-
met, the response is a *successful* skip:
786-
`{ "success": true, "data": { "skipped": true, "reason": "condition_not_met" } }`.
822+
met, the response is a *successful* skip — the run result rides in `data`, and
823+
the skip marker sits on its `output`:
824+
`{ "success": true, "data": { "success": true, "output": { "skipped": true, "reason": "condition_not_met" } } }`.
787825
A self-triggering flow caught by the loop guard reports
788-
`reason: "reentrancy_loop_guard"` the same way. Check `skipped` before assuming
789-
the work happened.
826+
`reason: "reentrancy_loop_guard"` the same way. Check `data.output.skipped`
827+
before assuming the work happened.
790828
</Callout>
791829

792830
## Console Flow Viewer & Test Runner
793831

794-
Every flow can surface in the Console metadata browser under `/_console/`.
795-
ObjectUI's flow viewer replaces the default JSON inspector with rich tabs when
796-
the flow viewer plugin is installed:
832+
Every flow surfaces in the Console metadata browser under `/_console/`, where
833+
ObjectUI's app-shell registers `FlowPreview` as the `flow` metadata preview
834+
(`registerMetadataPreview('flow', FlowPreview)`) in place of the default JSON
835+
inspector. It renders the graph on a canvas — editable (add a node, patch a
836+
selection) only in the designer's edit mode, read-only otherwise. Opening a flow
837+
shows the **full-width canvas with no side panel**; the toolbar toggles one
838+
collapsible panel at a time:
797839

798-
| Tab | Component | Purpose |
840+
| Side panel | Component | Purpose |
799841
|:---|:---|:---|
800-
| **Overview** | `FlowViewer` | Renders trigger type, variables, nodes, edges, and `errorHandling` as inspector cards |
801-
| **Run** | `FlowTestRunner` | Auto-generates a form for every `isInput: true` variable (with type-aware coercion for `number` / `boolean` / `object` / `list`), executes the flow against the per-project kernel, and shows the returned outputs + run id |
802-
| **Runs** | `FlowRunsPanel` | Lists historical executions for the selected flow with status, duration, and a deep-link to the run record |
803-
804-
The runner posts to the same
805-
[`POST /api/v1/automation/:name/trigger`](#run-a-flow-via-api) endpoint,
806-
scoped to the active project. All three components participate in the Studio
807-
authentication / project-scope context, so they work identically in
808-
single-project mode and in cloud mode.
842+
| **Variables** || The flow's declared variables with their types and input/output flags. Opt-in — it is the fallback panel, not one that opens on its own |
843+
| **Problems** | `ProblemsPanel` | Validation findings; selecting one reveals the offending node or edge on the canvas |
844+
| **Debug** | `FlowSimulatorPanel` | Steps the graph client-side, highlighting the active node, the visited nodes, and the traversed edges |
845+
| **Runs** | `FlowRunsPanel` | Real run history from [`GET /api/v1/automation/:name/runs`](#observing-runs), each run's step log nested by loop iteration / parallel branch / try-catch handler |
846+
847+
Actually *running* a flow is a separate Console page — **Developer › Flow Runs**
848+
(`developer/flow-runs`). Its test runner auto-generates a form for every
849+
`isInput: true` variable (with type-aware coercion for `number` / `boolean` /
850+
`object` / `list`) and calls `client.automation.execute(name, { params })`
851+
the same [`POST /api/v1/automation/:name/trigger`](#run-a-flow-via-api)
852+
endpoint — then renders the returned result. A screen flow that comes back
853+
`paused` is handed to the same `FlowRunner` the record and list surfaces use,
854+
so multi-step wizards (and `object-form` steps) can be driven to completion
855+
from here instead of orphaning a suspended run. A run-history panel beside it
856+
lists recent runs and expands one for its step detail. The Console is not
857+
project-scoped: the client comes from the app adapter, with no `projectId` in
858+
the route.
809859

810860
## Flows in practice
811861

@@ -1040,6 +1090,6 @@ To branch on **which** event fired, test `previous` — it is empty on create
10401090

10411091
## Related
10421092

1043-
- [Workflow Metadata](/docs/automation/workflows)Event-triggered automation rules
1093+
- [Workflow Metadata](/docs/automation/workflows)why there is no standalone Workflow Rule type, and what replaces it
10441094
- [Object Metadata](/docs/data-modeling/objects) — Objects that flows operate on
10451095
- [Validation Metadata](/docs/data-modeling/validation) — Data validation rules

0 commit comments

Comments
 (0)