Skip to content

Commit 5ac59ff

Browse files
docs(skills): objectstack-automation teaches the CEL value envelope in fields / assignment values and / 100.0 for money
A flow value slot (create_record / update_record fields.*, an assignment value) no longer reads the single-brace template dialect: a string is the literal text it spells and a {…} token is refused at objectstack validate, registerFlow and the executor, naming the CEL spelling. The skill's item 1, item 2 and the script → update_record example now teach the { dialect: 'cel', source: '…' } envelope, the has() guard for a key that may be absent, and / 100.0 for money (CEL divides two integers as integers), and keep the two spellings a value slot still accepts ({NOW()} / {TODAY() ± N}, {$User.PATH}) and the text slots and filter that keep the template. The eval that pinned {recalc.discount} in fields now expects the envelope. Token ratchet paid inside each file. Claude-Session: https://claude.ai/code/session_01CXydFDyiQwNbGFkmwrcRQq Co-authored-by: Claude <noreply@anthropic.com>
1 parent c8bb3c8 commit 5ac59ff

2 files changed

Lines changed: 33 additions & 32 deletions

File tree

‎skills/objectstack-automation/SKILL.md‎

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -204,14 +204,14 @@ a query instead of narrowing it.
204204
one it does **not** resolve that IS a recognised filter placeholder
205205
(`{current_user_id}`, `{current_year_start}`) passes through **verbatim** for
206206
the query engine to expand. So a flow variable named after a placeholder
207-
**shadows** it. Only `filter` gets this hand-off — in `title`, `message`,
208-
`fields` and `url` a bare `{current_year_start}` is a nonsense reference.
207+
**shadows** it. Only `filter` gets this hand-off — in `title`, `message`
208+
and `url` a bare `{current_year_start}` is a nonsense reference.
209209
- **Static checkability splits by position.** A `{record.…}` token **inside a
210210
filter** naming an unknown field, or hopping a relation the start node does not
211211
list in `config.expand`, is an **ERROR** at `objectstack validate`: it resolves
212212
to nothing, the condition is DROPPED, and the node refuses to execute. The
213-
*same* reference **outside** a filter (message body, `http` url, write payload)
214-
only renders an empty string — a **warning**. A `{var}` naming a flow variable
213+
*same* reference **outside** a filter (message body, `http` url) only
214+
renders an empty string — a **warning**. A `{var}` naming a flow variable
215215
or node output is **not statically checkable at all**.
216216

217217
---
@@ -221,29 +221,32 @@ a query instead of narrowing it.
221221
Legal metadata that authors — AI especially — get wrong; most are caught by
222222
`objectstack build`, but write them right the first time:
223223

224-
1. **Flow node VALUE interpolation uses SINGLE braces.** Value fields on a node's
225-
`config` (`fields`, `inputs`, notify `message`/`title`, …) interpolate
226-
`{token}`:
227-
- `{var}` / `{record.title}` — variable / record field
228-
- `{record.tags.0}` — **array index** (e.g. a `multiple: true` lookup, stored as an array)
229-
- `{$User.Id}` / `{NOW()}` / `{TODAY() + 30}` — current user / date macros
230-
- `{round(x)}` `{floor(x)}` `{ceil(x)}` `{abs(x)}` `{min(a,b)}` `{max(a,b)}` —
231-
mirror the CEL stdlib 1:1. `round` is **integer-only** (no `round(x, 2)`);
232-
for N decimals write `{round(x * 100) / 100}` (scale 2)
233-
- anything without `{…}` is a **literal**
234-
235-
❌ `body: '{{ai_reply}}'` — double-brace is the *formula / template-field* dialect, **not** flow values
236-
❌ `ticket: '$source.id'` — a bare `$ref` is a literal string, not interpolated
237-
✅ `body: '{ai_reply}'`, `ticket: '{source.id}'`
238-
❌ `'{ROUND(x, 2)}'` / `'{Math.round(x)}'` / `'{(x).toFixed(2)}'` — any other
239-
name in call position **fails the node** with a named error naming the
240-
supported set. The build does **not** catch these (conditions are checked,
241-
call-position names are not) and a `fault` edge cannot route it.
224+
1. **A `fields` / `assignment` VALUE is a literal or a CEL envelope.** In
225+
`create_record` / `update_record` `fields.*` and an `assignment` value (its
226+
two legacy shapes too) a string is the literal text it spells, and a `{…}`
227+
token is **refused** — at `objectstack validate` (`expression-invalid`),
228+
`registerFlow` and the executor — naming its CEL spelling. Compute with
229+
`{ dialect: 'cel', source: '…' }`:
230+
- `'{record.owner}'` → `source: 'has(record.owner) ? record.owner : null'` —
231+
CEL refuses an absent key where the template wrote nothing, so nothing /
232+
`null` / a default is now YOUR call; `'{record.tags.0}'` → `'record.tags[0]'`
233+
- money: `source: 'round(x * 100) / 100.0'` — CEL divides two integers as
234+
integers, so `/ 100` turns `123.46` into `123`
235+
- still accepted there until CEL can write them: `{NOW()}` / `{TODAY() ± N}`
236+
and `{$User.<path>}`
237+
238+
Text slots (notify `title` / `message`, `inputs`, `http` `url` / `body`, …)
239+
and `filter` keep the SINGLE-brace template — `{var}` / `{record.title}`,
240+
`{record.tags.0}` (array index), `{$User.Id}`, `{NOW()}`, `{TODAY() + 30}`,
241+
`{round(x)}`-style arithmetic; no `{…}` ⇒ literal; `{{x}}` is the
242+
template-field dialect. ❌ `'{ROUND(x, 2)}'` — an unknown name in call
243+
position **fails the node** at run time, unchecked at build, not
244+
`fault`-routable.
242245

