From 7d8c856080d6632d539cabb8f013403e8c356577 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 08:39:42 +0000 Subject: [PATCH 1/7] skills(formula): collapse cron + template surfaces to one row each (FRM-F-06 / A-01 / G-05 / B-04 / F-07) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The frontmatter routes cron away ("Do not use for ... cron schedules") while the body taught it across four sections; the corpus authors zero of it (`cron` 0, `tmpl` 0, any formatter 0). Both are live surfaces, so each keeps ONE schema-pointing row with its carriers; the two quick-reference sections and the formatter table go. - FRM-F-06 / A-01: `### Cron surfaces`, `### Template surfaces`, `## Cron quick reference` and `## Template quick reference` collapse into one `### Cron and template surfaces` table, two rows, pointing at `shared/expression.zod.ts` for the full carrier list. - FRM-G-05: the deprecated `visibleOn` / `visibility` alias row leaves the CEL surfaces table; the two `Object.titleFormat` notes dedupe to one carrier cell. - FRM-B-04: the third "no js dialect" restatement goes; the canonical one in Core contract stays. - FRM-F-07: the view-list-filter prohibition now names the alternative it was withholding — date macro tokens, with the pointer to objectstack-query `rules/filters.md`. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1 --- skills/objectstack-formula/SKILL.md | 93 +++++------------------------ 1 file changed, 15 insertions(+), 78 deletions(-) diff --git a/skills/objectstack-formula/SKILL.md b/skills/objectstack-formula/SKILL.md index 265cb19ed6..c4c378f1bb 100644 --- a/skills/objectstack-formula/SKILL.md +++ b/skills/objectstack-formula/SKILL.md @@ -418,7 +418,6 @@ to the envelope. | `View` / `Page` | `visibleWhen` (form section/field, page component) | cel | | `Field` | `defaultValue` (envelope only; bare string = literal) | cel | | `ConditionalValidation` | `when` | cel | -| `View` / `Page` | `visibleOn` / `visibility` (deprecated aliases of `visibleWhen`, ADR-0089) | cel | | `Action` | `disabled` | cel (or boolean) | | `Hook` | `condition` | cel | | `SharingRule` | `condition` | cel | @@ -427,87 +426,25 @@ to the envelope. | `audit` / `metrics` / `tracing` | `condition` / `successCriteria` | structured \| cel | View list filters are **not** a CEL surface — they are structured JSON filter -rules (`ViewFilterRuleSchema`), so do not emit CEL there. +rules (`ViewFilterRuleSchema`), so do not emit CEL there. For "last 30 days" +style windows use the **date macro tokens** (`data/date-macros.zod.ts`) — +objectstack-query `rules/filters.md` has the token list. -### Cron surfaces (recurring schedules) +### Cron and template surfaces -All accept bare strings (auto-wrapped to `{dialect:'cron', source}`) or the -`` cron`...` `` helper. 5- or 6-field cron + aliases (`@daily`, `@hourly`, …). +Two more registered dialects ride the same envelope. Neither is CEL, both +accept a bare string (auto-wrapped at validate time) or their helper, and both +read the same variable scope. -| Surface | Field | -|:---|:---| -| `Job.schedule.expression` | canonical | -| `connector.schedule` | scheduled connector sync | -| `system/cache.schedule` | warmup | -| `system/disaster-recovery.schedule` | backup + drill | -| `automation/execution.cronExpression` | scheduled state | -| `api/export.cronExpression` | scheduled exports (×2) | - -### Template surfaces (`{{ path }}` interpolation) - -Mustache subset — a **field/variable path** plus an optional **whitelisted -formatter**: `{{ path }}` or `{{ path | formatter[:arg] }}`. No conditionals, -no arbitrary logic (move logic into a CEL field). Same variable scope as CEL. -Double braces only — single `{x}` is **not** a valid hole. - -**Formatters (7.6)** — value→string is defined per formatter (not implicit): - -| Formatter | Example | Output | -|:---|:---|:---| -| `currency[:CODE]` | `{{ record.amount \| currency }}` / `:EUR` | `$1,234.50` | -| `number[:decimals]` | `{{ record.n \| number:2 }}` | `1,234.50` | -| `percent[:decimals]` | `{{ record.rate \| percent }}` (0.42→) | `42%` | -| `date[:short\|long\|iso]` / `datetime[:…]` | `{{ record.due \| date:long }}` | locale date | -| `upper` / `lower` / `trim` | `{{ record.code \| upper }}` | `ABC` | -| `truncate:N` | `{{ record.body \| truncate:80 }}` | `…` | -| `default:'…'` | `{{ record.x \| default:'N/A' }}` | fallback | -| `json` | `{{ record.obj \| json }}` | JSON | - -```ts -tmpl`Deal {{ record.name }} — {{ record.amount | currency }} closes {{ record.close_date | date:long }}` -``` - -| Surface | Field | -|:---|:---| -| `Object.titleFormat` | record title — **deprecated** (→ `nameField`, ADR-0079) | -| `ai/model-registry` | `promptTemplate.system`, `promptTemplate.user` | -| `system/email-template` | `subject`, `bodyHtml`, `bodyText` (plain strings, `{{ }}` rendered by the email pipeline) | - -There is no JS expression surface: procedural JS is the L2 -`ScriptBody { language: 'js' }` surface (hook bodies), not an expression -dialect. - ---- - -## Cron quick reference - - -```ts -import { cron } from '@objectstack/spec'; - -schedule: cron`0 6 * * MON` // every Monday at 06:00 -schedule: cron`@daily` // alias — every midnight -schedule: cron`*/15 * * * *` // every 15 minutes -``` - -Bare strings work too on cron-typed fields, but the `cron` helper makes intent -explicit. - ---- - -## Template quick reference - - -```ts -import { tmpl } from '@objectstack/spec'; - -subject: tmpl`Deal {{record.name}} needs review, {{os.user.name}}` -body: tmpl`{{record.name}} closes {{record.close_date | date:long}}` -``` +| Dialect | Helper | Grammar | Carriers | +|:---|:---|:---|:---| +| `cron` | `` cron`0 6 * * MON` `` | 5- or 6-field cron plus `@daily` / `@hourly` aliases | `Job.schedule.expression` (canonical), `connector.schedule`, `automation/execution.cronExpression`, `api/export.cronExpression` | +| `template` | `` tmpl`Hello {{ record.first_name }}` `` | `{{ path }}` or `{{ path \| formatter[:arg] }}` — double braces only, whitelisted formatters, no conditionals | `system/email-template` `subject` / `bodyHtml` / `bodyText`, `ai/model-registry` `promptTemplate.system` / `.user`, `Object.titleFormat` (deprecated → `nameField`, ADR-0079) | -Missing paths render as empty string. `Date` instances are ISO-formatted. -(`Object.titleFormat` also takes a template but is deprecated — use `nameField`, -ADR-0079.) +Both surfaces are declared in `shared/expression.zod.ts`; read it for the full +carrier list, the formatter whitelist and the cron alias set. Missing template +paths render as the empty string. Move logic into a CEL field — a template +holds a path and a formatter, nothing else. --- From b0eb967d8ea55196d1365cc508cdc810b5eb7d7d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 08:40:27 +0000 Subject: [PATCH 2/7] =?UTF-8?q?skills(formula):=20diet=20section=205=20?= =?UTF-8?q?=E2=80=94=20drop=20cost=20narration=20and=20the=20pre-17=20hist?= =?UTF-8?q?ory=20(FRM-D-05=20/=20D-04=20/=20B-03)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Section 5 was 20.7% of the file (1,241 tok). The rules in it are load-bearing; the narration around them was not. - FRM-D-04: the pre-protocol-17 `logger.warn` history goes. Two rules were buried in it and survive in one sentence: `before*` and `after*` abort alike with no `onError` escape, and a condition that does not compile aborts the same way. - FRM-D-05: the `before*` exception restated the table row above it at 165 tokens — it is now one clause pointing back at that row. The per-row bulk semantics keep their ADR-0058 statement; the query-count and fetch-cost narration goes. - FRM-B-03: the `has()`-on-a-declared-field rule was stated once for `record` and again for `previous`. It is now one statement covering both, in the `has()` section where the rule lives. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1 --- skills/objectstack-formula/SKILL.md | 60 ++++++++--------------------- 1 file changed, 16 insertions(+), 44 deletions(-) diff --git a/skills/objectstack-formula/SKILL.md b/skills/objectstack-formula/SKILL.md index c4c378f1bb..9ba1b9491f 100644 --- a/skills/objectstack-formula/SKILL.md +++ b/skills/objectstack-formula/SKILL.md @@ -120,9 +120,11 @@ ADR-0068 — spec field docs write predicates like `current_user.positions`. `null`. To check for "value present and non-blank" use the stdlib helper `isBlank()` or compare to `null` explicitly. -Every predicate reads a record that is **total over the object's declared -fields**, so `has(record.)` is uniformly `true` and -tells you nothing at all. The idiom that reads like a guard is not one: +Every predicate reads a `record` — and, where §5 binds it, a `previous` — that +is **total over the object's declared fields**: a declared column the driver +never returned reads as `null`, not as a fault. So `has(record.)` +and `has(previous.)` are uniformly `true` and tell you nothing at +all. The idiom that reads like a guard is not one: ```text # WRONG — both has() calls are true on a NULL row, so this reaches `null < null`, @@ -314,20 +316,10 @@ roots — one scope, one meaning, whichever surface reads it. `previous` where it is unbound — like a typo'd key (`record.stauts`), a retired field, or a comparison CEL has no overload for — does **not** degrade to "the hook did not fire": it **fails the write**, with an error naming the hook and -the key. Until protocol 17 the gate emitted a `logger.warn` and returned -`false`, which is what makes this table load-bearing rather than stylistic: a -`before*` guard swallowed into `false` silently let writes through, and an audit -hook swallowed into `false` silently dropped records. Those are opposite -failures, so "the condition said no" and "the platform could not work out what -the condition says" are now different outcomes and the second one is loud — -`before*` and `after*` in the same direction, with no `onError` escape -(`onError` governs a handler that throws, and the condition is evaluated before -any handler runs). A condition that does not even **compile** aborts the same -way. - -So write insert-event conditions over `record` alone — that mistake used to cost -you a hook that quietly never ran, and now costs you every write the hook is -attached to. +the key — `before*` and `after*` alike, with no `onError` escape (`onError` +governs a handler that throws, and the condition is evaluated before any handler +runs). A condition that does not even **compile** aborts the same way. So write +insert-event conditions over `record` alone. **A transition condition needs no special handling for bulk writes.** Write it once, on an `after*` event, and it means the same thing whether the @@ -339,38 +331,18 @@ write carries an id or a predicate: P`previous.status != 'done' && record.status == 'done'` ``` -A predicate (`multi: true`) write is N record changes, so the platform evaluates -and fires every record-scoped declaration on it **per row**: `previous` is that -row's own pre-write state and `record` is that row's real state, not the bare -payload (ADR-0058, bulk-write addendum). The matched rows are read once for the -whole batch, so this costs one extra query, not one per row. Record-change flow -triggers ride the same dispatch, so an `record-after-update` flow's start -condition behaves identically. - -⚠️ **The exception is a `before*` hook, and it is not a bug to be fixed later.** -`beforeUpdate` / `beforeDelete` fire ONCE for the whole batch — they may still -rewrite the payload, and one `updateMany` carries one payload — so `previous` is -unbound there and a condition reading it fails the write. The error is a -diagnosis rather than a raw `No such key: previous`: it names the batch, says the -`before*` phase is why, and points at the matching `after*` event, where the -same condition evaluates per row exactly as authored. Put transition conditions -on `after*`; keep `before*` conditions to the incoming payload -(`record.`). +A predicate (`multi: true`) write is N record changes, so every record-scoped +declaration on it is evaluated **per row** — `previous` is that row's own +pre-write state, `record` its real state, not the bare payload (ADR-0058, +bulk-write addendum). Record-change flow triggers ride the same dispatch. The +one exception is the `before*` row of the table above, and it is not a bug to be +fixed later: put transition conditions on `after*`, and keep `before*` +conditions to the incoming payload (`record.`). Above ~10 000 matched rows the platform refuses a predicate write on an object with after-hooks rather than fan out that many handler runs inside one write — paginate the write. It is a refusal, never a silent downgrade to one hook call. -**`previous` is total over the object's declared fields.** A declared column the -driver never returned reads as `null`, not as a fault. Guard with `!= null`, -**never** with `has(...)`: a materialised `null` is a *present* key, so -`has(previous.spent)` is uniformly true for a declared field and tells you -nothing about its value. - -**Cost:** none of its own. `previous` rides on the prior row the engine already -fetches for update hooks; a condition that never mentions it adds no fetch at -all. - --- ## Mechanical translation table (legacy → CEL) From 4f74e131547d7f71e6393341b240854363a755b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 08:41:39 +0000 Subject: [PATCH 3/7] skills(formula): cut retired history, roadmap and self-restatement (FRM-D-01 / B-07 / B-06 / D-09 / D-07 / D-02 / B-05 / D-03 / G-03 / G-01 / G-02 / G-04 / E-03) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FRM-D-01 / B-07: "Strategic context" — why CEL was picked over the deleted Salesforce-flavor engine — was the first content after the H1. An author emitting a predicate does not need the engine-selection history. - FRM-B-06: ADR-0032 was stated three times, and "Verify your work" cross-refs the top-of-file note that carried it. One statement now, in "Verify your work", and it keeps both halves the callout had: build fails with a located did-you-mean, and the runtime throws. - FRM-D-09: the `type Expression` block transcribed `shared/expression.zod.ts`. One sentence and the pointer replace it. - FRM-D-07 / D-02: "you do not learn three languages" and the roadmap for emitting `ast` go; the envelope rule they both carried survives once. The auto-wrap fact moved to the cron/template row in the previous commit. - FRM-B-05: "Determinism contract" restated the seed pattern's own note. The three numbered conditions fold into that note. - FRM-D-03 / G-03: "Open questions" cited `CelExprSchema` twice; the symbol exists nowhere in the repo (0 hits outside this file). - FRM-G-01: the CLI is `os`, not `objectstack` — 5 mentions, now 0. - FRM-G-02: the frontmatter pins 17.x; the retired milestone numbers are gone. - FRM-G-04: a CEL string literal takes either quote; the corpus writes double quotes inside P` ` more than ten times, so "(always)" was false. - FRM-E-03: `* 100.0` was a cel-js workaround. `registerNumericCoercions` (`packages/formula/src/stdlib.ts`) registers the mixed `double op int` / `int op double` overloads, so `* 100` is correct now. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1 --- skills/objectstack-formula/SKILL.md | 94 ++++++++--------------------- 1 file changed, 24 insertions(+), 70 deletions(-) diff --git a/skills/objectstack-formula/SKILL.md b/skills/objectstack-formula/SKILL.md index 9ba1b9491f..8f01361b91 100644 --- a/skills/objectstack-formula/SKILL.md +++ b/skills/objectstack-formula/SKILL.md @@ -24,23 +24,11 @@ computation or boolean predicates: **CEL** (Google Common Expression Language). This skill is the canonical reference for AI authors emitting formula / condition / predicate / dynamic-seed metadata. -> **Strategic context.** The future authors of metadata are AI agents. -> CEL was chosen because it has (a) a formal grammar, (b) a public training -> corpus, (c) AST-first persistence, and (d) sandboxed bounded execution. -> The previous custom Salesforce-flavor engine was **deleted** in M9.5. -> > **Predicates / formulas are bare CEL — never wrap field references in `{…}` > braces.** The #1 authoring mistake is a condition like > `{record.rating} >= 4`: in CEL, `{…}` is a **map literal**, so it is a parse > error. Write bare CEL: `record.rating >= 4`. Braces are *only* for `{{ … }}` -> text templates (see Template surfaces). -> -> **As of 7.6 (ADR-0032) a malformed expression no longer fails silently.** -> It used to evaluate to `null`/`false` (a flow "fired" but did nothing). Now -> `objectstack build` **fails** with a located, corrective, schema-aware message -> (unknown `record.` → did-you-mean), and at runtime the engine **throws** -> (the flow/rule fails loudly). The `validate_expression` agent tool runs the -> same shared validator so you can check an expression *before* saving. +> text templates (see Cron and template surfaces). --- @@ -59,18 +47,9 @@ formula / condition / predicate / dynamic-seed metadata. ## Core contract -Every expression in metadata is the same envelope: - -```ts -type Expression = { - dialect: 'cel' | 'cron' | 'template'; - source?: string; - ast?: unknown; - meta?: { rationale?: string; generatedBy?: string }; -}; -``` - -**Three registered dialects**: +Every expression in metadata is the same envelope — `dialect` plus `source`, +with optional `ast` and `meta` — declared as `Expression` / `ExpressionInput` in +`shared/expression.zod.ts`. **Three registered dialects**: | Dialect | Engine | Purpose | Helper | Example | |:-----------|:-----------------------|:--------------------------------------------------|:--------------|:---------------------------------------| @@ -82,15 +61,8 @@ There is **no `js` dialect** — it was retired. Procedural JavaScript is the L2 `ScriptBody { language: 'js' }` authoring surface (hook bodies, mapping transforms — see objectstack-data), not an expression dialect. -**Authors emit the right dialect for the surface.** Bare strings on cron and -template fields are auto-wrapped at validate time, but emitting the full -envelope is preferred for clarity. `cron` and `template` use the same variable -scope as CEL — you do **not** learn three languages. - > **AI authors:** when emitting structured-output JSON for metadata, always -> emit the full envelope `{ dialect, source }` — do not emit bare strings. -> After M9.7 lands, you will emit `ast` directly. Until then, emit `source` -> and let `objectstack compile` parse it. +> emit the full envelope `{ dialect, source }` — never a bare string. --- @@ -105,7 +77,7 @@ scope as CEL — you do **not** learn three languages. | Equality | `==` / `!=` | | Logical | `&&` / `\|\|` / `!` | | Ternary | `cond ? a : b` | -| String literal | `'single quotes'` (always) | +| String literal | `'abc'` — CEL parses `"abc"` too | | Membership | `record.region in ['us', 'eu']` | | Key existence (NOT null-safety) | `has(record.foo)` | | Null check | `record.foo == null` or `isBlank(record.foo)` | @@ -210,7 +182,7 @@ If you need a helper that doesn't exist, prefer adding it to the stdlib (small, pure, dependency-free) over inlining a complex CEL expression. > **Only the functions above are callable.** An UNKNOWN function — `PRIOR()`, a -> legacy `ISBLANK()`, a typo'd `isBlnk()` — **fails `objectstack build`** with a +> legacy `ISBLANK()`, a typo'd `isBlnk()` — **fails `os build`** with a > "no matching overload" type error, rather than silently no-op'ing the > predicate at run time. Use `previous.x` (not `PRIOR()`), `isBlank()` (not `ISBLANK()`). @@ -240,7 +212,7 @@ F`record.salutation + ' ' + record.first_name + ' ' + record.last_name` ```ts F`coalesce(record.cost, 0) > 0 - ? ((coalesce(record.revenue, 0) - record.cost) * 100.0) / record.cost + ? ((coalesce(record.revenue, 0) - record.cost) * 100) / record.cost : 0.0` ``` @@ -258,9 +230,8 @@ For field-level conditional rules, emit the canonical field properties: `visibleWhen`, `readonlyWhen`, and `requiredWhen`. Never emit `conditionalRequired` — it was REMOVED in protocol 17 and is a parse error. -❌ Salesforce-flavor — **fails CEL compile**: `objectstack build` errors with a -located message, and the flow engine throws if it ever reaches runtime (see the -ADR-0032 note at the top of this skill): +❌ Salesforce-flavor — **fails CEL compile**: `os build` errors with a +located message, and the flow engine throws if it ever reaches runtime: ```ts "status = 'qualified'" @@ -282,8 +253,11 @@ ADR-0032 note at the top of this skill): { close_date: new Date(Date.now() + 45 * 86400000), created_at: new Date() } ``` -This is the determinism gate: `objectstack build` runs twice produce -byte-identical `dist/objectstack.json` only when seed dates use CEL. +This is the determinism gate: two consecutive `os build` runs produce +byte-identical `dist/objectstack.json` only when every dynamic seed value is +`` cel`...` `` — no `new Date()`, no `Date.now()`, no random or otherwise +impure source. The stdlib helpers honour the pinned `now` from `EvalContext`, +so they are safe inside one. ### 5. Update hook condition — `previous` vs `record` @@ -420,37 +394,17 @@ holds a path and a formatter, nothing else. --- -## Determinism contract - -Builds are deterministic only if: - -1. All seed dynamic values use `cel\`...\`` (no `new Date()`, no `Date.now()`). -2. CEL stdlib helpers honor the pinned `now` from `EvalContext`. -3. No expression source contains random / non-pure data. - -Two consecutive `objectstack build` runs must produce byte-identical -`dist/objectstack.json` — spot-check by diffing the artifact. - ---- - -## Open questions - -- Authors will emit `ast` directly once `CelExprSchema` is published as JSON - Schema for AI constrained decoding (M9.7). -- A visual node-graph editor backed by `CelExprSchema` is M9.8 (Studio). - ---- - ## Verify your work -A malformed expression no longer fails silently (ADR-0032, see the note near the -top of this skill): both `os validate` and `os build` run the shared validator -over every formula and predicate in the stack — CEL syntax **plus** -`record.` existence on the target object — and fail non-zero with a -did-you-mean. Use `os validate` as the fast post-edit check (no artifact emitted; -`npm run validate` in a scaffolded project). To check a *single* expression -before saving it, call the `validate_expression` agent tool, which runs the same -validator inline. +A malformed expression does not fail silently (ADR-0032). It used to evaluate to +`null` / `false` — a flow "fired" and did nothing; now `os validate` and +`os build` run the shared validator over every formula and predicate in the +stack — CEL syntax **plus** `record.` existence on the target object — +and fail non-zero with a located, did-you-mean message, while at runtime the +engine **throws** and the rule fails loudly. Use `os validate` as the fast +post-edit check (no artifact emitted; `npm run validate` in a scaffolded +project). To check a *single* expression before saving it, call the +`validate_expression` agent tool, which runs the same validator inline. --- From a85153ed5d6b8dfc016418ad794f37156934881c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 08:44:15 +0000 Subject: [PATCH 4/7] skills(formula): teach the three contract facts the package omitted (FRM-F-01 / F-03 / F-02 / F-04 / B-01 / B-02 / E-01 / D-08) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The corpus measurement behind these: `type: 'formula'` fields have zero live usages and the only hits are a removal test; flow/trigger conditions are authored five times and the scope was taught wrong; field conditional rules are the second-commonest task and two of their roots were missing. - FRM-F-01: a `formula` field is virtual — `where` / `orderBy` / `searchableFields` on one are refused `400 INVALID_FIELD` at both doors (`packages/spec/src/migrations/entries/semantic/17.engine-find-formula-filter-refused.ts`; `packages/objectql/src/filter-comparand-shape.ts`). The filter and sort axes were taught nowhere in the catalog. Remedy is the stored column, in the same words the sort and search axes already use. - FRM-F-03: a form-view `visibleWhen` is client-side only and faults OPEN (`packages/spec/src/ui/view.zod.ts` `FormFieldSchema` / `FormSectionSchema`; `packages/metadata-core/src/form-predicate-root-policy.ts`). The package taught fault-closed globally, so the note says where each holds. - FRM-F-02: the `parent` root (line-item cell `readonlyWhen` / `requiredWhen`, `packages/spec/src/data/field.zod.ts`) and the `data` root (metadata-editing forms, `BOUND_FORM_VIEW_PREDICATE_ROOTS`) join the cheat-sheet. - FRM-F-04: flow conditions bind bare identifiers — the automation engine spreads the record's variables to top level (`packages/services/service-automation/src/engine.ts`). The translation table's first row implied the opposite universally. - FRM-B-02 / B-01: the `node_modules/@objectstack/formula/` pointer named a package that ships `dist` only, which this file already says is unreadable. Its slot goes to `references/_index.md`, which every other domain package links and this one orphaned. - FRM-E-01: the predicate pattern block is now type-checked. Both previously marked blocks were the cron and template quick references, so no "Mandatory pattern" was checked at all. - FRM-D-08: the no-date-arithmetic rule carried five prescriptions as prose. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1 --- skills/objectstack-formula/SKILL.md | 50 +++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/skills/objectstack-formula/SKILL.md b/skills/objectstack-formula/SKILL.md index 8f01361b91..05d403647c 100644 --- a/skills/objectstack-formula/SKILL.md +++ b/skills/objectstack-formula/SKILL.md @@ -72,6 +72,8 @@ transforms — see objectstack-data), not an expression dialect. |:---|:---| | Current record field | `record.first_name` | | Previous record (update hooks, validation rules) | `previous.status` — §5 | +| Master-detail line item's header | `parent.status` — cell `readonlyWhen` / `requiredWhen` | +| Metadata-editing form row | `data` — the row under edit, repeater rows included | | Hook input payload | `input.amount` | | Identity context | `os.user.id`, `os.org.id`, `os.org.tier`, `os.env` | | Equality | `==` / `!=` | @@ -144,7 +146,17 @@ resolves at runtime, and this table documents them all. | `addMonths(d, n)` | timestamp | Shift **any** date by `n` months; clamps to month-end (`addMonths(date('2026-01-31'), 1)` → Feb 28) | | `date(s)` / `datetime(s)` | timestamp | Parse an ISO date / date-time string to a timestamp | -> **No date arithmetic.** Do NOT write `end - start`, `date + n`, or `today() + 30` — a date mixed with a number faults and the field silently nulls (the build rejects it); `end - start` does not fault, it yields a `duration` stored as `{}`. Use `daysBetween(start, end)` for a span in days, and `daysFromNow(n)` / `addDays(d, n)` / `addMonths(d, n)` to shift a date. Inclusive day span: `daysBetween(record.start_date, record.end_date) + 1`. Tenure in years: `daysBetween(record.hire_date, today()) / 365`. For a genuine sub-day offset use `now() + duration("3h")` — the calendar-day helpers always land on UTC midnight. +> **No date arithmetic.** A date mixed with a number faults and the build +> rejects it; `end - start` does not fault — it yields a `duration` stored as +> `{}`. + +| Want | Write — never `end - start`, `date + n`, `today() + 30` | +|:---|:---| +| Span in days | `daysBetween(start, end)` | +| Inclusive span | `daysBetween(record.start_date, record.end_date) + 1` | +| Shift a date | `daysFromNow(n)` / `addDays(d, n)` / `addMonths(d, n)` | +| Tenure in years | `daysBetween(record.hire_date, today()) / 365` | +| Sub-day offset | `now() + duration("3h")` — the calendar helpers land on UTC midnight | **Numbers** @@ -220,10 +232,13 @@ F`coalesce(record.cost, 0) > 0 ✅ + ```ts -P`record.status == 'qualified'` -P`record.amount > 10000 && record.region in ['us', 'eu']` -P`!isBlank(record.po_number)` +import { P } from '@objectstack/spec'; + +P`record.status == 'qualified'`; +P`record.amount > 10000 && record.region in ['us', 'eu']`; +P`!isBlank(record.po_number)`; ``` For field-level conditional rules, emit the canonical field properties: @@ -325,7 +340,7 @@ When migrating Salesforce-flavor metadata, apply these rules in order: | Legacy | CEL | |:---|:---| -| `bare_field` | `record.bare_field` | +| `bare_field` | `record.bare_field` — except in a flow condition, below | | `OLD.x` | `previous.x` | | `NEW.x` | `record.x` | | `=` (comparison) | `==` | @@ -348,6 +363,11 @@ When migrating Salesforce-flavor metadata, apply these rules in order: > does not quietly skip the hook, it **fails the write**. On `after*` events it > IS bound, per matched row, on bulk and single-record writes alike. +> ⚠️ **Flow conditions are the exception to row 1.** The automation engine +> spreads the record's variables to top level, so a bare `status` resolves in a +> flow start condition. `record.status` resolves there too and is the only +> spelling valid on every other surface — prefer it. + --- ## Surfaces that take an Expression @@ -371,6 +391,20 @@ to the envelope. | `Seed.records[*]` | any value | cel (via `cel\`\``) | | `audit` / `metrics` / `tracing` | `condition` / `successCriteria` | structured \| cel | +⚠️ **A `formula` field is virtual — no driver materialises a column for it**, so +`where`, `orderBy` and `searchableFields` naming one are refused +`400 INVALID_FIELD` at both doors. It still READS correctly, which is why the +refusal is needed: a `where` on one used to answer `200` with zero rows. +Denormalise onto a stored field and query that. `summary` and `autonumber` have +real columns and need no such care. + +⚠️ **A form-view `visibleWhen` is the one predicate here that faults OPEN.** It +is CLIENT-SIDE only, and an unbound root falls back to `true`, so the control +renders for everyone — never use one as access control; that is permission-set +field-level security. Everywhere else on this table the opposite holds: an +unevaluable `Hook` / `SharingRule` `condition` or validation predicate **aborts +the write** (§5). + View list filters are **not** a CEL surface — they are structured JSON filter rules (`ViewFilterRuleSchema`), so do not emit CEL there. For "last 30 days" style windows use the **date macro tokens** (`data/date-macros.zod.ts`) — @@ -410,5 +444,7 @@ project). To check a *single* expression before saving it, call the ## See also -- `node_modules/@objectstack/formula/` — engine + stdlib -- `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — `Expression`, `ExpressionInput`, `cel` / `F` / `P` +- [references/_index.md](./references/_index.md) — the Zod schemas behind every + surface above +- `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — `Expression`, + `ExpressionInput`, `cel` / `F` / `P` From b3331d2c39bfdcd7a2cf05cb5722e8e358c27395 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 08:45:09 +0000 Subject: [PATCH 5/7] skills(formula): say this is the companion package, and stop routing cron away (FRM-A-02 / A-01) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four host skills end their description with "load objectstack-formula alongside" (objectstack-data, -ui, -api, -automation); this one never said it was that companion, so an author reading it cold had no way to know why it had been loaded. FRM-A-01: the description also said "Do not use for ... cron schedules" while the body taught cron across four sections. The body now carries one schema-pointing cron row, so the exclusion goes and the Skill Boundaries table keeps the routing to objectstack-automation. `skills/README.md` and `content/docs/ai/skills-reference.mdx` are the generator output of `pnpm --filter @objectstack/spec gen:skill-docs` — both move, and nothing in them is hand-edited. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1 --- content/docs/ai/skills-reference.mdx | 6 +++--- skills/README.md | 2 +- skills/objectstack-formula/SKILL.md | 9 +++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/content/docs/ai/skills-reference.mdx b/content/docs/ai/skills-reference.mdx index 270b9894f5..690bdf4ba6 100644 --- a/content/docs/ai/skills-reference.mdx +++ b/content/docs/ai/skills-reference.mdx @@ -51,7 +51,7 @@ ObjectStack ships **11 skills** — one per authoring domain, plus process skill | 6 | [AI](#ai) | `ai` | `skills/objectstack-ai/` | Design ObjectStack AI skills, tools, knowledge sources, conversations, model registry entries, and MCP integrations. | | 7 | [API](#api) | `api` | `skills/objectstack-api/` | Design the server-side API surface that an ObjectStack runtime exposes — REST endpoints, auth providers, realtime channels, error envelopes, batch/versioning contracts. | | 8 | [i18n](#i18n) | `i18n` | `skills/objectstack-i18n/` | Author ObjectStack translation bundles — object/field labels, view text, app navigation strings, automation messages — and configure locale fallback, coverage reporting, and the per-locale source layout. | -| 9 | [Formula](#formula) | `expression` | `skills/objectstack-formula/` | Author CEL expressions used across ObjectStack — formula fields, field conditional rules (`visibleWhen`, `readonlyWhen`, `requiredWhen`), validation / sharing / visibility predicates, flow conditions, and dynamic seed values. | +| 9 | [Formula](#formula) | `expression` | `skills/objectstack-formula/` | Author CEL expressions used across ObjectStack — formula fields, field conditional rules (`visibleWhen`, `readonlyWhen`, `requiredWhen`), validation / sharing / visibility predicates, flow conditions, and dynamic seed values. This is the companion skill that objectstack-data, -ui, -api and -automation each tell you to load alongside them. | | 10 | [PM Dispatch](#pm-dispatch) | `process` | `skills/objectstack-pm-dispatch/` | Run a project-manager dispatch loop over a GitHub backlog: triage and queue ready issues, claim each one, dispatch it to a parallel developer agent that returns a structured JSON report, review the results against GitHub, and drive accepted pull requests to landing — escalating to the maintainer only what genuinely needs a human decision. Ships the developer-agent operating template the loop injects into every dispatch (no custom agent types required) and the upstream-reporting procedure for platform defects an app project finds. | | 11 | [Upgrade](#upgrade) | `process` | `skills/objectstack-upgrade/` | Upgrade an ObjectStack metadata project across a protocol major — run the deterministic conversion chain, then work the semantic residue the chain cannot express (intent choices, custom code on retired APIs, stale prose) to a decision with the project's owner, and finish with a green `validate` plus a human-readable upgrade report. | @@ -173,11 +173,11 @@ Do not use for general i18n library questions unrelated to ObjectStack bundles. **Domain** `expression` · **Path** `skills/objectstack-formula/` -Author CEL expressions used across ObjectStack — formula fields, field conditional rules (`visibleWhen`, `readonlyWhen`, `requiredWhen`), validation / sharing / visibility predicates, flow conditions, and dynamic seed values. +Author CEL expressions used across ObjectStack — formula fields, field conditional rules (`visibleWhen`, `readonlyWhen`, `requiredWhen`), validation / sharing / visibility predicates, flow conditions, and dynamic seed values. This is the companion skill that objectstack-data, -ui, -api and -automation each tell you to load alongside them. Use when the user is writing an `F`, `P`, or `cel` tagged-template literal, or asks "how do I express X as a formula / predicate". -Do not use for SQL fragments (driver-native), cron schedules (cron dialect), or L2 hook bodies (those belong in objectstack-data). +Do not use for SQL fragments (driver-native) or L2 hook bodies (those belong in objectstack-data). **Tags:** `cel`, `formula`, `predicate`, `condition`, `validation`, `visibility`, `seed-dynamic` diff --git a/skills/README.md b/skills/README.md index dd1f67036e..c22c4ec07e 100644 --- a/skills/README.md +++ b/skills/README.md @@ -37,7 +37,7 @@ apps too). | [AI](./objectstack-ai/SKILL.md) | `ai` | Design ObjectStack AI skills, tools, knowledge sources, conversations, model registry entries, and MCP integrations. | | [API](./objectstack-api/SKILL.md) | `api` | Design the server-side API surface that an ObjectStack runtime exposes — REST endpoints, auth providers, realtime channels, error envelopes, batch/versioning contracts. | | [i18n](./objectstack-i18n/SKILL.md) | `i18n` | Author ObjectStack translation bundles — object/field labels, view text, app navigation strings, automation messages — and configure locale fallback, coverage reporting, and the per-locale source layout. | -| [Formula](./objectstack-formula/SKILL.md) | `expression` | Author CEL expressions used across ObjectStack — formula fields, field conditional rules (`visibleWhen`, `readonlyWhen`, `requiredWhen`), validation / sharing / visibility predicates, flow conditions, and dynamic seed values. | +| [Formula](./objectstack-formula/SKILL.md) | `expression` | Author CEL expressions used across ObjectStack — formula fields, field conditional rules (`visibleWhen`, `readonlyWhen`, `requiredWhen`), validation / sharing / visibility predicates, flow conditions, and dynamic seed values. This is the companion skill that objectstack-data, -ui, -api and -automation each tell you to load alongside them. | | [PM Dispatch](./objectstack-pm-dispatch/SKILL.md) | `process` | Run a project-manager dispatch loop over a GitHub backlog: triage and queue ready issues, claim each one, dispatch it to a parallel developer agent that returns a structured JSON report, review the results against GitHub, and drive accepted pull requests to landing — escalating to the maintainer only what genuinely needs a human decision. Ships the developer-agent operating template the loop injects into every dispatch (no custom agent types required) and the upstream-reporting procedure for platform defects an app project finds. | | [Upgrade](./objectstack-upgrade/SKILL.md) | `process` | Upgrade an ObjectStack metadata project across a protocol major — run the deterministic conversion chain, then work the semantic residue the chain cannot express (intent choices, custom code on retired APIs, stale prose) to a decision with the project's owner, and finish with a green `validate` plus a human-readable upgrade report. | diff --git a/skills/objectstack-formula/SKILL.md b/skills/objectstack-formula/SKILL.md index 05d403647c..21ee6b501a 100644 --- a/skills/objectstack-formula/SKILL.md +++ b/skills/objectstack-formula/SKILL.md @@ -4,10 +4,11 @@ description: > Author CEL expressions used across ObjectStack — formula fields, field conditional rules (`visibleWhen`, `readonlyWhen`, `requiredWhen`), validation / sharing / visibility predicates, flow conditions, and dynamic - seed values. Use when the user is writing an `F`, `P`, or `cel` - tagged-template literal, or asks "how do I express X as a formula / - predicate". Do not use for SQL fragments (driver-native), cron schedules - (cron dialect), or L2 hook bodies (those belong in objectstack-data). + seed values. This is the companion skill that objectstack-data, -ui, -api + and -automation each tell you to load alongside them. Use when the user is + writing an `F`, `P`, or `cel` tagged-template literal, or asks "how do I + express X as a formula / predicate". Do not use for SQL fragments + (driver-native) or L2 hook bodies (those belong in objectstack-data). license: Apache-2.0 compatibility: Requires @objectstack/spec 17.x and @objectstack/formula 17.x (CEL) metadata: From d3a32664c96ca72e6371cb429153cf6ae5fed673 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 11:52:05 +0000 Subject: [PATCH 6/7] skills(formula): regenerate skill docs after merging main Generator output only (`pnpm --filter @objectstack/spec gen:skill-docs`), re-run after the merge so it derives from all 11 SKILL.md files at the merged tree rather than at the pre-merge one. The whole diff is the FRM-A-02 description edit; nothing here is hand-written. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1 --- content/docs/ai/skills-reference.mdx | 6 +++--- skills/README.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/content/docs/ai/skills-reference.mdx b/content/docs/ai/skills-reference.mdx index 84de10f301..02ab1e7820 100644 --- a/content/docs/ai/skills-reference.mdx +++ b/content/docs/ai/skills-reference.mdx @@ -51,7 +51,7 @@ ObjectStack ships **11 skills** — one per authoring domain, plus process skill | 6 | [AI](#ai) | `ai` | `skills/objectstack-ai/` | Design ObjectStack AI skills, tools, knowledge sources, conversations, model registry entries, and MCP integrations. | | 7 | [API](#api) | `api` | `skills/objectstack-api/` | Design the server-side API surface that an ObjectStack runtime exposes — REST endpoints, auth providers, realtime channels, error envelopes, batch contracts. | | 8 | [i18n](#i18n) | `i18n` | `skills/objectstack-i18n/` | Author ObjectStack translation bundles — object/field labels, view text, app navigation strings, automation messages — and configure locale fallback, coverage reporting, and the per-locale source layout. | -| 9 | [Formula](#formula) | `expression` | `skills/objectstack-formula/` | Author CEL expressions used across ObjectStack — formula fields, field conditional rules (`visibleWhen`, `readonlyWhen`, `requiredWhen`), validation / sharing / visibility predicates, flow conditions, and dynamic seed values. | +| 9 | [Formula](#formula) | `expression` | `skills/objectstack-formula/` | Author CEL expressions used across ObjectStack — formula fields, field conditional rules (`visibleWhen`, `readonlyWhen`, `requiredWhen`), validation / sharing / visibility predicates, flow conditions, and dynamic seed values. This is the companion skill that objectstack-data, -ui, -api and -automation each tell you to load alongside them. | | 10 | [PM Dispatch](#pm-dispatch) | `process` | `skills/objectstack-pm-dispatch/` | Run a project-manager dispatch loop over a GitHub backlog: triage and queue ready issues, claim each one, dispatch it to a parallel developer agent that returns a structured JSON report, review the results against GitHub, and drive accepted pull requests to landing — escalating to the maintainer only what genuinely needs a human decision. Ships the developer-agent operating template the loop injects into every dispatch (no custom agent types required). | | 11 | [Upgrade](#upgrade) | `process` | `skills/objectstack-upgrade/` | Upgrade an ObjectStack metadata project across a protocol major — run the deterministic conversion chain, then work the semantic residue the chain cannot express (intent choices, custom code on retired APIs, stale prose) to a decision with the project's owner, and finish with a green `validate` plus a human-readable upgrade report. | @@ -173,11 +173,11 @@ Do not use for general i18n library questions unrelated to ObjectStack bundles. **Domain** `expression` · **Path** `skills/objectstack-formula/` -Author CEL expressions used across ObjectStack — formula fields, field conditional rules (`visibleWhen`, `readonlyWhen`, `requiredWhen`), validation / sharing / visibility predicates, flow conditions, and dynamic seed values. +Author CEL expressions used across ObjectStack — formula fields, field conditional rules (`visibleWhen`, `readonlyWhen`, `requiredWhen`), validation / sharing / visibility predicates, flow conditions, and dynamic seed values. This is the companion skill that objectstack-data, -ui, -api and -automation each tell you to load alongside them. Use when the user is writing an `F`, `P`, or `cel` tagged-template literal, or asks "how do I express X as a formula / predicate". -Do not use for SQL fragments (driver-native), cron schedules (cron dialect), or L2 hook bodies (those belong in objectstack-data). +Do not use for SQL fragments (driver-native) or L2 hook bodies (those belong in objectstack-data). **Tags:** `cel`, `formula`, `predicate`, `condition`, `validation`, `visibility`, `seed-dynamic` diff --git a/skills/README.md b/skills/README.md index 0871f4e64e..0e687d2c0c 100644 --- a/skills/README.md +++ b/skills/README.md @@ -37,7 +37,7 @@ apps too). | [AI](./objectstack-ai/SKILL.md) | `ai` | Design ObjectStack AI skills, tools, knowledge sources, conversations, model registry entries, and MCP integrations. | | [API](./objectstack-api/SKILL.md) | `api` | Design the server-side API surface that an ObjectStack runtime exposes — REST endpoints, auth providers, realtime channels, error envelopes, batch contracts. | | [i18n](./objectstack-i18n/SKILL.md) | `i18n` | Author ObjectStack translation bundles — object/field labels, view text, app navigation strings, automation messages — and configure locale fallback, coverage reporting, and the per-locale source layout. | -| [Formula](./objectstack-formula/SKILL.md) | `expression` | Author CEL expressions used across ObjectStack — formula fields, field conditional rules (`visibleWhen`, `readonlyWhen`, `requiredWhen`), validation / sharing / visibility predicates, flow conditions, and dynamic seed values. | +| [Formula](./objectstack-formula/SKILL.md) | `expression` | Author CEL expressions used across ObjectStack — formula fields, field conditional rules (`visibleWhen`, `readonlyWhen`, `requiredWhen`), validation / sharing / visibility predicates, flow conditions, and dynamic seed values. This is the companion skill that objectstack-data, -ui, -api and -automation each tell you to load alongside them. | | [PM Dispatch](./objectstack-pm-dispatch/SKILL.md) | `process` | Run a project-manager dispatch loop over a GitHub backlog: triage and queue ready issues, claim each one, dispatch it to a parallel developer agent that returns a structured JSON report, review the results against GitHub, and drive accepted pull requests to landing — escalating to the maintainer only what genuinely needs a human decision. Ships the developer-agent operating template the loop injects into every dispatch (no custom agent types required). | | [Upgrade](./objectstack-upgrade/SKILL.md) | `process` | Upgrade an ObjectStack metadata project across a protocol major — run the deterministic conversion chain, then work the semantic residue the chain cannot express (intent choices, custom code on retired APIs, stale prose) to a decision with the project's owner, and finish with a green `validate` plus a human-readable upgrade report. | From eeaedfc9fe9a11fd6710694aa9c4f951c9bd784f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 12:19:40 +0000 Subject: [PATCH 7/7] skills(formula): correct the sort refusal code and the cron/template pointer (contract review round 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two spans failed in-seat contract review; both were verified at source before editing and both are paid in-file — the file lands back on 5,158 tokens exactly, no growth. FAIL 1 (FRM-F-01, the query-axis note). The three axes do not share one error code. `where` and `searchableFields` answer `400 INVALID_FIELD`, but `orderBy` answers `400 INVALID_SORT` — `assertOrderByIsMaterializable` sets `err.code = 'INVALID_SORT'` at the engine seam (`packages/objectql/src/engine.ts`), and `assertSortFieldsExist` does the same at the ingress. An agent branching on the code would have branched wrong on the sort refusal. Both codes are now named. FAIL 2 (the cron/template pointer). The paragraph sent readers to `shared/expression.zod.ts` for "the formatter whitelist and the cron alias set", and that file carries neither. Worse, the sets used to live in the quick references this flight deleted, so the pointer was the last mention of both — and this same file correctly says `@objectstack/formula` ships `dist` only, so "go read it" resolves to nothing. Now: the seven aliases are inlined in the cron row from `packages/formula/src/cron-engine.ts`, the template row names `TEMPLATE_FORMATTERS` (really exported from `@objectstack/formula`, derived from the formatter table in `packages/formula/src/template-engine.ts`) as the introspectable whitelist, and the `expression.zod.ts` pointer is narrowed to the carrier lists it does declare. Paid by cutting narration of the same class this flight exists to remove: the `asc`/`desc` anecdote from the new note, and the Skill Boundaries cron parenthetical now that the surfaces table names `Job.schedule.expression` and the dialect. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1 --- skills/objectstack-formula/SKILL.md | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/skills/objectstack-formula/SKILL.md b/skills/objectstack-formula/SKILL.md index 21ee6b501a..f5df2d3c43 100644 --- a/skills/objectstack-formula/SKILL.md +++ b/skills/objectstack-formula/SKILL.md @@ -41,7 +41,7 @@ formula / condition / predicate / dynamic-seed metadata. | Define seed records | objectstack-data (use `cel\`...\`` for dynamic dates) | | Author flow / automation step | objectstack-automation (use `P\`...\`` for `condition`) | | Author L2 hook body (TS code) | objectstack-data | -| Cron schedule | objectstack-automation (`schedule.expression` is `cron` dialect) | +| Cron schedule | objectstack-automation | | SQL fragment | driver-native; not unified into the expression registry | --- @@ -393,11 +393,11 @@ to the envelope. | `audit` / `metrics` / `tracing` | `condition` / `successCriteria` | structured \| cel | ⚠️ **A `formula` field is virtual — no driver materialises a column for it**, so -`where`, `orderBy` and `searchableFields` naming one are refused -`400 INVALID_FIELD` at both doors. It still READS correctly, which is why the -refusal is needed: a `where` on one used to answer `200` with zero rows. -Denormalise onto a stored field and query that. `summary` and `autonumber` have -real columns and need no such care. +all three query axes refuse one at both doors: `where` and `searchableFields` +answer `400 INVALID_FIELD`, `orderBy` answers `400 INVALID_SORT`. It still READS +correctly, which is why refusing matters: a `where` on one used to answer +`200` with zero rows. Denormalise onto a stored field and query that. `summary` +and `autonumber` have real columns and need no such care. ⚠️ **A form-view `visibleWhen` is the one predicate here that faults OPEN.** It is CLIENT-SIDE only, and an unbound root falls back to `true`, so the control @@ -413,19 +413,17 @@ objectstack-query `rules/filters.md` has the token list. ### Cron and template surfaces -Two more registered dialects ride the same envelope. Neither is CEL, both -accept a bare string (auto-wrapped at validate time) or their helper, and both -read the same variable scope. +Two more dialects ride the same envelope. Neither is CEL; both take +a bare string (auto-wrapped) or their helper, and read the same variable scope. | Dialect | Helper | Grammar | Carriers | |:---|:---|:---|:---| -| `cron` | `` cron`0 6 * * MON` `` | 5- or 6-field cron plus `@daily` / `@hourly` aliases | `Job.schedule.expression` (canonical), `connector.schedule`, `automation/execution.cronExpression`, `api/export.cronExpression` | -| `template` | `` tmpl`Hello {{ record.first_name }}` `` | `{{ path }}` or `{{ path \| formatter[:arg] }}` — double braces only, whitelisted formatters, no conditionals | `system/email-template` `subject` / `bodyHtml` / `bodyText`, `ai/model-registry` `promptTemplate.system` / `.user`, `Object.titleFormat` (deprecated → `nameField`, ADR-0079) | +| `cron` | `` cron`0 6 * * MON` `` | 5- or 6-field cron, or one of `@yearly` `@annually` `@monthly` `@weekly` `@daily` `@hourly` `@reboot` | `Job.schedule.expression` (canonical), `connector.schedule`, `automation/execution.cronExpression`, `api/export.cronExpression` | +| `template` | `` tmpl`Hello {{ record.first_name }}` `` | `{{ path }}` or `{{ path \| formatter[:arg] }}` — double braces only, no conditionals; the formatter whitelist is `TEMPLATE_FORMATTERS`, exported from `@objectstack/formula` | `system/email-template` `subject` / `bodyHtml` / `bodyText`, `ai/model-registry` `promptTemplate.system` / `.user`, `Object.titleFormat` (deprecated → `nameField`, ADR-0079) | -Both surfaces are declared in `shared/expression.zod.ts`; read it for the full -carrier list, the formatter whitelist and the cron alias set. Missing template -paths render as the empty string. Move logic into a CEL field — a template -holds a path and a formatter, nothing else. +`shared/expression.zod.ts` declares both surfaces and their carriers. +Missing template paths render as the empty string. Move logic into a CEL field: +a template holds a path and a formatter, nothing else. ---