Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/automation-skill-antipatterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
---

docs(skill): add "valid-but-silently-wrong" anti-patterns to the automation authoring skill

Documentation-only — no package change. Adds this season's flow authoring
anti-patterns (the ones that pass build but fail at runtime, and that the new
build lints now catch) to `skills/objectstack-automation` Common Pitfalls, so the
AI author writes them right at the source: single-brace value interpolation
(#1315), `create_record` outputVariable holds the record / `{var.id}` (#1873),
time-relative rules as schedule+range not record-change date-equality (#1874),
`script` nodes must name a callable (#1870), conditions are bare CEL / stdlib only
(#1877).
44 changes: 44 additions & 0 deletions skills/objectstack-automation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,50 @@ read at runtime, not Zod-validated):
5. **Scheduled flows without idempotency.** If the flow runs twice
accidentally, the result should be the same.

### Valid-but-silently-wrong (passes build, fails at runtime)

These are *legal* metadata that authors — AI especially — get wrong. Most are now
caught by `objectstack build` (a hard error, or an advisory warning), but write
them right the first time:

6. **Flow node VALUE interpolation uses SINGLE braces.** Value fields on a node's
`config` (`fields`, `inputs`, notify `message`/`title`, …) interpolate
`{token}`:
- `{var}` / `{record.title}` — variable / record field
- `{record.tags.0}` — **array index** (e.g. a `multiple: true` lookup, stored as an array)
- `{$User.Id}` / `{NOW()}` / `{TODAY() + 30}` — current user / date macros
- anything without `{…}` is a **literal**

❌ `body: '{{ai_reply}}'` — double-brace is the *formula / template-field* dialect, **not** flow values
❌ `ticket: '$source.id'` — a bare `$ref` is a literal string, not interpolated
✅ `body: '{ai_reply}'`, `ticket: '{source.id}'`

7. **`create_record`'s `outputVariable` holds the created RECORD, not its id.**
Reference a field explicitly.
❌ `update_record … fields: { ref: '{newRec}' }` → yields the whole record object
✅ `fields: { ref: '{newRec.id}' }`

8. **Time-relative rules ("alert N days before a date") are SCHEDULE flows, not
record-change date-equality.** `record.end_date == daysFromNow(60)` on a
`record-*` trigger only fires if the record happens to be written on that exact
day — unattended rules never run.
✅ A daily `schedule` flow whose `get_record` filters `end_date` BETWEEN
`{TODAY()}` and `{TODAY() + N}`, then loops over the results.

9. **`script` nodes must name a callable.** Set `config.actionType` to a built-in
side-effect (`email` / `slack`) **or** `config.function` to a function
registered via `defineStack({ functions: { my_fn: (ctx) => … } })`. An empty
`script` node — or one pointing at an unregistered function — fails loudly.
Inline `config.script` JS is **not executed** by the built-in runtime (no
server-side sandbox) — move logic into a registered `function`.

10. **Conditions are bare CEL — only the stdlib is callable.** `now()`,
`today()`, `daysFromNow(n)`, `daysAgo(n)`, `isBlank(v)`, `coalesce(a, b)`,
`trim(s)`, plus CEL built-ins (`has`, `size`, `contains`, `startsWith`, …).
An UNKNOWN function (`PRIOR()`, a typo'd name) **fails the build**. And never
wrap a field reference in `{…}` inside a condition — that's a template brace
and fails as CEL: write `record.x`, not `{record.x}`.

---

## CRM Automation Blueprint
Expand Down