243246
2. **`create_record`'s `outputVariable` holds the created RECORD, not its id.**
244247
Reference a field explicitly.
245-
❌ `update_record … fields: { ref: '{newRec}' }` → yields the whole record object
246-
✅ `fields: { ref: '{newRec.id}' }`
248+
❌ `fields: { ref: { dialect: 'cel', source: 'newRec' } }` → writes the whole record object
249+
✅ `source: 'newRec.id'`
247250

248251
3. **`script` nodes call a registered function — that is all they do.** Set
249252
`config.function` to a function registered via
@@ -260,8 +263,7 @@ Legal metadata that authors — AI especially — get wrong; most are caught by
260263
it. Keep data effects on the flow graph (visible, governed, build-checkable):
261264

262265
```ts
263-
// ❌ DON'T: expect the function to update the record itself (it has no data API)
264-
// ✅ DO: function returns values → outputVariable → update_record persists
266+
// ✅ function returns values → outputVariable → update_record persists
265267
{ id: 'ai', type: 'script', config: {
266268
function: 'helpdesk.aiTriageStub', // returns { ai_category, ai_sentiment, … }
267269
inputs: { ticketId: '{record.id}' }, // inputs are interpolated
@@ -270,11 +272,10 @@ Legal metadata that authors — AI especially — get wrong; most are caught by
270272
{ id: 'apply', type: 'update_record', config: {
271273
objectName: 'helpdesk_ticket',
272274
filter: { id: '{record.id}' },
273-
fields: { ai_category: '{ai.ai_category}', ai_sentiment: '{ai.ai_sentiment}' },
275+
fields: { ai_category: { dialect: 'cel', source: 'ai.ai_category' } },
274276
} },
275277
```
276278

277-
`defineStack({ functions: { 'helpdesk.aiTriageStub': (ctx) => ({ ai_category: 'other', … }) } })`.
278279
If you genuinely need data-lifecycle **side effects** (read/write other records),
279280
that's an L2 **hook** (objectstack-data) — hooks get `ctx.api`; flow functions don't.
280281

‎skills/objectstack-automation/evals/flows-triggers-approvals.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
{
55
"id": 1,
66
"prompt": "Every night at 02:00, mark `subscription` records whose `renewal_date` is in the past as `lapsed`. `status` is a readonly field that only automation may change.",
7-
"expected_output": "Emits a `defineFlow` with `type: 'schedule'`, the cadence on the START NODE's `config.schedule: { type: 'cron', expression: '0 2 * * *' }` (FlowSchema has no top-level `schedule`; no cron tagged template), `runAs: 'system'` because a scheduled run has no trigger user and a readonly field is stripped under `runAs: 'user'`. One `update_record` node with `filter` as an ObjectQL where-map (`renewal_date: { $lt: '{TODAY()}' }`) and `fields: { status: 'lapsed' }` (not `values`), `label` on every node, nodes wired with `edges` (never `next`), ending in an `end` node.",
7+
"expected_output": "Emits a `defineFlow` with `type: 'schedule'`, the cadence on the START NODE's `config.schedule: { type: 'cron', expression: '0 2 * * *' }` (no top-level `schedule`; no cron tag), `runAs: 'system'` because a scheduled run has no trigger user and a readonly field is stripped under `runAs: 'user'`. One `update_record` node with `filter` as an ObjectQL where-map (`renewal_date: { $lt: '{TODAY()}' }`) and `fields: { status: 'lapsed' }` (not `values`), `label` on every node, `edges` (never `next`), an `end` node.",
88
"files": [],
99
"assertions": {
1010
"must_contain": ["defineFlow", "type: 'schedule'", "runAs: 'system'", "update_record", "edges"],
@@ -44,11 +44,11 @@
4444
{
4545
"id": 5,
4646
"prompt": "Add a step that calls our `pricing.recalc` function to compute the discount and saves it on the quote, then posts the quote to our tax service over HTTP; if the tax call fails, route to a handler node instead of failing the run.",
47-
"expected_output": "Emits a `script` node with `config.function: 'pricing.recalc'` (registered via `defineStack({ functions })`), `inputs`, and `outputVariable: 'recalc'`; the function is a pure compute step, so a following `update_record` persists `{recalc.discount}` via `fields`. The `http` node sets `timeoutMs`. Failure routing is an edge `{ source, target, type: 'fault' }` — a `label: 'error'` alone routes nothing.",
47+
"expected_output": "Emits a `script` node with `config.function: 'pricing.recalc'` (registered via `defineStack({ functions })`), `inputs`, and `outputVariable: 'recalc'`; the function is a pure compute step, so a following `update_record` persists it with `fields: { discount: { dialect: 'cel', source: 'recalc.discount' } }`. The `http` node sets `timeoutMs`. Failure routing is a `type: 'fault'` edge — a `label: 'error'` alone routes nothing.",
4848
"files": [],
4949
"assertions": {
50-
"must_contain": ["function:", "outputVariable", "type: 'fault'", "timeoutMs", "update_record"],
51-
"must_not_contain": ["label: 'error'", "query_record"]
50+
"must_contain": ["function:", "outputVariable", "type: 'fault'", "timeoutMs", "update_record", "dialect: 'cel'"],
51+
"must_not_contain": ["label: 'error'", "query_record", "{recalc"]
5252
}
5353
}
5454
]

0 commit comments

Comments
 (0)