Skip to content

Commit f915b87

Browse files
committed
docs(spec,service-automation): the Clause-② line reads yes (narrowing; widening) — recordId newly accepts an envelope — and three sentences the screen value slots made false (#19939 S2, patch round 1)
Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
1 parent 91688df commit f915b87

5 files changed

Lines changed: 19 additions & 14 deletions

File tree

‎.changeset/19939-flow-screen-value-slots.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
'@objectstack/service-automation': major
44
---
55

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.
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, which it refused before this change, and its literal stays a string.
77

8-
Clause-②: no (narrowing)
8+
Clause-②: yes (narrowing: a {…} token is refused at screen.defaults.*, a field's defaultValue and screen.recordId; widening: screen.recordId accepts a CEL value envelope it refused, and its published JSON Schema and inferred type gain the envelope arm)
99

1010
<!-- 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. -->
1111

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.*`).
12+
**BREAKING**: an accept-set narrowing on a published authoring surface, beside one widening (`recordId` accepts a CEL value envelope), 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.*`).
1313

1414
**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]`.
1515

@@ -36,7 +36,9 @@ The refusal at each position says what it serves.
3636

3737
**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.
3838

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.
39+
**Newly accepted.** A CEL value envelope at `recordId`, which `z.string()` refused before this change: the published JSON Schema for `recordId` gains the envelope arm (a string, or the envelope object), and so does the inferred `ScreenConfig['recordId']`.
40+
41+
**Still accepted, unchanged.** A CEL value envelope at `defaults` and at a field's `defaultValue`, and every literal at all three, 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.
4042

4143
### The kit
4244

‎packages/runtime/src/domains/automation.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3195,7 +3195,7 @@ export async function handleAutomationRequest(deps: DomainHandlerDeps, path: str
31953195
// wrong gate for this route (it would refuse the end user the flow
31963196
// paused for) — and left the door authenticated-only as a result. The
31973197
// residual it named was measured and is real: a `ScreenSpec` carries
3198-
// `defaults` / `defaultValue` interpolated against the live flow
3198+
// `defaults` / `defaultValue` resolved against the live flow
31993199
// variables (`builtin/screen-nodes.ts`), so a real screen flow over
32003200
// `{record.email}` / `{record.phone}` answered those values to ANY
32013201
// authenticated caller who knew a run id. The ruling of 2026-08-12

‎packages/services/service-automation/README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ a CEL value envelope, and text is a `{{ }}` template.**
188188
| Start-node `condition` | CEL — bare, no braces | `record.amount > 500` |
189189
| Edge `condition` | CEL — bare, no braces | `record.status == 'open'` |
190190
| Decision `conditions[].expression` | CEL — bare, no braces | `order_amount > 10000` |
191-
| Value slots — `create_record` / `update_record` `fields`, `assignment` values, and the maps handed to a callee: `subflow` / `map` `input`, `script` `inputs` | CEL value envelope; a plain string is the literal text it spells | `{ dialect: 'cel', source: "'Follow up on ' + record.name" }`, `'open'` |
191+
| Value slots — `create_record` / `update_record` `fields`, `assignment` values, the maps handed to a callee: `subflow` / `map` `input`, `script` `inputs`, and a `screen`'s `defaults`, field `defaultValue` and `recordId`, each evaluated before the screen is served | CEL value envelope; a plain string is the literal text it spells (`recordId`'s literal is the id, a string) | `{ dialect: 'cel', source: "'Follow up on ' + record.name" }`, `'open'` |
192192
| Text slots — `notify` `title` / `message`, `screen` `title` / `description`, `end` `message` | Template — `{{ }}` holes (ADR-0032 §3; a single-brace token is refused) | `'Deal won: {{ record.name }}'`, `'{{ record.amount \| currency }}'` |
193193

194194
A value slot refuses every `{…}` template token — `registerFlow()` and
@@ -208,8 +208,8 @@ Inside an object or list value, and in the legacy `assignment` shapes, an
208208
envelope is data: build the whole value as one envelope (a CEL map or list
209209
literal, its mixed values wrapped in `dyn(…)`), or move the assignment into the
210210
`assignments` map. Other value-like positions — a `filter`, `recipients`, an
211-
`http` payload, a screen's `defaults` — still read the single-brace dialect
212-
(`{record.id}`).
211+
`http` payload, a `map` or `loop` `collection` — still read the single-brace
212+
dialect (`{record.id}`).
213213

214214
A `subflow` or `map` `input`, and a `script`'s `inputs`, hand their values to a
215215
callee: each envelope is evaluated in the calling flow's scope (on a `map`, once

‎packages/spec/src/automation/builtin-node-config.zod.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -862,7 +862,10 @@ const SCREEN_RECORD_ID_REFUSAL =
862862
* `null` included, rather than serving an `edit` form with no record to
863863
* open (`service-automation`'s `screen-nodes.ts`);
864864
* - anything else is refused ({@link SCREEN_RECORD_ID_REFUSAL}); a malformed
865-
* envelope leads with {@link VALUE_ENVELOPE_REFUSAL}, as in every value slot.
865+
* envelope leads with {@link VALUE_ENVELOPE_REFUSAL}, as in every value
866+
* slot, at both build doors (their envelope pass). This union's own parse
867+
* leads with it too, except for an envelope the envelope arm parses and then
868+
* refuses (a blank `source`): the union reports that arm's own sentence.
866869
*
867870
* A union rather than {@link FlowValueSlotSchema}'s open `unknown`: the
868871
* literal arm is narrower here (an id is a string, as it always was), and the

‎packages/spec/src/automation/flow-text-slot-template.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,11 @@ export interface FlowNodeTextSlot {
168168
* one text renderer). Every OTHER string the flow interpolator reads keeps its
169169
* single-brace dialect: a value-like position whose single token hands its
170170
* resolved value over with its TYPE (`recipients`, `actionUrl`, `sourceId`,
171-
* `payload`, `templateData`, a screen's `recordId` / `defaults` / field
172-
* `defaultValue`, `http`, the `loop` / `map` `collection`, a `filter`) is not
173-
* a text template, and the value slots (`fields.*`, `assignments.*`, and the
174-
* callee maps `subflow.input.*`, `map.input.*`, `script.inputs.*`) are CEL's
175-
* (#19939).
171+
* `payload`, `templateData`, `http`, the `loop` / `map` `collection`, a
172+
* `filter`) is not a text template, and the value slots (`fields.*`,
173+
* `assignments.*`, the callee maps `subflow.input.*`, `map.input.*`,
174+
* `script.inputs.*`, and a screen's `defaults.*`, field `defaultValue` and
175+
* `recordId`) are CEL's (#19939).
176176
*/
177177
export const FLOW_NODE_TEXT_SLOTS: readonly FlowNodeTextSlot[] = [
178178
{ nodeType: 'notify', key: 'title', label: 'notify title' },

0 commit comments

Comments
 (0)