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
docs(skills): formula §5 binding-scope table says an unevaluable condition fails the write (#4814) (#4906)
The `objectstack-formula` skill's `previous` binding-scope table listed exactly
where `previous` is unbound and then closed with "makes the whole expression
unevaluable" — the pre-17 outcome (warn + hook does not fire). #4775 changed
that outcome to aborting the operation, so the table's rows changed meaning
without a word of the table changing.
Adds the #4775 rule (before*/after* same direction, error names hook and key,
onError is not an escape) and the #4800/B1 cell in full: one `previous.*`
condition fails every predicate bulk update of that object, with a diagnosis
instead of `No such key: previous`, and a record-change flow trigger is not a
way around it. Plus a pointer under the legacy → CEL table, since `OLD.x` /
`ISCHANGED(x)` are how a migrating author arrives at `previous.x`.
Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ
Co-authored-by: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: skills/objectstack-formula/SKILL.md
+35-5Lines changed: 35 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -308,11 +308,36 @@ roots (#4784) — one scope, one meaning, whichever surface reads it.
308
308
|:---|:---|
309
309
| Update hook `condition` (single-record write), validation rule on update | the stored pre-write row |
310
310
| Insert events (`beforeInsert` / `afterInsert`), validation rule on insert |**unbound** — there is no prior state |
311
-
| Predicate bulk update (`multi: true`) hook `condition`|**unbound** — one write matches N rows and the hook fires once, so there is no single prior record |
312
-
313
-
Referencing `previous` where it is unbound makes the whole expression
314
-
unevaluable — so write insert-event conditions over `record` alone, and keep
315
-
transition conditions to single-record writes.
311
+
| Predicate bulk update (`multi: true`) hook `condition`|**unbound** — one write matches N rows and the hook fires once, so there is no single prior record. `record` is the bare payload here too, so a *declared* field this write does not set is unevaluable as well |
312
+
313
+
⚠️ **An unevaluable condition ABORTS the operation (#4775).** Referencing
314
+
`previous` where it is unbound — like a typo'd key (`record.stauts`), a retired
315
+
field, or a comparison CEL has no overload for — does **not** degrade to "the
316
+
hook did not fire": it **fails the write**, with an error naming the hook and
317
+
the key. Until protocol 17 the gate emitted a `logger.warn` and returned
318
+
`false`, which is what makes this table load-bearing rather than stylistic: a
319
+
`before*` guard swallowed into `false` silently let writes through, and an audit
320
+
hook swallowed into `false` silently dropped records. Those are opposite
321
+
failures, so "the condition said no" and "the platform could not work out what
322
+
the condition says" are now different outcomes and the second one is loud —
323
+
`before*` and `after*` in the same direction, with no `onError` escape
324
+
(`onError` governs a handler that throws, and the condition is evaluated before
325
+
any handler runs). A condition that does not even **compile** aborts the same
326
+
way.
327
+
328
+
So write insert-event conditions over `record` alone, and keep transition
329
+
conditions to single-record writes — that mistake used to cost you a hook that
330
+
quietly never ran, and now costs you every write the hook is attached to.
331
+
332
+
**On a `multi: true` bulk update the cost lands on every batch (#4800/B1).**
333
+
One hook condition reading `previous.*` makes *every* predicate bulk update of
334
+
that object fail, and the failure names a hook that has nothing to do with the
335
+
write. Fail-loud takes no exception here, but the error is a diagnosis rather
336
+
than a raw `No such key: previous`: it says this is a predicate bulk write, that
337
+
the N matched rows have no single prior record to bind, and gives the two ways
338
+
out — rewrite the condition without `previous`, or target the write at one
339
+
record (update by id). A record-change flow trigger is **not** a way around it:
340
+
it binds the same lifecycle hook and receives the same unbound `previous`.
316
341
317
342
**`previous` is total over the object's declared fields.** A declared column the
318
343
driver never returned reads as `null`, not as a fault. Guard with `!= null`,
@@ -349,6 +374,11 @@ When migrating Salesforce-flavor metadata, apply these rules in order:
349
374
|`ISCHANGED(x)`|`previous.x != record.x`|
350
375
|`MONTH_DIFF`, `MID`, `LEFT`, `RIGHT`, `SUBSTITUTE`|_not in stdlib — propose addition_|
351
376
377
+
> ⚠️ `OLD.x` and `ISCHANGED(x)` both land on `previous.x`, which exists only
378
+
> where `previous` is **bound** — see §5. On an insert event, or on a
379
+
> `multi: true` predicate bulk update, it is not; since #4775 that does not
0 commit comments