Skip to content

Commit 71f0150

Browse files
committed
wip: changeset and the regenerated builtin-node-config reference (#19939 S2)
Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
1 parent 53414ea commit 71f0150

2 files changed

Lines changed: 53 additions & 6 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
'@objectstack/spec': major
3+
'@objectstack/service-automation': major
4+
---
5+
6+
A `screen` node's three positions that hand a value to the client are value slots now: an object form's `defaults` (each field's prefill), a field's `defaultValue`, and an object form's `recordId` (the record an `edit` form opens). Each value is a CEL value envelope, `{ dialect: 'cel', source: '…' }`, that the executor evaluates in the run's scope before the screen goes on the wire, so the client receives the value it computes, never the envelope; or a literal, served as written. A `{…}` template token there is refused at `objectstack validate`, at `registerFlow` and by the executor, naming its CEL spelling, as in every other value slot. `recordId` gains the envelope arm, and its literal stays a string.
7+
8+
Clause-②: no (narrowing)
9+
10+
<!-- adr-0087: not-required (already-registered flow-value-slot-template-dialect-refused) The value-slot retirement's step-18 D3 entry, registered on this line before this change, is amended in this diff: its surface widens to a screen's defaults map, a field's defaultValue and recordId, and its replacement, reason and acceptance criteria say what each serves where the template served nothing. No new D3 entry and no D2 conversion: every whole-path spelling answers differently for an absent value. -->
11+
12+
**BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `major` on the v18 line (`.changeset/pre.json` is open on `main` in `next` pre mode, so the release is `18.0.0-next.*`).
13+
14+
**Why.** ADR-0032 Decision 2 makes a computed value whole-field CEL, and Decision 3 deletes the single brace. A screen's prefill and its record id are computed values handed to the client. Until now the executor ran each through the single-brace interpolator: a `{token}` resolved there, an envelope written in `defaults` or a `defaultValue` went to the client as the object `{ dialect: 'cel', source: '…' }` it spells, and `recordId` was the text of whatever the token found, so a token that found a record served the id `[object Object]`.
15+
16+
**What changes where a value may be absent.** Where a whole token resolved to nothing, the template served nothing there. Under CEL an absent variable or key fails the run at the screen, with its source, and the guarded form `has(vars.x) ? vars.x : null` serves `null`:
17+
18+
- In an object form's `defaults`, the form keeps a `null` prefill as a value and opens the field blank, over the object field's own `defaultValue`, which it applied when the prefill left the field out. To keep that default for an absent value, write it in the guard: `has(vars.x) ? vars.x : 'prospecting'`.
19+
- A field's `defaultValue` seeds the field with `null`.
20+
- `recordId` must evaluate to the record's id, a non-blank string. Anything else, `null` included, fails the run at the screen. The template served such a screen with no record id, and its `edit` form opened with no record and could not save. Where the record may be absent, route around the screen with a `decision` on the value instead of guarding the id.
21+
22+
The refusal at each position says what it serves.
23+
24+
## FROM → TO
25+
26+
| you wrote | write instead | what changes |
27+
|:--|:--|:--|
28+
| `defaults: { account: '{account_id}' }` | `defaults: { account: { dialect: 'cel', source: 'account_id' } }` | an absent `account_id` fails the run; guarded, it prefills `null` and the field opens blank over the object field's `defaultValue` |
29+
| `defaults: { stage: '{x}' }` where the object field's default should apply when `x` is absent | `defaults: { stage: { dialect: 'cel', source: "has(vars.x) ? vars.x : 'prospecting'" } }` | the default is written in the guard, because a `null` prefill is a value |
30+
| `defaults: { parent: '{rec}' }` | `defaults: { parent: { dialect: 'cel', source: 'rec' } }` | the record is served as a map, a list as a list |
31+
| `fields: [{ name: 'email', defaultValue: '{lead.email}' }]` | `fields: [{ name: 'email', defaultValue: { dialect: 'cel', source: 'lead.email' } }]` | guarded, it prefills `null` |
32+
| `recordId: '{record.id}'` | `recordId: { dialect: 'cel', source: 'record.id' }` | the envelope must evaluate to a non-blank string id: `null`, a map or a list fails the run at the screen |
33+
| `recordId: 'acc_1'` | unchanged | a string literal is the id |
34+
35+
**The one-line fix: write each value of a screen's `defaults`, each field's `defaultValue` and an object form's `recordId` as a CEL value envelope; write the object field's default into the guard where it should apply, and route around a screen whose record may be absent.**
36+
37+
**Who is affected, measured.** In this repository: the CRM example's lead conversion (`crm_convert_lead_wizard`: 3 `defaults` values in its two object-form steps), the object-form example in the flows guide, and test fixtures; all are migrated in this change. hotcrm's sites at these positions were not measured.
38+
39+
**Still accepted, unchanged.** A CEL value envelope and every literal, a string `recordId` included. The single-brace dialect keeps resolving where it still lives: a `map` or `loop` `collection`, a `filter` value, a `notify` `recipients` entry, an `http` body. The value-slot retirement's earlier changesets on this line list a screen's `defaults` among those positions; this one supersedes that line.
40+
41+
### The kit
42+
43+
- **The contract.** `ScreenConfigSchema.defaults` and a field's `defaultValue` take `FlowValueSlotSchema`, and `recordId` takes a string or a CEL value envelope (`@objectstack/spec/automation`), so the executor's contract parse refuses a token as a guard. Each published JSON Schema declares the dropped refinement (`dropped-refinements.baseline.json`).
44+
- **The ledger.** `FLOW_NODE_EXPRESSION_PATHS` gains `screen.defaults.*`, `screen.fields[].defaultValue` and `screen.recordId` (role `value`), and `LEDGER_DECLARED_NODE_CONFIG_SCHEMAS` carries `ScreenConfigSchema`. `registerFlow`, `objectstack validate` and `flow-bare-dollar-reference` / `flow-double-brace-interpolation`'s value-slot hints follow the ledger, so all of them cover the three positions. The build doors keep refusing a `recordId` literal that is not a string, as before.
45+
- **The executor.** The `screen` node resolves each position through the value-slot resolver the CRUD `fields` map and a callee's input share: an envelope is evaluated by `AutomationEngine.evaluateValueEnvelope` in the run's scope, before the screen is served. A screen that passes through without pausing goes on no wire, and evaluates nothing.
46+
- **ADR-0087.** The step-18 D3 entry `flow-value-slot-template-dialect-refused` is amended. No key is removed, so there is no tombstone, and there is no D2 conversion.

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,9 @@ this contract describe the same surface from the two sides.
7777
The `create_record` / `update_record` `fields` map carries the same value
7878
contract since #19938 (`FlowValueSlotSchema`, the "value slots" section):
7979
a field value is a CEL value envelope or a literal — a `{token}` template is
80-
refused there since #19939 — and the three maps are the expression ledger's
81-
`value`-role slots.
80+
refused there since #19939 — and those maps are the expression ledger's
81+
`value`-role slots, as are, since #19939, the three maps a node hands to a
82+
callee and a `screen`'s `defaults.*`, field `defaultValue` and `recordId`.
8283

8384
Deliberately absent:
8485
- `decision` / `script` / `subflow` / `wait` / `connector_action` — the
@@ -230,8 +231,8 @@ A value: a CEL value envelope `{ dialect: 'cel', source }` evaluated by the expr
230231
| **objectName** | `string` | optional | Render this object's full create/edit form instead of a flat field list |
231232
| **idVariable** | `string` | optional | Object form only: variable bound to the saved record's id — a name without a leading `$` (the `$` names are the flow engine's own), read as `{{ name }}` |
232233
| **mode** | `Enum<'create' \| 'edit'>` | optional (default: `"create"`) | Object form only: create (default) or edit; 'edit' needs a recordId to name its target |
233-
| **recordId** | `string` | optional | Object form only: id of the record to edit (required for mode: 'edit' to be useful) |
234-
| **defaults** | `Record<string, any>` | optional | Object form only: prefilled values |
234+
| **recordId** | `string \| { dialect: 'cel'; source: string; ast?: any; meta?: object }` | optional | Object form only: id of the record to edit (required for mode: 'edit' to be useful) — a string, or a CEL value envelope `{ dialect: 'cel', source }` evaluated when the screen is served to the id, a non-blank string (anything else, null included, fails the run at the screen); a `{…}` template token is refused |
235+
| **defaults** | `Record<string, any>` | optional | Object form only: prefilled values keyed by the object's field names — each a CEL value envelope `{ dialect: 'cel', source }` evaluated when the screen is served (the client receives the value, never the envelope), or a literal written as it is; a `{…}` template token is refused |
235236

236237
### Nested Shape: `ScreenConfig.fields[number]`
237238

@@ -242,7 +243,7 @@ A value: a CEL value envelope `{ dialect: 'cel', source }` evaluated by the expr
242243
| **type** | `string` | optional | Input type |
243244
| **required** | `boolean` | optional | Whether a value is required to submit |
244245
| **options** | `{ value: any; label: string }[]` | optional | Choices for a select-style field; option values must stay distinct when read as text |
245-
| **defaultValue** | `any` | optional | Prefilled value (interpolates `{token}` templates) |
246+
| **defaultValue** | `any` | optional | Prefilled value: a CEL value envelope `{ dialect: 'cel', source }` evaluated when the screen is served (the client receives the value, never the envelope), or a literal written as it is — a `{…}` template token is refused |
246247
| **placeholder** | `string` | optional | Input placeholder text |
247248
| **visibleWhen** | `string` | optional | CEL predicate controlling visibility, evaluated client-side |
248249
| **min** | `number` | optional | Minimum accepted value (numeric fields); enforced on resume |
@@ -264,7 +265,7 @@ A value: a CEL value envelope `{ dialect: 'cel', source }` evaluated by the expr
264265
| **type** | `string` | optional | Input type |
265266
| **required** | `boolean` | optional | Whether a value is required to submit |
266267
| **options** | `{ value: any; label: string }[]` | optional | Choices for a select-style field; option values must stay distinct when read as text |
267-
| **defaultValue** | `any` | optional | Prefilled value (interpolates `{token}` templates) |
268+
| **defaultValue** | `any` | optional | Prefilled value: a CEL value envelope `{ dialect: 'cel', source }` evaluated when the screen is served (the client receives the value, never the envelope), or a literal written as it is — a `{…}` template token is refused |
268269
| **placeholder** | `string` | optional | Input placeholder text |
269270
| **visibleWhen** | `string` | optional | CEL predicate controlling visibility, evaluated client-side |
270271
| **min** | `number` | optional | Minimum accepted value (numeric fields); enforced on resume |

0 commit comments

Comments
 (0)