Skip to content

Commit e462186

Browse files
feat(spec,automation): create_record / update_record fields.* accept the CEL value envelope — declared and evaluated together (#20205)
Fixes #19938 Fixes #11182 Clause-②: yes One branch, one PR, spec commit first, one merge: maintainer ruling B on #19938 (record `5816929495`, 「19938 同意」), which folds the #11182 engine half into this change. #11182 ruling D (`5805777944`, 「11182 D 其他同意」) governs the content. ## What changes A value in a `create_record` or `update_record` node's `fields` map may now be a CEL value envelope, `{ dialect: 'cel', source: '…' }`, under the same shape and dialect rules the `assignment` node's `assignments` map already has. The slot is declared in the expression ledger and evaluated by the executor in the same merge, so it is never declared without being evaluated. A plain string in `fields.*` is still a `{token}` template and means what it meant in 17.x. No spelling changes meaning (ruling D). ### Commit 1 (`de7c28942`): spec, the contract half (#19938) - **Expression ledger** (`flow-node-expression-paths.ts`): two new `value` rows, `create_record.fields.*` and `update_record.fields.*`. The census grows from five rows to seven. The prose that called the assignment map the only value slot is corrected, and so is the shipped `predicateSlotRefusal` sentence that named it as the only `value`-role spelling. - **Value contract** (`builtin-node-config.zod.ts`): `CreateRecordConfigSchema` / `UpdateRecordConfigSchema` `fields` values take `FlowValueSlotSchema`. One factory builds every value slot's contract, so the shape rule is stated once. It is declared above the CRUD schemas: `OS_EAGER_SCHEMAS=1` runs every lazy factory at module load, and a schema declared further down would be in its temporal dead zone there. - **Q2, the seat's reading**: the refusal sentence is slot-neutral. `VALUE_ENVELOPE_REFUSAL` reads "A value carrying a `dialect` key is read as an expression envelope, and this one is not a valid CEL value envelope." The published `ASSIGNMENT_VALUE_ENVELOPE_REFUSAL` is kept and is the same string, and `AssignmentExpressionValueSchema`'s dialect message is neutral too. A refused field value is no longer told it is "an assignment value". - **Ratchet channel**: `LEDGER_DECLARED_NODE_CONFIG_SCHEMAS` carries both CRUD contracts. Their descriptors publish `fields` as `additionalProperties: true`, exactly like `assignment`, so the marker rides the spec Zod. - `resolveFlowNodeValueSlots`: every authored value of a value slot, strings included, located by the ledger's own walk. The lint hint uses it, so lint never re-spells the path shapes. - Generated artefacts are regenerated, never hand-edited: api-surface, export-origins, declaration-map, json-schema manifest and reference docs. The three new dropped-refinement sites (`CreateRecordConfig`, `UpdateRecordConfig`, `FlowValueSlot`) are declared, with the ledger header totals 207 → 210 and 574 → 577, the build's own reading. - Pins: the census, the fields entries, the value-slot resolver, the CRUD value contract (accept, preserve, refuse at the field's path, slot-neutral sentence), and the ratchet pin in `config-expression-ledger.test.ts`. ### Commit 2 (`8e37b80ff`): engine, the executor half (#11182) - **Executor** (`crud-nodes.ts`, the `resolveFieldValues` function): each top-level `fields` value that is envelope-shaped goes through `AutomationEngine.evaluateValueEnvelope`, the call the `assignment` executor makes (one evaluator, one CEL scope, one notion of malformed). Every other value goes through `interpolate()` exactly as the whole map used to. A malformed envelope, or one that faults on the live values, fails the node and writes nothing. - **Consumers**: `engine.ts` `valueEnvelopeRefusals` and lint `checkDeclaredValue` read the slot-neutral `FlowValueSlotSchema` / `VALUE_ENVELOPE_REFUSAL`. Comments in `engine.ts`, `logic-nodes.ts`, `node-executor.zod.ts` and `validate-expressions.ts` no longer call `assignments.*` the only value slot. - **Author-time hint** (ruling D point 1): `objectstack validate` warns (never errors) when any value slot holds a `{…}` template expression, meaning arithmetic or a call to one of the six functions, and points it at the envelope. The warning states the one conversion trap: `/ 100` becomes `/ 100.0`. - **Wrong guidance fixed** (ruling D point 2): the `template.ts` docblock and the shipped `round()` arity refusal now prescribe `round(x * 100) / 100.0`. Measured on this tree: CEL answers `1234` for `round(x * 100) / 100` and `1234.57` for `/ 100.0` at `x = 1234.5678`, while the template dialect answers `1234.57` for both. `/ 100.0` is right in both dialects. The arity pin in `template-functions.test.ts` asserts the new prescription and the measured reason. - **Docs** (`flows.mdx`): value slots, the envelope in the Create Record example, the three refusal doors (the stale "faults at run time, tracked in #15430" paragraph is corrected), the dialect table's scale-2 row, and a CEL-envelope row. - **Changeset**: minor for `@objectstack/spec`, `@objectstack/service-automation` and `@objectstack/lint`, with `Clause-②: yes (widening)` (Q3, the seat's reading). It names what newly passes, what newly refuses, and the nested and literal rule. - `skills/objectstack-automation/SKILL.md` (Tier H) is untouched, per ruling D point 2. The merge of `origin/main` (`94216fb72`) went through `scripts/pm/os-regen-merge.sh`. It resolved without conflicts, `check:generated` stayed at 15/15 current afterwards, and the branch's delta against main contains only this change. ## Why the hint covers only template expressions The hint covers arithmetic and the six functions, where CEL is a strict superset and the only conversion trap is stated in the warning itself. It does not cover: - a plain `{var}` / `{var.path}` reference: CEL adds nothing, and an absent key would flip from `undefined` to a fault; - `NOW()` / `TODAY()`: CEL's `now()` / `today()` are timestamps with no `string(timestamp)`, and the string form is the v18 carrier's (#19939) to add; - `$User.*`: the flow's CEL scope binds no user. Hinting any of these would steer authors toward metadata that the runtime honours but that makes the value worse. Census, a heuristic scan of object-literal `fields` / `assignments` blocks: the hint fires on 0 flow sites outside tests in objectstack (`94216fb72`) and on 2 in HotCRM (`2f7b232`, the two `quote-generation.flow.ts` money fields #11182 measured). ## Measured: the 42-row probe grid, base against after The grid is 7 values × {create_record, update_record} × {number, text, json} columns. The doors are `FlowSchema.safeParse`, the `os validate` pipeline (`normalizeStackInput` → unknown-key lints → `ObjectStackDefinitionSchema` → `runAuthoringRules('validate')`), `registerFlow`, and a run over a real ObjectQL with a recording driver. - **Base (`455dcc060`)** reproduces the prior report exactly. Every envelope passes every door and is written as a literal object: text and JSON columns report success, and the number column is refused by the data engine. - **After**: - The 18 template and literal rows are byte-identical to base. - The 6 valid-envelope rows are evaluated (`price * 2` writes `42` on the number, text and JSON columns). - The 18 malformed rows (no `source`, non-parsing CEL, a `template` dialect) are refused at validate (`error` / `expression-invalid`) and at `registerFlow`, located at `config.fields.FIELD` with the slot-neutral sentence. - `FlowSchema.parse` is unchanged, because node config is an open record. - **Runtime publish gate (`metadata-protocol`)**, not measured before and measured now through `saveMetaItem` over the protocol's stub-engine harness: - A malformed `fields.*` envelope goes from SAVED to `422 INVALID_METADATA` with `expression-invalid` at the node. - A valid envelope still saves. - A `{round(price * 100) / 100}` template saves with the hint as an advisory, in all three value slots. - The `assignment` control rows were refused before and after; only the sentence changed. - **Nested values** (mechanism assumption 3): only the top-level value of a field is judged. An envelope-shaped object nested in a JSON value or an array is data, is interpolated as before, and is written verbatim, which the executor test pins. A top-level JSON value that is itself an object with a string `dialect` is now an envelope; the changeset states the rule and the escape (bind it to a variable and write `'{thatVariable}'`). No flow in this repo or HotCRM writes an envelope-shaped object into `fields`. The heuristic scan found 0 outside tests; its control leg found 14 envelope-shaped `assignments` values in objectstack tests. ## Reverse verification (one-off, from the committed state) - The executor half was reverted to the old whole-map `interpolate()` at both sites through `scripts/ablation-replace.mjs`: anchor 2 → 0, blob `a292a5ac9181` → `1d6328c8d76d`. The executor test went from 26/26 to 8 failed / 18 passed: the six evaluation pins and two run-time-fault pins went red, and the preservation and registration pins stayed green because registration is ledger-driven. The restore was proven: the blob equals HEAD and `git diff HEAD` is empty. - The lint hint was short-circuited. The lint test went to 4 failed / 22 passed, all four of them the hinted cases, and the restore was proven. - The two ledger rows were deleted. The spec pins went to 6 failed / 127 passed (census, the two slot declarations, the two field resolutions, the value-slot resolver), and the restore was proven. ## Verification at `94216fb72` - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 110 commands. `--ran` reports 110 derived, 110 run, 0 NOT-MEASURED, 0 UNRUN. Three gates first refused with exit 3 (prerequisite not met) and passed after the builds they asked for: `check:skill-examples`, `check:dual-build-cjs-loads` and `check:type-check-debt`, the last one re-run after direct rebuilds because the ablation restores had touched mtimes. - Package suites, all command-exit 0: - `@objectstack/spec`: 541 files, 15924 tests - `@objectstack/service-automation`: 146 files, 1757 tests - `@objectstack/lint`: 109 files, 4209 tests - `@objectstack/metadata-protocol`: 189 files passed, 3 skipped (2700 tests) - `typecheck` is green for all four. - The other direct consumers of the changed exports are green: `examples/app-showcase/test/predicate-write-bulk-intent.test.ts` (17/17, parses with `UpdateRecordConfigSchema`) and `packages/cli/src/flow-node-undeclared-field-write.integration.test.ts` (7/7, drives `registerCrudNodes`). ## Acceptance notes - **`registerFlow` has no ADR-0112 envelope.** Every expression refusal there throws one aggregated plain `Error` with no `code` or `status`, and this is pre-existing and door-wide. The registration pins therefore assert the located message substance (node, slot, path, sentence). The `os validate` pins assert the rule id `expression-invalid` and severity `error`, and the publish gate answers `422` / `INVALID_METADATA`. - **One excuse was added to the `validate-expressions.test.ts` meta-guard: `grammar`.** It is an import-specifier artefact, not a receiver. The guard's scan `RULE_CODE.matchAll(/\b([a-z][\w$]*)\??\.[A-Za-z_$]/g)` reads `grammar.js` inside `'./flow-template-grammar.js'` as a receiver, the same artefact it already excuses as `scope`, `fields` and `guards`. The import is already a named import, so the construct the scanner misreads is the module path itself. My new locals were renamed instead of excused: the value-slot loop reuses the excused `found` (the same resolver-result shape), and the token scan uses a `RegExp.exec` loop with no lowercase receiver. What the guard checks for real receivers is unchanged. The engine commit was amended so that this fix sits inside it and each commit is green on its own. - **File surface beyond the claims' lists**, each a test of a claimed file: - `template-functions.test.ts`: the arity-refusal prescription pin for `template.ts`; - `validate-expressions.test.ts`: the meta-guard entry above; - two new test files: `crud-fields-value-envelope.test.ts` and `validate-expressions.fields-value-slot.test.ts`. - **Observations, not filed:** - `flow-field-expression-scale.integration.test.ts:23` still calls `round(x * 100) / 100` "the CEL-identical authoring pattern" in a test comment. The oracle it runs is the template dialect, where that is correct. Carrier: #19939. - `dropped-refinements.baseline.json`'s unpinned `measured.refinementSitesThatDidProject` reads 369 while the build measures 409. It was already stale before this change and is left as found. - A number written into a text column is accepted by ObjectQL, for literal `42` at base as well. This carries the prior report's observation forward. --- _Generated by [Claude Code](https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2bcd5cf commit e462186

25 files changed

Lines changed: 1223 additions & 215 deletions
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/service-automation": minor
4+
"@objectstack/lint": minor
5+
---
6+
7+
`create_record` / `update_record` field values accept the CEL value envelope, declared and evaluated together.
8+
9+
A value in a `create_record` or `update_record` node's `fields` map may now be a CEL value envelope, `{ dialect: 'cel', source: '…' }`, with the same shape and dialect rules the `assignment` node's `assignments` map already has. The envelope is evaluated by the expression engine that flow conditions use, so the whole CEL stdlib is reachable from a field value, and the result is written with its type kept:
10+
11+
```ts
12+
fields: {
13+
subject: 'Quote for {account.name}', // `{token}` template — unchanged
14+
total: { dialect: 'cel', source: 'round(amount * 100.0) / 100.0' }, // CEL, evaluated to the value written
15+
}
16+
```
17+
18+
Clause-②: yes (widening) — a published authoring slot's accept set grows (a valid envelope in `fields.*` is newly evaluated), and the one newly refused shape is the edge the `assignments` map accepted when it gained the envelope: a malformed one.
19+
20+
**What newly passes.** A valid CEL value envelope as a top-level `fields` value, on both nodes. Before this release the executor wrote such an object into the record verbatim: a text or JSON column stored `{"dialect":"cel","source":"…"}` and the run reported success, and a number column was refused by the data engine.
21+
22+
**What newly refuses.** A top-level `fields` value that is a plain object with a string `dialect` key and is NOT a valid CEL value envelope. That covers a missing, empty or whitespace-only `source`, an `ast` with no `source`, a `template` or `cron` dialect, and a `source` that does not parse as CEL. Every door refuses it, located at `config.fields.<field>`: `AutomationEngine.registerFlow` refuses the flow, `objectstack validate` reports an `expression-invalid` error, the runtime publish gate answers `422 INVALID_METADATA`, and the node's execute-time contract parse refuses it. Such an object used to be written as data.
23+
24+
**The rule for nested and literal values.** Only the top-level value of each field is judged. An object nested inside a JSON value or an array is data, whatever keys it carries, and strings inside it still interpolate. A plain string is always a `{token}` template with its existing meaning, and every other literal is written as before. A JSON column whose intended literal value is itself an object with a string `dialect` key is now read as an envelope. To write such an object as data, bind it to a flow variable and write `'{thatVariable}'` (a sole token keeps its type). Measured: no flow in this repository or in HotCRM writes an envelope-shaped object into `fields`.
25+
26+
**The refusal sentence is slot-neutral.** A refused field value used to be told it was "an assignment value". The sentence every value-slot refusal leads with is now `VALUE_ENVELOPE_REFUSAL`: "A value carrying a `dialect` key is read as an expression envelope, and this one is not a valid CEL value envelope." The published `ASSIGNMENT_VALUE_ENVELOPE_REFUSAL` is kept and is the same string, so code that matches on the constant keeps matching. Code that matched the old literal text ("An assignment value carrying…") does not.
27+
28+
**New in `@objectstack/spec/automation`** (5 exports, 0 removed):
29+
30+
- `VALUE_ENVELOPE_REFUSAL`, the slot-neutral refusal sentence.
31+
- `FlowValueSlotSchema` / `FlowValueSlot` / `FlowValueSlotParsed`, the value contract every value slot shares (`AssignmentValueSchema` is the same rule under the assignment map's description).
32+
- `resolveFlowNodeValueSlots(nodeType, config)`, which returns every authored value in the ledger's value slots, strings included.
33+
- The expression ledger `FLOW_NODE_EXPRESSION_PATHS` has two new rows, `create_record.fields.*` and `update_record.fields.*` (role `value`), and `LEDGER_DECLARED_NODE_CONFIG_SCHEMAS` carries both CRUD contracts.
34+
35+
**Author-time hint (`@objectstack/lint`).** `objectstack validate` warns when a value slot holds a `{…}` template expression, meaning arithmetic or a call to `round` / `floor` / `ceil` / `abs` / `min` / `max`, and points it at the envelope. The warning never fails a build, and the template form keeps working unchanged. Plain references, the `NOW()` / `TODAY()` macros and `$User` paths are not hinted. CEL's `now()` / `today()` are timestamps rather than the strings those macros write, and the flow's CEL scope binds no user.
36+
37+
**Corrected guidance: `/ 100.0`, not `/ 100`.** The template dialect's `round()` arity refusal used to call `round(x * 100) / 100` the CEL authoring pattern. In CEL that expression truncates: `round()` returns an int, and int / int is integer division, so `x = 1234.5678` gives `1234` instead of `1234.57`. The refusal now prescribes `round(x * 100) / 100.0`, which is correct in both dialects. In the template dialect `/ 100` and `/ 100.0` give the same value.

‎content/docs/automation/flows.mdx‎

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -200,28 +200,35 @@ or missing-`required` violation (#4277). A node type that publishes no
200200
A value's **shape** selects its form — there is no mode key. A plain string is
201201
always `{token}` interpolation (a bare `a + b` is the literal text `a + b`, not
202202
CEL); an object that names a `dialect` is an expression envelope and must be a
203-
valid `cel` one — a missing, empty or non-string `source`, or a `template` /
204-
`cron` dialect, is refused at the variable's path. Numbers, booleans, arrays
205-
and plain objects are assigned as literals. A later `notify` node renders the
206-
variable as any other: `message: '{digest}'`, and it renders the **evaluated**
207-
value.
203+
valid `cel` one — a missing, empty, whitespace-only or non-string `source`, an
204+
`ast` with no `source`, or a `template` / `cron` dialect, is refused at the
205+
variable's path. Numbers, booleans, arrays and plain objects are assigned as
206+
literals. A later `notify` node renders the variable as any other:
207+
`message: '{digest}'`, and it renders the **evaluated** value.
208+
209+
The same rules hold for the field values of `create_record` / `update_record`
210+
(below): the `assignments` map and the `fields` map are the flow's **value
211+
slots**, and each accepts a CEL value envelope beside `{token}` templates and
212+
literals. Only a slot's top-level value is judged — an object nested inside a
213+
JSON value or an array is data, whatever keys it carries.
208214

209215
<Callout type="info" title="Where a malformed envelope is refused">
210216

211217
A malformed envelope never reaches run time silently: the same refusal runs at
212-
`objectstack validate` (a located finding naming the variable), at the runtime
213-
publish gate a Studio / REST / MCP flow write goes through, and at
214-
`registerFlow`, which refuses to register the flow. All three ask the same two
215-
questions in the same order — is it a valid `cel` envelope
216-
(`AssignmentValueSchema`), and does its source parse as CEL
218+
`objectstack validate` (a located finding naming the variable or field), at the
219+
runtime publish gate a Studio / REST / MCP flow write goes through (a `422`),
220+
and at `registerFlow`, which refuses to register the flow — in every value
221+
slot. All three ask the same two questions in the same order — is it a valid
222+
`cel` envelope (`FlowValueSlotSchema`), and does its source parse as CEL
217223
(`validateExpression`) — so a flow that registers is a flow whose envelopes
218224
those two validators accept.
219225

220-
Two shapes sit outside what either validator can judge and fault loudly at run
221-
time instead of assigning a value: an `ast`-only envelope (no `source` — the
222-
CEL engine evaluates `source`), and a whitespace-only `source`, which passes
223-
`min(1)` and reads as "not authored" to the validator while the engine parses
224-
it untrimmed. Both are tracked in [#15430].
226+
That includes the two shapes the persistence contract alone would accept but no
227+
engine can run — an `ast`-only envelope (the CEL engine evaluates `source`) and
228+
a whitespace-only `source` — refused at authoring since [#15430]. What remains
229+
for run time is an envelope that parses but cannot evaluate on the live values
230+
(an absent variable, say): it fails the run with its source attached, and
231+
nothing is assigned or written in its place.
225232

226233
[#15430]: https://github.com/objectstack-ai/objectstack/issues/15430
227234

@@ -240,6 +247,9 @@ it untrimmed. Both are tracked in [#15430].
240247
title: 'Follow up on {record.name}',
241248
assignee: '{record.owner}',
242249
due_date: '{TODAY() + 7}', // braces required — without them this writes the literal text
250+
// CEL value envelope — evaluated to the value written, same rules as an
251+
// assignment value. `100.0`, not `100`: CEL divides two integers as integers.
252+
estimate: { dialect: 'cel', source: 'round(record.amount * 0.15 * 100.0) / 100.0' },
243253
},
244254
},
245255
}
@@ -1785,7 +1795,16 @@ means the same thing inside braces.
17851795
| Start-node `condition` | **CEL** (bare, no braces) | `record.amount > 500` | `record.*`, `previous.*`, bare field names, `vars.*` |
17861796
| Edge `condition` | **CEL** (bare, no braces) | `record.status == 'open'` | same as above |
17871797
| Decision-node `conditions[].expression` | **CEL** (bare, no braces) | `order_amount > 10000` | flow variables by name, and `vars.*` |
1788-
| Field values in `create_record` / `update_record` | **Interpolation** (braces required) | `'Follow up on {record.name}'`, `'{TODAY() + 7}'` | `{var}`, `{var.path}`, `{$User.Id}`, `{$User.Email}`, `{NOW()}`, `{TODAY()}`, `{TODAY() + 90}` (whole days), and the CEL-mirrored numeric functions `round`, `floor`, `ceil`, `abs`, `min`, `max` (#11060) — `round` is **integer-only**, exactly like CEL's (there is no `round(x, 2)`); for N decimals write the CEL idiom `{round(x * 100) / 100}` (scale 2) |
1798+
| Field values in `create_record` / `update_record` | **Interpolation** (braces required) | `'Follow up on {record.name}'`, `'{TODAY() + 7}'` | `{var}`, `{var.path}`, `{$User.Id}`, `{$User.Email}`, `{NOW()}`, `{TODAY()}`, `{TODAY() + 90}` (whole days), and the CEL-mirrored numeric functions `round`, `floor`, `ceil`, `abs`, `min`, `max` (#11060) — `round` is **integer-only**, exactly like CEL's (there is no `round(x, 2)`); for N decimals write `{round(x * 100) / 100.0}` (scale 2). Keep the decimal point: in CEL `round()` returns an int and `int / int` is integer division, so `round(x * 100) / 100` drops the decimals there — `/ 100.0` is right in both dialects |
1799+
| Field values and assignment values, as a **CEL value envelope** | **CEL** (in an envelope) | `{ dialect: 'cel', source: 'round(price * 100.0) / 100.0' }` | flow variables by name, and `vars.*` — the whole CEL stdlib (`joinNonEmpty`, …) |
1800+
1801+
A value slot takes either form, chosen by shape: a string is interpolation, an
1802+
object naming a `dialect` is a CEL envelope. The template form keeps working
1803+
unchanged; `objectstack validate` points a template **expression** — arithmetic
1804+
or one of the six functions inside braces — at the envelope with a warning,
1805+
never an error. Plain references (`{record.name}`), the date macros and
1806+
`{$User.*}` are left alone: CEL's `now()` / `today()` are timestamps, not the
1807+
strings the macros write, and the flow's CEL scope binds no user.
17891808

17901809
<Callout type="warn">
17911810
**The failure modes to memorize:**

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ the contract on what a value may be. The form↔Zod ledger test still pins the
6969
descriptor's free-form `assignments` map as the openness it is; that pin and
7070
this contract describe the same surface from the two sides.
7171

72+
The `create_record` / `update_record` `fields` map carries the same value
73+
contract since #19938 (`FlowValueSlotSchema`, the "value slots" section):
74+
a field value may be a CEL value envelope beside a `{token}` template or a
75+
literal, and the three maps are the expression ledger's `value`-role slots.
76+
7277
Deliberately absent:
7378
- `decision` / `script` / `subflow` / `wait` / `connector_action` — the
7479
descriptor-schemaless class (config-schemas.test.ts). `wait` and
@@ -86,8 +91,8 @@ Deliberately absent:
8691
## TypeScript Usage
8792

8893
```typescript
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';
94+
import { AssignmentConfigSchema, AssignmentExpressionValueSchema, AssignmentValueSchema, CreateRecordConfigSchema, DeleteRecordConfigSchema, EndConfigSchema, FlowValueSlotSchema, GetRecordConfigSchema, MapConfigSchema, ScreenConfigSchema, ScreenFieldConfigSchema, UpdateRecordConfigSchema } from '@objectstack/spec/automation';
95+
import type { AssignmentConfig, AssignmentExpressionValue, AssignmentValue, CreateRecordConfig, DeleteRecordConfig, EndConfig, FlowValueSlot, GetRecordConfig, MapConfig, ScreenConfig, ScreenFieldConfig, UpdateRecordConfig } from '@objectstack/spec/automation';
9196

9297
// Validate data
9398
const result = AssignmentConfigSchema.parse(data);
@@ -108,7 +113,7 @@ const result = AssignmentConfigSchema.parse(data);
108113

109114
## AssignmentExpressionValue
110115

111-
CEL value envelope `{ dialect: 'cel', source }` — evaluated by the expression engine to the value the variable takes; the whole CEL stdlib (`joinNonEmpty`, …) is reachable
116+
CEL value envelope `{ dialect: 'cel', source }` — evaluated by the expression engine to the value the slot takes; the whole CEL stdlib (`joinNonEmpty`, …) is reachable
112117

113118
### Properties
114119

@@ -136,7 +141,7 @@ Value the variable takes: a string (`{token}` flow interpolation — a sole toke
136141
| Property | Type | Required | Description |
137142
| :--- | :--- | :--- | :--- |
138143
| **objectName** | `string` | ✅ | Object to insert into |
139-
| **fields** | `Record<string, any>` | optional | Field values to write on the new record |
144+
| **fields** | `Record<string, any>` | optional | Field values to write on the new record: each key is a field name, each value a `{token}` template, a CEL value envelope, or a literal |
140145
| **outputVariable** | `string` | optional | Flow variable bound to the created record |
141146

142147

@@ -165,6 +170,13 @@ Value the variable takes: a string (`{token}` flow interpolation — a sole toke
165170
| **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. |
166171

167172

173+
---
174+
175+
## FlowValueSlot
176+
177+
A value: a string (`{token}` flow interpolation — a sole token keeps its type), a CEL value envelope `{ dialect: 'cel', source }` evaluated by the expression engine (the CEL stdlib such as `joinNonEmpty` is reachable), or any other literal
178+
179+
168180
---
169181

170182
## GetRecordConfig
@@ -272,7 +284,7 @@ Value the variable takes: a string (`{token}` flow interpolation — a sole toke
272284
| :--- | :--- | :--- | :--- |
273285
| **objectName** | `string` | ✅ | Object to update |
274286
| **filter** | `Record<string, any>` | optional | Field/value pairs identifying the record(s) to update |
275-
| **fields** | `Record<string, any>` | optional | Field values to write |
287+
| **fields** | `Record<string, any>` | optional | Field values to write: each key is a field name, each value a `{token}` template, a CEL value envelope, or a literal |
276288
| **multi** | `boolean` | optional | Declare bulk intent: update every row the filter matches (default false — a predicate update without it is refused by the engine) |
277289

278290

‎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 — 1522 schemas across 14 protocol modules
3+
description: Every schema published by @objectstack/spec — 1523 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) | 12 | 68 | Agents, tools, skills, RAG and knowledge sources, model registry, conversations. |
2323
| [API Protocol](/docs/references/api) | 32 | 429 | REST contracts, endpoints, routing, realtime, batch, discovery. |
24-
| [Automation Protocol](/docs/references/automation) | 14 | 74 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. |
24+
| [Automation Protocol](/docs/references/automation) | 14 | 75 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. |
2525
| [Data Protocol](/docs/references/data) | 29 | 175 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. |
2626
| [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. |
2727
| [Integration Protocol](/docs/references/integration) | 1 | 24 | The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances. |
@@ -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) | 34 | 275 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
3535
| [UI Protocol](/docs/references/ui) | 16 | 159 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
36-
| **Total** | **196** | **1522** | 14 protocol modules |
36+
| **Total** | **196** | **1523** | 14 protocol modules |
3737

3838
---
3939

@@ -105,15 +105,15 @@ REST contracts, endpoints, routing, realtime, batch, discovery.
105105

106106
## Automation Protocol
107107

108-
**Source:** `packages/spec/src/automation/` · **Import:** `@objectstack/spec/automation` · **14 pages, 74 schemas**
108+
**Source:** `packages/spec/src/automation/` · **Import:** `@objectstack/spec/automation` · **14 pages, 75 schemas**
109109

110110
Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records.
111111

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

0 commit comments

Comments
 (0)