diff --git a/content/docs/guides/formula.mdx b/content/docs/guides/formula.mdx index 695b5e9f7..9ec531d83 100644 --- a/content/docs/guides/formula.mdx +++ b/content/docs/guides/formula.mdx @@ -54,6 +54,27 @@ All dialects share the same variable scope (`record`, `previous`, `os.user`, `os.org`, `os.env`, `vars` for flow steps), so you author once and never have to relearn the syntax across surfaces. + +**Predicates are bare CEL — never wrap field references in `{…}` braces.** The +most common mistake (and the root cause of issue #1491) 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 belong only in `{{ … }}` +text templates. As of 7.6 a malformed expression no longer silently does +nothing — it fails `objectstack build` and throws at runtime. + + +**Template formatting (7.6).** A `template` hole is a field path with an optional +whitelisted formatter — `{{ path | formatter[:arg] }}` — with defined value→string +semantics (no arbitrary logic; put logic in a CEL field): + +```ts +tmpl`Deal {{record.name}} — {{record.amount | currency}} closes {{record.close_date | date:long}}` +``` + +Formatters: `currency[:CODE]`, `number[:decimals]`, `percent[:decimals]`, +`date[:short|long|iso]`, `datetime[:…]`, `upper`, `lower`, `trim`, `truncate:N`, +`default:'…'`, `json`. Single-brace `{x}` is **not** a valid template hole. + At **input** time you may write a bare string for shorthand — the spec transforms it into the right envelope based on the field type. The compiled artifact always contains the full envelope (and, after M9.2, the AST). @@ -288,8 +309,12 @@ const result = ExpressionEngine.evaluate( // => { ok: true, value: 42 } ``` -Evaluation never throws — failures return `{ ok: false, error }`. Callers -choose whether to surface, log, or silently fall back. +The low-level engine never throws — `evaluate()` returns `{ ok: false, error }`. +But **call sites must not silently swallow that** (ADR-0032): `objectstack build` +**fails** on an invalid expression (with a located, schema-aware message), and at +runtime the flow/rule engines **throw** a loud, attributed error instead of +treating a bad expression as `false`/`null`. The `validate_expression` agent tool +runs this same validator so you can check an expression before saving. --- diff --git a/skills/objectstack-formula/SKILL.md b/skills/objectstack-formula/SKILL.md index bc4fc8948..700ce8c46 100644 --- a/skills/objectstack-formula/SKILL.md +++ b/skills/objectstack-formula/SKILL.md @@ -27,8 +27,19 @@ formula / condition / predicate / dynamic-seed metadata. > 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. -> **Do not emit Salesforce-flavor syntax** — it will silently evaluate to -> `null`. +> +> **Predicates / formulas are bare CEL — never wrap field references in `{…}` +> braces.** The #1 authoring mistake (root cause of #1491) 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. --- @@ -272,10 +283,29 @@ All accept bare strings (auto-wrapped to `{dialect:'cron', source}`) or the | `ai/orchestration.cron` | recurring runs | | `ai/devops-agent.iterationFrequency` | iteration cadence | -### Template surfaces (`{{path}}` interpolation) +### Template surfaces (`{{ path }}` interpolation) -Strict Mustache subset — only `{{record.x}}`, `{{os.user.id}}`, etc. No -conditionals, no helpers. Same scope as CEL. +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 | |:---|:---|