You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit c200819
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: .changeset/19939-flow-notify-value-slots.md
+15-10Lines changed: 15 additions & 10 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,17 +4,19 @@
4
4
'@objectstack/lint': patch
5
5
---
6
6
7
-
A `notify` node's computed values are value slots now: `recipients`, the values of `templateData` and `payload`, `sourceObject`, `sourceId` and `actorId` take a CEL value envelope, `{ dialect: 'cel', source: 'record.owner' }`, that the executor evaluates in the run's scope, or a literal written as it is. `recipients` holds one recipient or a list of them, each entry a string or an envelope, and an entry that evaluates to a list adds each of its recipients. The node's link, `actionUrl`, is a text slot like `title`: it renders `{{ }}` holes, `'/crm_case/{{ record.id }}'`. A single-brace `{…}` token at any of these positions is refused at `objectstack validate`, at `registerFlow` and by the executor, naming its CEL spelling (its `{{ }}` spelling at `actionUrl`).
7
+
A `notify` node's computed values are value slots now: `recipients`, `sourceId` and the values of `templateData` take a CEL value envelope, `{ dialect: 'cel', source: 'record.owner' }`, that the executor evaluates in the run's scope, or a literal written as it is. `recipients` holds one recipient or a list of them, each entry a string or an envelope, and an entry that evaluates to a list adds each of its recipients. The node's link, `actionUrl`, is a text slot like `title`: it renders `{{ }}` holes, `'/crm_case/{{ record.id }}'`. Its `sourceObject`, `actorId` and `payload` are literals, read as written: no flow computed one, so nothing is evaluated there. A single-brace `{…}` token at any of these positions is refused at `objectstack validate`, at `registerFlow` and by the executor, naming its CEL spelling at a value slot, its `{{ }}` spelling at `actionUrl`, and, at a literal, the remedy to write the value.
8
8
9
-
Clause-②: yes (narrowing: a {…} token is refused at notify recipients (the value or a list entry), templateData and payload values, sourceObject, sourceId, actorId and actionUrl, and a templateData or payload value that names a dialect must be a valid CEL value envelope; widening: recipients, sourceObject, sourceId and actorId accept a CEL value envelope they refused, a recipients list accepts envelope entries, and their published JSON Schema and inferred types gain the envelope arm)
9
+
Clause-②: yes (narrowing: a {…} token is refused at notify recipients (the value or a list entry), templateData values, sourceId, actionUrl, sourceObject, actorId and any string of a payload value, a templateData value that names a dialect must be a valid CEL value envelope, and a CEL value envelope written as a payload value is refused; widening: recipientsand sourceId accept a CEL value envelope they refused, a recipients list accepts envelope entries, and their published JSON Schema and inferred types gain the envelope arm)
10
10
11
-
<!-- adr-0087: not-required (already-registered flow-value-slot-template-dialect-refused, flow-text-slot-single-brace-refused, flow-text-slot-unbound-dollar-root-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 notify node's computed values, and its replacement, reason and acceptance criteria say what a recipient evaluates to, that a list adds each recipient and null none, and why the list token is not rewritten. The two text-slot D3 entries widen their surface to the notify node's actionUrl. No new D3 entry and no D2 conversion: an absent variable dropped a recipient under the template and fails the run under CEL, a list token written as a list entry reached nobody under the template and reaches each recipient as an envelope, and a Date in a link rendered JSON-quoted under the interpolator and as its ISO text under the template engine. -->
11
+
<!-- adr-0087: not-required (already-registered flow-value-slot-template-dialect-refused, flow-text-slot-single-brace-refused, flow-text-slot-unbound-dollar-root-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 notify node's computed values and to its three literals, and its replacement, reason and acceptance criteria say what a recipient evaluates to, that a list adds each recipient and null none, why the list token is not rewritten, and that a literal takes the value itself. The two text-slot D3 entries widen their surface to the notify node's actionUrl. No new D3 entry and no D2 conversion: an absent variable dropped a recipient under the template and fails the run under CEL, a list token written as a list entry reached nobody under the template and reaches each recipient as an envelope, and a Date in a link rendered JSON-quoted under the interpolator and as its ISO text under the template engine. -->
12
12
13
13
**BREAKING**: an accept-set narrowing on a published authoring surface, beside one widening (four positions accept 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.*`).
14
14
15
15
**Why.** ADR-0032 Decision 2 makes a computed value whole-field CEL, and Decision 3 deletes the single brace; a link with holes is text. Until now the executor ran each of these positions through the single-brace interpolator, and one of its answers reached nobody: a whole token holding a list, written as an entry of a `recipients` list — `recipients: ['{owners}']` with `owners = ['u1', 'u2']` — was handed to the messaging service as ONE recipient, `'u1,u2'`, and the run reported success. A token holding a record there became the recipient `'[object Object]'`, and so did one in a link or a `sourceId`.
16
16
17
-
**What changes where a value may be absent.** A recipient envelope that evaluates to `null` or a blank string adds no recipient, as a token that resolved to nothing did, and a node left with none fails, naming the expressions that evaluated to nothing. An absent variable or key fails the run under CEL, naming it, where the template dropped the recipient; guard a field that may be unset, `has(record.assignee) ? record.assignee : null`. A relation hop on a scalar id (`record.account.owner` without the start node's `expand`) fails the evaluation itself, naming the key it could not read, where the template resolved it to nothing. `sourceObject`, `sourceId` and `actorId` must evaluate to a string; `null` leaves the key unset, and a half-specified click-through pair is still dropped. A recipient, or one of `sourceObject` / `sourceId` / `actorId`, that evaluates to anything else — a map, a number — fails the node, naming what it evaluated to.
17
+
**What changes where a value may be absent.** A recipient envelope that evaluates to `null` or a blank string adds no recipient, as a token that resolved to nothing did, and a node left with none fails, naming the expressions that evaluated to nothing. An absent variable or key fails the run under CEL, naming it, where the template dropped the recipient; guard a field that may be unset, `has(record.assignee) ? record.assignee : null`. A relation hop on a scalar id (`record.account.owner` without the start node's `expand`) fails the evaluation itself, naming the key it could not read, where the template resolved it to nothing. `sourceId` must evaluate to a string; `null` leaves the key unset, and a half-specified click-through pair is still dropped. A recipient, or a `sourceId`, that evaluates to anything else — a map, a number — fails the node, naming what it evaluated to.
18
+
19
+
**The literals.**`sourceObject`, `actorId` and the values of `payload` passed through the single-brace interpolator too, but no flow computed one: the census found no `{…}` token at any of them, in this repository or in hotcrm. So no value slot is built there. They are read as written, and a `{…}` token in one — in any string of a `payload` value, at any depth — is refused with the literal's remedy: a single-brace token is not resolved here; write the value. An envelope written there is refused too, since nothing evaluates it (a deeper one inside a `payload` value is data, as it always was).
18
20
19
21
**What does not change.** A literal at every value slot is written as it is: a recipient string (`'team:ops'`, a user id, an email address) is one recipient, as before, and is not split. `template`, `topic`, `channels` and `severity` are read raw, as before. An explicit `actionUrl` still wins over the link synthesized from `sourceObject` / `sourceId`, and a link that renders no text yields to it, as an empty one did.
20
22
@@ -27,21 +29,24 @@ Clause-②: yes (narrowing: a {…} token is refused at notify recipients (the v
27
29
|`recipients: ['{owners}']` (a list variable) |`recipients: [{ dialect: 'cel', source: 'owners' }]`| each of its recipients is notified, where the template sent one joined string |
28
30
|`recipients: ['{$User.Id}']`|`recipients: [{ dialect: 'cel', source: 'current_user != null ? current_user.id : null' }]`| none: a run with no user adds no recipient |
29
31
|`sourceId: '{record.id}'`|`sourceId: { dialect: 'cel', source: 'record.id' }`| an absent id fails the run instead of dropping the link |
32
+
|`sourceObject: '{record.object_name}'`, `actorId: '{record.owner}'`, `payload: { who: '{record.owner}' }`| the value itself: `sourceObject: 'crm_case'`| the key is a literal now; nothing computes it |
30
33
|`templateData: { name: '{record.name}' }`|`templateData: { name: { dialect: 'cel', source: 'record.name' } }`| the template gets `null` (a guard's answer) where the key was absent |
31
34
|`actionUrl: '/crm_case/{record.id}'`|`actionUrl: '/crm_case/{{ record.id }}'`| a `Date` in the link renders as its ISO text, not JSON-quoted |
32
35
33
-
**The one-line fix: write each `notify` value position as a CEL value envelope, `{ dialect: 'cel', source: 'record.owner' }`, and double the braces in `actionUrl`.**
36
+
**The one-line fix: write each `notify` value position as a CEL value envelope, `{ dialect: 'cel', source: 'record.owner' }`, double the braces in `actionUrl`, and write `sourceObject`, `actorId` and `payload` as the values themselves.**
34
37
35
38
**Who is affected, measured.** In this repository, at the base: the showcase example (16 recipient strings, 7 `sourceId`s and 4 links), the todo example (2 recipients and 2 `sourceId`s), the flows guide, the email-templates and common-patterns guides, two dogfood fixtures and test fixtures; all are migrated in this change, and a recipient field that may be unset is guarded. hotcrm at `1d7148bf2d` writes 19 notify nodes in 15 flow files, each with one `{…}` recipient (2 of them `{$User.Id}`), 35 `{…}``templateData` values and a link with holes; it is on spec 17.7.0, so nothing there moves until it upgrades.
36
39
37
-
**Newly accepted.** A CEL value envelope at `recipients` (the value, or each entry of a list), `sourceObject`, `sourceId` and `actorId`: their published JSON Schema gains the envelope arm, and so do the inferred `NotifyConfig` types. At a `templateData` or `payload` key an object naming a `dialect` was data before; it is an expression now, evaluated per run, and a malformed one is refused.
40
+
**Newly accepted.** A CEL value envelope at `recipients` (the value, or each entry of a list) and `sourceId`: their published JSON Schema gains the envelope arm, and so do the inferred `NotifyConfig` types. At a `templateData` key an object naming a `dialect` was data before; it is an expression now, evaluated per run, and a malformed one is refused. `sourceObject`, `actorId` and `payload` keep their published shapes (a string, a string, a map of any values), with no envelope arm.
41
+
42
+
**Newly refused at the literals.** A `{…}` token in `sourceObject`, in `actorId` or in any string of a `payload` value, and a CEL value envelope written as a `payload` value, which was handed on as the object it spells.
38
43
39
44
**Still accepted, unchanged.** Every literal at these positions. The single-brace dialect keeps resolving where it still lives: a `filter` value, an `http` body. The value-slot retirement's earlier changesets on this line, and the text-slot changeset, list `recipients`, `actionUrl`, `sourceId`, `payload` and `templateData` among those positions; this one supersedes that line.
40
45
41
46
### The kit
42
47
43
-
-**The contract.**`NotifyConfigSchema`: `recipients` takes a string, a list of strings and envelopes, or one envelope; `templateData`and `payload` take `FlowValueSlotSchema` per key; `sourceObject`, `sourceId`and `actorId` take a string or an envelope; `actionUrl` refuses a single-brace token through the text-slot judge. Each `describe()` is restated.
44
-
-**The ledger.**`FLOW_NODE_EXPRESSION_PATHS` gains six`notify` value rows. `recipients` carries `eachElement`: a list is read entry by entry, at its index (`recipients[1]`). `FLOW_NODE_TEXT_SLOTS` gains `notify.actionUrl`. `NotifyConfigSchema` joins `LEDGER_DECLARED_NODE_CONFIG_SCHEMAS`, which carries the markers to the reconciliation ratchet.
45
-
-**The executor.** The `notify` node evaluates each envelope through the shared value-slot resolver and renders `actionUrl` through the text renderer; it no longer imports the single-brace interpolator.
46
-
-**The lint.** The `flow-double-brace-interpolation` hint names `actionUrl` among the text slots; `objectstack validate` refuses a token at each position through the value-slot and text-slot judges, with no rule change.
48
+
-**The contract.**`NotifyConfigSchema`: `recipients` takes a string, a list of strings and envelopes, or one envelope; `templateData`takes `FlowValueSlotSchema` per key; `sourceId`takes a string or an envelope; `actionUrl` refuses a single-brace token through the text-slot judge; `sourceObject`, `actorId` and `payload` keep their literal types, and the contract refuses a token or an envelope there with the literal's remedy. That refusal is marked read-as-written, so the build doors' value arm reports it at `FlowSchema.parse`, `objectstack validate` and `registerFlow` as the executor's parse does. Each `describe()` is restated.
49
+
-**The ledger.**`FLOW_NODE_EXPRESSION_PATHS` gains three`notify` value rows. `recipients` carries `eachElement`: a list is read entry by entry, at its index (`recipients[1]`). `FLOW_NODE_TEXT_SLOTS` gains `notify.actionUrl`. `NotifyConfigSchema` joins `LEDGER_DECLARED_NODE_CONFIG_SCHEMAS`, which carries the markers to the reconciliation ratchet.
50
+
-**The executor.** The `notify` node evaluates each envelope through the shared value-slot resolver, renders `actionUrl` through the text renderer and reads the literals as written; it no longer imports the single-brace interpolator.
51
+
-**The lint.** The `flow-double-brace-interpolation` hint names `actionUrl` among the text slots, and at a notify literal both wrong-syntax hints say that neither brace dialect resolves there and name the value itself; `objectstack validate` refuses a token at each position through the value-slot and text-slot judges and the contract.
47
52
-**ADR-0087.** The step-18 D3 entries `flow-value-slot-template-dialect-refused`, `flow-text-slot-single-brace-refused` and `flow-text-slot-unbound-dollar-root-refused` are amended. No key is removed, so there is no tombstone, and there is no D2 conversion.
|**severity**|`Enum<'info' \| 'warning' \| 'critical'>`| optional | Severity forwarded to the messaging service |
107
-
|**sourceObject**|`string \| { dialect: 'cel'; source: string; ast?: any; meta?: object }`| optional | Object name of the record the notification links to (writes sys_notification.source_object) — a string, or a CEL value envelope`{ dialect: 'cel', source }` evaluated per run to one; a `{…}` template token is refused. Only takes effect together with sourceId — a half-specified click-through target is dropped at execute time, so the inbox never renders a dead link. |
107
+
|**sourceObject**|`string`| optional | Object name of the record the notification links to (writes sys_notification.source_object) — a literal, read as written: a single-brace`{token}` is not resolved here and is refused, as is a CEL value envelope; write the value. Only takes effect together with sourceId — a half-specified click-through target is dropped at execute time, so the inbox never renders a dead link. |
108
108
|**sourceId**|`string \| { dialect: 'cel'; source: string; ast?: any; meta?: object }`| optional | Record id the notification links to (writes sys_notification.source_id) — a string, or a CEL value envelope `{ dialect: 'cel', source }` evaluated per run to one (`record.id`); a `{…}` template token is refused. Only takes effect together with sourceObject — a half-specified click-through target is dropped at execute time, so the inbox never renders a dead link. |
109
-
|**actorId**|`string \| { dialect: 'cel'; source: string; ast?: any; meta?: object }`| optional | User id that caused the event (writes sys_notification.actor_id) — a string, or a CEL value envelope`{ dialect: 'cel', source }` evaluated per run to one (`current_user.id`); a `{…}` template token is refused|
109
+
|**actorId**|`string`| optional | User id that caused the event (writes sys_notification.actor_id) — a literal, read as written: a single-brace`{token}` is not resolved here and is refused, as is a CEL value envelope; write the value|
110
110
|**actionUrl**|`string`| optional | Explicit click-through URL, rendered per run with `{{ }}` holes like `title` (`/crm_case/{{ record.id }}`); a single-brace `{token}` is refused. Overrides the link synthesized from sourceObject/sourceId. |
111
-
|**payload**|`Record<string, any>`| optional | Extra values merged into the notification payload — each a CEL value envelope `{ dialect: 'cel', source }` evaluated per run, or a literal written as it is; a `{…}` template token is refused|
111
+
|**payload**|`Record<string, any>`| optional | Extra values merged into the notification payload — literals, read as written: a single-brace `{token}` in any string of one is not resolved here and is refused, as is a CEL value envelope written as a value; write the value|
0 commit comments