From 057ce4365b6bec13a932962c05c744dbe04ac6eb Mon Sep 17 00:00:00 2001 From: "objectstack-fleet[bot]" <332303061+objectstack-fleet[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:39:25 +0000 Subject: [PATCH] docs(skills): objectstack-automation teaches the {{ }} delimiter in flow text slots The notify example, the dialect paragraph and eval id 2 restate the text slots (notify title / message, screen title / description, end message) in the {{ }} delimiter the build and save doors read, and name which slots keep the single-brace template. Claude-Session: https://claude.ai/code/session_01JmWtcHfGbC4ncw4GFKWuRA Co-authored-by: Claude --- skills/objectstack-automation/SKILL.md | 29 +++++++++---------- .../evals/flows-triggers-approvals.json | 6 ++-- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/skills/objectstack-automation/SKILL.md b/skills/objectstack-automation/SKILL.md index 263401c481f..4c2e40373ce 100644 --- a/skills/objectstack-automation/SKILL.md +++ b/skills/objectstack-automation/SKILL.md @@ -105,8 +105,8 @@ undeclared key is a named parse error. **RAW** keys never interpolate: a ```ts { id: 'tell_owner', type: 'notify', label: 'Notify Owner', config: { recipients: '{record.assignee}', // REQUIRED — id, CSV, or string[] - title: 'Done: {record.title}', // inline path; XOR `template` (RAW, localizable) - message: 'Closed by {$User.Id}', // body; only with inline `title` + title: 'Done: {{ record.title }}', // inline; XOR `template` (RAW, localizable) + message: 'Closed {{ record.closed_at | date:long }}', // body; only with inline `title` topic: 'task', // RAW; default 'notify' severity: 'warning', // RAW; CLOSED enum info|warning|critical channels: ['inbox'], // RAW; default inbox @@ -196,21 +196,18 @@ run reports success. `objectstack validate` names the offending template. ### Filter tokens (`config.filter`) -The one slot where two `{…}` dialects meet, and the one whose failure **widens** -a query instead of narrowing it. - - **Precedence — flow variables win, placeholders pass through.** The flow template engine runs first. A whole-string token it resolves is a flow value; one it does **not** resolve that IS a recognised filter placeholder (`{current_user_id}`, `{current_year_start}`) passes through **verbatim** for the query engine to expand. So a flow variable named after a placeholder - **shadows** it. Only `filter` gets this hand-off — in `title`, `message` - and `url` a bare `{current_year_start}` is a nonsense reference. + **shadows** it. Only `filter` gets this hand-off — in `url` a bare + `{current_year_start}` is a nonsense reference. - **Static checkability splits by position.** A `{record.…}` token **inside a filter** naming an unknown field, or hopping a relation the start node does not list in `config.expand`, is an **ERROR** at `objectstack validate`: it resolves to nothing, the condition is DROPPED, and the node refuses to execute. The - *same* reference **outside** a filter (message body, `http` url) only + *same* reference **outside** a filter (a `{{ }}` hole, `http` url) only renders an empty string — a **warning**. A `{var}` naming a flow variable or node output is **not statically checkable at all**. @@ -235,13 +232,15 @@ Legal metadata that authors — AI especially — get wrong; most are caught by - still accepted there until CEL can write them: `{NOW()}` / `{TODAY() ± N}` and `{$User.}` - Text slots (notify `title` / `message`, `inputs`, `http` `url` / `body`, …) - and `filter` keep the SINGLE-brace template — `{var}` / `{record.title}`, - `{record.tags.0}` (array index), `{$User.Id}`, `{NOW()}`, `{TODAY() + 30}`, - `{round(x)}`-style arithmetic; no `{…}` ⇒ literal; `{{x}}` is the - template-field dialect. ❌ `'{ROUND(x, 2)}'` — an unknown name in call - position **fails the node** at run time, unchecked at build, not - `fault`-routable. + Text slots (notify `title` / `message`, screen `title` / `description`, end + `message`) render `{{ }}` holes — a variable path, optional formatter, no + logic; `os validate` / `registerFlow` refuse a `{…}` token there with its + `{{ }}` spelling. `{$User.Id}` / `{NOW()}` have no hole (`{{ $User.Id }}` + renders blank): assign them to a variable first. Every other slot + (`recipients`, `inputs`, `http`, `filter`) keeps the SINGLE-brace template: + `{record.tags.0}`, `{TODAY() + 30}`, `{round(x)}`; no `{…}` ⇒ literal. + ❌ `'{ROUND(x, 2)}'` — an unknown name in call position **fails the node** + at run time, unchecked at build, not `fault`-routable. 2. **`create_record`'s `outputVariable` holds the created RECORD, not its id.** Reference a field explicitly. diff --git a/skills/objectstack-automation/evals/flows-triggers-approvals.json b/skills/objectstack-automation/evals/flows-triggers-approvals.json index 20784404736..a07063509d8 100644 --- a/skills/objectstack-automation/evals/flows-triggers-approvals.json +++ b/skills/objectstack-automation/evals/flows-triggers-approvals.json @@ -14,11 +14,11 @@ { "id": 2, "prompt": "When an opportunity moves into stage `closed_won`, send the account owner an inbox notification with the deal name.", - "expected_output": "Emits a `record_change` flow whose binding lives on the `start` node `config`: `objectName: 'opportunity'`, `triggerType: 'record-after-update'`, and a bare-CEL `condition` `previous.stage != 'closed_won' && record.stage == 'closed_won'` so it fires only on the transition. A `notify` node with `recipients: '{record.owner_id}'`, `title` with single-brace interpolation, `channels: ['inbox']`. Reminds that `requires` must list `automation`, `triggers` and `messaging` (otherwise notify reports success with `skipped: true`).", + "expected_output": "Emits a `record_change` flow whose binding lives on the `start` node `config`: `objectName: 'opportunity'`, `triggerType: 'record-after-update'`, and a bare-CEL `condition` `previous.stage != 'closed_won' && record.stage == 'closed_won'` so it fires only on the transition. A `notify` node with `recipients: '{record.owner_id}'`, `title: 'Won: {{ record.name }}'`, `channels: ['inbox']`. Reminds that `requires` must list `automation`, `triggers` and `messaging` (otherwise notify reports success with `skipped: true`).", "files": [], "assertions": { - "must_contain": ["triggerType", "record-after-update", "previous.", "notify", "recipients", "messaging"], - "must_not_contain": ["trigger:", "{{"] + "must_contain": ["triggerType", "record-after-update", "previous.", "notify", "recipients", "messaging", "{{"], + "must_not_contain": ["trigger:"] } }, {