Skip to content

Commit 501bfc6

Browse files
docs(skills): objectstack-data, -automation and -platform teach ownership queues and assignment rules (ADR-0142 phase 2) with one eval each (#27426)
Fixes #26475 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:三个对外发布的 skill 开始教「所有权队列 + 分派规则」(ADR-0142 第二阶段,PR #26337 落地的能力):`objectstack-data` 新增小节 *Ownership queues*(保留列 `queue`、可编译的 `defineQueue` 示例、claim / release / move 各一句)与 `rules/security.md` 的 `queue` 共享收件人和 `current_user.queues` 占位符;`objectstack-automation` 新增小节 *Assignment rules*(可编译的 `defineAssignmentRule` 示例:`criteria` / `assignTo: { type: 'queue' }` / `strategy` / `load`,外加 `queue` 审批人指针);`objectstack-platform` 给 `queues` / `assignmentRules` 两个键各一句并点明需 `sharing` 能力;三个 skill 各加一条 eval。 - **为什么改**:能力已进 spec 与文档,但 skills 不教,AI 从 skills 写 CRM 类应用时只能手搓——`assigned_to` 下拉、轮询计数字段、钩子挑 owner;平台已经付出的 ADR-0142 用不上。本 PR 是 PR #26337 欠下的 `Skills-docs` 后续。 - **风险与回滚**:只改 `skills/**` 与一条 `@objectstack/skills` patch changeset;不动 spec、运行时、`content/docs/**`、ratchet 脚本。每个文件 token 数不升(新文字靠折叠同 skill `rules/**` / `references/**` 已有的重复句付账,eval 靠精简既有示范答案的措辞)。风险在折叠处:若某句被我误判为重复,读者要多开一个 rules 文件。回滚 = revert 本 PR。 - **先看**(按风险):① `skills/objectstack-automation/SKILL.md` —— 新节 *Assignment rules* 与 Valid-but-silently-wrong 条目 3/4/5 的折叠;② `skills/objectstack-data/SKILL.md` —— 新节 *Ownership queues* 与 Common Patterns 三处折叠(Naming / Field Type Selection / 索引引言);③ 三个 `evals/*.json` —— 新 case 的断言,以及既有示范答案精简后是否失真。 - **两分钟验证**:`node scripts/check-skills-token-ratchet.mjs` —— 预期末行「84 authored bundle file(s) within their ceilings」,七个目标文件 headroom 0–17;`pnpm --filter @objectstack/spec check:skill-examples`(先 `pnpm --filter '@objectstack/client-react...' build`)—— 预期「274 prose examples type-check」,含两个新 `os:check` 块。 - **需要你做的**:Tier H(`skills/**`)—— 授权账号 APPROVE 一次,席位落地;无其它动作。 ## What changes Teach what PR #26337 (ADR-0142 phase 2) merged, quoting key names off the tree: `packages/spec/src/security/queue.zod.ts` (`QueueSchema`: `name`, `label`, `members[].type: 'position'`, `members[].value`), `packages/spec/src/automation/assignment-rule.zod.ts` (`AssignmentRuleSchema`: `name`, `object`, `entries[]` with `criteria`, `assignTo: { type: 'queue' | 'user', value }`, `strategy: 'round_robin' | 'least_loaded'`, `load`), the stack keys `queues` / `assignmentRules` (`stack.zod.ts` L839/L848), the injected `queue` column (`injected-system-columns.ts` `QUEUE_COLUMN`), the `queue` sharing recipient (`sharing.zod.ts` `ShareRecipientType`) and the `queue` approver type (`approval.zod.ts` `ApproverType`). `defineQueue` and `defineAssignmentRule` are subpath exports (`@objectstack/spec/security`, `@objectstack/spec/automation`), not root exports — the examples import them that way. | File | Added | Paid by (folded sentence → its home) | |:--|:--|:--| | `skills/objectstack-data/SKILL.md` | `### Ownership queues — park a record for a team to claim` under *Security & Access Control*: the injected `queue` column, a compiled `defineQueue` example, claim / release / move one sentence each, the hand-rolled alternatives refused | *Naming Rules Summary* table → `rules/naming.md` L9–12 (one line kept); *Field Type Selection* category list → `rules/field-types.md` (L9 count, L44–45 `password` / `secret`, L98 `user`, L116–117 embedded; the two credential traps and the `user` / `formula` notes kept inline); the two index blockquotes (retired `type` / `partial`; unique scope and bare `unique: true`) → `rules/indexing.md` L26–33, L46–75 (one blockquote kept) | | `skills/objectstack-data/rules/security.md` | the `queue` sharing recipient (`sharedWith: { type: 'queue', value }` shares the rule's rows, parks none) in the *Choose by what the rows are keyed on* bullet; the `current_user.queues` placeholder row (`rls.zod.ts` L57–58) | `permissionSets:` refusal bullet, the Layer-0 "both keys are load-bearing" blockquote, *Renaming or removing a shipped access item*, the `secret` paragraph (facts also in `rules/field-types.md` L45) and the `viewAllRecords` bullet — each tightened in place, every fact kept | | `skills/objectstack-automation/SKILL.md` | `## Assignment rules — route a new record to a queue or a person` before *Triggers*: when a rule runs, a compiled `defineAssignmentRule` example (parked entry; `least_loaded` + `load`; `round_robin` noted), `strategy` only on a queue target, the `queue` approver pointer to `references/state-machines-and-approvals.md` § Approver Types, the hand-rolled alternatives refused; *When to Use* names the family | Valid-but-silently-wrong item 5 (notify `skipped: true`) → the `requires` table's `messaging` row (same file); item 4 → the CEL note at the top and item 1's brace rule; item 3's two opening paragraphs → the `script` node-table row; *CRM Automation Blueprint* table → one paragraph; *When to Use* bullets → one paragraph (the frontmatter `description` holds them); the `readonly` / `runAs: 'system'` blockquote tightened (objectstack-data SKILL.md *Conditional Field Rules* states the same rule); the fault-edge blockquote, the filter-operand bullet, the Flow Variables example, *Elevate the write*, the system-sweep note, the Triggers intro and the integrations tail tightened in place | | `skills/objectstack-platform/SKILL.md` | one paragraph after the key enumeration: what `queues` and `assignmentRules` declare, their `define*` factories and subpaths, and that both need the ownership-queue service `@objectstack/plugin-sharing` registers (`sharing` in `requires`) or the boot is refused (`describeMissingOwnershipQueueService`) | the intro paragraph → the frontmatter `description`; *Port resolution* → `references/operations.md` § Ports & networking; the `DriverPlugin` fenced snippet → `references/bootstrap.md` § Programmatic Bootstrap (the `SqlDriver` line kept inline); the seed `mode` table → `objectstack-data/references/seeds.md` § Import modes; `manifest.engines.protocol` tightened | | `skills/objectstack-data/evals/hooks-security-seeds-datasources.json` | eval 8: ownerless web-form leads a team holds until a rep claims one; `must_contain` `defineQueue` / `type: 'position'` / `type: 'queue'` / `CONCURRENT_UPDATE`, `must_not_contain` `assigned_to` / `beforeInsert` | evals 1–7: model answers and three prompts tightened (words removed, every `must_contain` token and fact kept) | | `skills/objectstack-automation/evals/flows-triggers-approvals.json` | eval 12: round-robin routing into a queue plus a `queue` approver; `must_contain` `defineAssignmentRule` / `strategy: 'round_robin'` / `type: 'queue'` / `type: 'approval'`, `must_not_contain` `assigned_to` / `record-after-create` | evals 1–11: model answers and seven prompts tightened | | `skills/objectstack-platform/evals/config-plugins-ops.json` | eval 9: a stack declaring `queues:` and `assignmentRules:` with `'sharing'` in `requires`; `must_not_contain` `assigned_to` / `beforeInsert` | evals 1–8: model answers and five prompts tightened | | `.changeset/26475-skills-teach-queues-assignment-rules.md` | `'@objectstack/skills': patch`, both declaration lines | — | Token table (`ceil(utf8 bytes / 4)`, the ratchet's convention; stamp line excluded on SKILL.md; `main` = origin/main `45c9be5f`, `now` = `a0193e39`): | File | main | now | ceiling | net | |:--|--:|--:|--:|--:| | `skills/objectstack-data/SKILL.md` | 6120 | 6106 | 6123 | −14 | | `skills/objectstack-data/rules/security.md` | 3687 | 3681 | 3687 | −6 | | `skills/objectstack-automation/SKILL.md` | 6457 | 6449 | 6457 | −8 | | `skills/objectstack-platform/SKILL.md` | 6014 | 6006 | 6014 | −8 | | `skills/objectstack-data/evals/hooks-security-seeds-datasources.json` | 1573 | 1573 | 1573 | 0 | | `skills/objectstack-automation/evals/flows-triggers-approvals.json` | 2734 | 2732 | 2734 | −2 | | `skills/objectstack-platform/evals/config-plugins-ops.json` | 1690 | 1689 | 1690 | −1 | No ceiling moves. No re-wrap counted as payment: every freed token is a deleted word, a removed duplicate or a dropped fence. Frontmatter is untouched on all three skills (a `description` / `tags` edit regenerates `content/docs/ai/skills-reference.mdx`, another lane's file); `check:skill-docs` and `check:skill-refs` report in sync. ## Tests Final head `a0193e39` (origin/main `45c9be5f` merged in before the PR; it moved `packages/spec/src/ui/component.zod.ts`, so `@objectstack/spec` was rebuilt under the verify lock, VERDICT command-exit 0, before the readings below). <details><summary>`pnpm --filter @objectstack/spec check:skill-examples` — green, then the reverse leg (exit 0 / exit 1)</summary> Green on `a0193e39`: `✅ 274 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them` (240 skills+docs, 11 spec TSDoc, 23 client SDK). Prerequisite: `pnpm --filter '@objectstack/client-react...' build` under the lock (VERDICT command-exit 0, 6m42s) — the gate refuses with exit 3 while `packages/client-react/dist` is unbuilt. Reverse leg, both new blocks mutated through `scripts/ablation-replace.mjs` (anchor count 1 → 0, replacement 0 → 1, blob changed; `--hold` on the data file, wrap mode on the automation file), on-disk counts during the run `strategie-count=1 team-count=1`: ``` skills/objectstack-automation/SKILL.md:311:7 error TS2561: Object literal may only specify known properties, but 'strategie' does not exist in type '{ name: string; assignTo: { type: "user" | "queue"; value: string; }; criteria?: … strategy?: "round_robin" | … Did you mean to write 'strategy'? skills/objectstack-data/SKILL.md:421:15 error TS2322: Type '"team"' is not assignable to type '"position"'. GATE_EXIT=1 ``` Restore proven per path: `blob after restore == blob at HEAD` (data `3513dbc6…`, automation `b77e9446…`), `git diff HEAD` empty on both, tree clean. </details> <details><summary>`node scripts/check-skills-token-ratchet.mjs` — exit 0</summary> ``` ✓ skills/objectstack-automation/SKILL.md is 6449 tokens (ceiling 6457; headroom 8) ✓ skills/objectstack-automation/evals/flows-triggers-approvals.json is 2732 tokens (ceiling 2734; headroom 2) ✓ skills/objectstack-data/SKILL.md is 6106 tokens (ceiling 6123; headroom 17) ✓ skills/objectstack-data/evals/hooks-security-seeds-datasources.json is 1573 tokens (ceiling 1573; headroom 0) ✓ skills/objectstack-data/rules/security.md is 3681 tokens (ceiling 3687; headroom 6) ✓ skills/objectstack-platform/SKILL.md is 6006 tokens (ceiling 6014; headroom 8) ✓ skills/objectstack-platform/evals/config-plugins-ops.json is 1689 tokens (ceiling 1690; headroom 1) ✓ check-skills-token-ratchet: 84 authored bundle file(s) within their ceilings; 10 generator-owned file(s) measured, not ratcheted. ``` </details> <details><summary>Derived families — `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `a0193e39`, 46 commands, exit codes captured before any pipe</summary> 45 × exit 0: `check:skills-token-ratchet` (+ self-test), `check:skill-top-level-keys` (`enumerates all 52 authorable top-level keys`), `check:skill-identifier-liveness` (`Leg 1: 487 citation(s) over 70 published file(s)`), `check:skill-compatibility`, `check:skill-frame-sync`, `check:skill-docs`, `check:doc-authoring`, `check:corpus-claim-drift`, `check:closed-ceremony-text`, `check:role-word`, `check:nul-bytes`, `check:pm-skill-id-lint`, `check:pm-governed-merges`, `check:published-files`, `check:adr-0087-registration --base origin/main`, `check:changeset-no-major`, `check:empty-changeset`, `check:changeset-gate-self-tests`, `check:closing-keyword-parity`, `check:comment-mask-corpus`, `check:doc-route-spelling --advisory`, `check:cross-package-test-inputs`, `check:driver-memory-census`, `check:dts-closure`, `check:lean-entry-closure`, `check:sourcemap-no-sources-content`, `check:watch-hint-literal`, `check:gitlink-declared`, `check:merge-driver`, `check:refd-timer-probe`, `check:ci-filter-parity`, `check:agent-test-spelling`, `check:pm-changeset-deadline-census`, `@objectstack/lint check:doc-formula-expressions`, and the derived `--self-test`s. 1 × exit 3, NOT MEASURED locally: `pnpm check:dual-build-cjs-loads` — `PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/` (33 packages; wants a full `pnpm build`, a farm-wide run that belongs to CI; this diff touches no package source). Also run: `pnpm --filter @objectstack/skills build` — `✓ sync-catalog self-test: 9 cases pass` · `95 file(s) across 11 skill(s) copied from skills/** into dist/skills and read back byte-identical`; `check:skill-refs` — `9 generated files in sync`. </details> No package test suite is owed: the diff touches `skills/**` and `.changeset/` only (`@objectstack/skills` ships the catalog by `sync-catalog.mjs`, run above); clause ① is empty. `test:repo` and the full farm are CI's. ## Acceptance notes - **Conflict named.** `.claude/agents/os-dev.md` says `pr_create` carries `assignees: [LOGIN]`; the dispatch order says omit it under the maintainer's 2026-10-29 ruling on #27363 (a seat-driven draft is assigned to nobody until the seat's review). This PR follows the order and carries no assignee; the file still says otherwise. - **PM hypothesis 1 (fold homes) partly falsified.** `skills/objectstack-platform/references/*` hold the `DriverPlugin` construction, port resolution and the barrel pattern, but NOT the Hono two-package table, the "no `@objectstack/adapter-*`" line, the `basePath` caveat or the driver selection table — those stayed. `skills/objectstack-automation/references/*` hold none of the Valid-but-silently-wrong prose, so every automation fold is intra-file (duplicates of the node table, the CEL note and the `requires` table) or a tightening. The data folds are as hypothesised: `rules/naming.md`, `rules/field-types.md` (all 49 types present, checked by name) and `rules/indexing.md` hold the folded text. - **Hypothesis 2 confirmed** — see the reverse leg. **Hypothesis 3 confirmed**: no eval executor exists; each new case's `expected_output` was checked programmatically to contain every `must_contain` token and none of the `must_not_contain` ones. - **Out of scope, class a (reproducible), noted not filed — pre-existing eval self-contradictions in `skills/objectstack-platform/evals/config-plugins-ops.json` on origin/main**: the model answer of eval 1 contains `tableName`, eval 2 contains `data:beforeInsert` and `onInstall`, eval 4 contains `@objectstack/adapter-`, eval 7 contains `role:` and `admin_full_access` — each a string the same eval's `must_not_contain` forbids, so a verbatim ideal answer grades wrong (the defect class the landed changeset `26515-automation-eval-fixtures.md` fixed for the automation skill). Repro: parse the file and test each `must_not_contain` string against its own `expected_output`. reach: named producer — the shipped eval fixture is the producer, the skills catalog grader the consumer. Left as written here (my tightening of those answers kept the wording); carrier: the `domain:skills` seat. Dedupe words: platform eval must_not_contain self-contradiction tableName onInstall. - **Beyond the minimum**: `rules/security.md` also gains the `current_user.queues` placeholder row (the RLS placeholder table listed six reserved keys and the kernel-name list already named `queues`). Nothing from the minimum teaching was cut. - `skills/objectstack-data/evals/README.md` (ceiling 143, headroom 1) was not updated to mention queues: one word exceeds it, and the ruling allows no raise. - No `Skills-docs:` line: kind `none`, no `(narrowing)` arm. ## Verification ```bash node scripts/check-skills-token-ratchet.mjs # 84 files within ceilings; the seven target files as tabled above node scripts/check-skill-top-level-keys.mjs # 52 keys enumerated; the new paragraph sits outside the key paragraph node scripts/check-skill-identifier-liveness.mjs # Leg 1 487 citations OK (the new `current_user.queues` row included) pnpm --filter '@objectstack/client-react...' build && pnpm --filter @objectstack/spec check:skill-examples # 274 blocks, the two new os:check blocks included ``` Reading the teaching: `skills/objectstack-data/SKILL.md` § Ownership queues; `skills/objectstack-automation/SKILL.md` § Assignment rules; `skills/objectstack-platform/SKILL.md` the paragraph after the key enumeration; the three new eval cases (ids 8 / 12 / 9). --- _Generated by [Claude Code](https://claude.ai/code/session_01QSeKYmhU5KEj1nukYhp1Tg)_ --------- Co-authored-by: objectstack-fleet[bot] <objectstack-fleet[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent a33fce2 commit 501bfc6

8 files changed

Lines changed: 269 additions & 236 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): objectstack-data, -automation and -platform teach ownership queues and assignment rules (ADR-0142 phase 2), one eval each
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** The `queue` and `assignment_rule` metadata types, the `queues` / `assignmentRules` stack keys, the reserved `queue` column, the claim / release / move verbs, the `queue` sharing recipient and the `queue` approver type shipped in the spec and the docs, but no published skill taught them — `objectstack-platform` listed the two keys without a sentence, and no eval exercised any of it. An agent authoring an app from the skills could route an ownerless record only by hand-rolling it: a per-record picklist, a round-robin counter field, or a hook that picks an owner.
11+
- **Now.** `objectstack-data` gains *Ownership queues — park a record for a team to claim* (the injected `queue` column, a compiled `defineQueue` example, claim / release / move in one sentence each) and `rules/security.md` the `queue` sharing recipient and the `current_user.queues` placeholder; `objectstack-automation` gains *Assignment rules — route a new record to a queue or a person* (a compiled `defineAssignmentRule` example with `criteria`, `assignTo: { type: 'queue' }`, `strategy` and `load`, and the `queue` approver pointer); `objectstack-platform` says what `queues` and `assignmentRules` are and that both need `sharing` in `requires`. One eval per skill asserts the declared shape and refuses the hand-rolled alternatives.
12+
- Every file stays at or under its token ceiling: the new text is paid for by folding sentences the same skill's `rules/**` and `references/**` already hold, and by tightening existing eval model answers. No ceiling moves, no frontmatter changes, no schema, key or behaviour changes.

‎skills/objectstack-automation/SKILL.md‎

Lines changed: 84 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,9 @@ metadata:
2222

2323
## When to Use This Skill
2424

25-
- You are building a **visual flow** (auto-launched, screen, or scheduled).
26-
- You need a **state machine** or **approval process** for a business object.
27-
- You are setting up **event-driven triggers** (record create/update/delete).
28-
- You need **scheduled automation** (daily reports, data cleanup).
29-
- You are **integrating a vendor system** or receiving its callback.
25+
Flows (auto-launched, screen, scheduled), state machines and approvals,
26+
record-change triggers, scheduled automation, vendor integrations and their
27+
callbacks, and **assignment rules** that route a new record to a queue or a person.
3028

3129
> **Predicates and conditions are CEL** — every `condition` / `guard` /
3230
> `entryCondition` / filter `value` here is an **Expression** envelope evaluated
@@ -122,14 +120,8 @@ error. **RAW** keys are read as written: a
122120
### Flow Variables
123121

124122
`variables` is an **array** of `{ name, type, isInput, isOutput }` entries —
125-
not a name-keyed map; a variable has no `label`:
126-
127-
```typescript
128-
variables: [
129-
{ name: 'case_id', type: 'text', isInput: true }, // passed in when invoked
130-
{ name: 'approval_result', type: 'boolean', isOutput: true }, // returned on completion
131-
],
132-
```
123+
not a name-keyed map; a variable has no `label`: `variables: [{ name: 'case_id',
124+
type: 'text', isInput: true }, { name: 'approval_result', type: 'boolean', isOutput: true }]`.
133125

134126
→ [references/examples-flows.md](./references/examples-flows.md) § Flow Example — Auto-Escalate Overdue Cases.
135127

@@ -138,35 +130,29 @@ variables: [
138130
> **Handling a failed node: a `fault` edge.** `{ source, target, type: 'fault' }`
139131
> routes a failed node to a handler instead of ending the run. **`type: 'fault'`
140132
> is what routes — a `label: 'error'` alone does nothing:** an ordinary edge
141-
> traverses on SUCCESS, so the handler would run on success and never on
142-
> failure (`objectstack validate` reports `flow-error-label-not-fault`).
143-
> A handled failure does NOT consume a flow-level `errorHandling.retry`, which
144-
> replays the flow from the start — prefer a fault edge when the failure is
145-
> local. The handler reads `{{ <nodeId>.error }}` (or run-wide
146-
> `{{ $error.message }}`; in CEL, `vars["$error"]`). The run then reports
147-
> success, and the failed step stays in the trace.
133+
> traverses on SUCCESS (`objectstack validate` reports `flow-error-label-not-fault`).
134+
> A handled failure does not consume a flow-level `errorHandling.retry`, which
135+
> replays from the start — prefer a fault edge for a local failure. The handler
136+
> reads `{{ <nodeId>.error }}` (run-wide `{{ $error.message }}`; in CEL,
137+
> `vars["$error"]`); the run then reports success and the failed step stays in the trace.
148138
149139
| ROUTES (runtime failure) | Does NOT route (fatal either way) |
150140
|:--|:--|
151141
| 404, rate-limit, rejected write, failed subflow | missing required config key (`objectName`, `url`, `flowName`, `connectorId`/`actionId`); filter operand that evaluated to `null`; graph past the nesting ceiling; unscoped run |
152142

153-
> **Writing a `readonly` field? Set `runAs: 'system'`.** `readonly: true`
154-
> governs the end-user surface: under the default `runAs: 'user'`, the engine
155-
> **strips** a `readonly` field from any non-system write — `create_record` and
156-
> `update_record` alike — the step reports success but the value never lands,
157-
> and the drop is named in the step's warnings. A flow that
158-
> maintains a `readonly` field (approval stamps, conversion flags, SLA
159-
> markers, rollups) must run `runAs: 'system'`.
160-
> `os validate` / `os build` fail a `runAs:'user'` `update_record` that writes
161-
> a `readonly` field, so it surfaces at build time. (`readonlyWhen` fields are
162-
> the same story, per record state —
163-
> flagged as a warning.) Do **not** work around this by removing `readonly`;
164-
> that loses the field's edit protection.
165-
166-
> **Elevate the write, not the flow.** A `screen` flow stays `runAs: 'user'`.
167-
> When one step in it must write a `readonly` field, move that step into a
168-
> dedicated `runAs: 'system'` flow and call it from a `subflow` node — raising
169-
> the whole flow silently elevates every other write in it.
143+
> **Writing a `readonly` field? Set `runAs: 'system'`.** Under the default
144+
> `runAs: 'user'` the engine **strips** a `readonly` field from `create_record`
145+
> and `update_record` alike — the step reports success, the value never lands,
146+
> the drop is named in the step's warnings — so a flow maintaining one (approval
147+
> stamps, conversion flags, SLA markers, rollups) runs `runAs: 'system'`.
148+
> `os validate` / `os build` fail a `runAs:'user'` `update_record` writing one
149+
> (`readonlyWhen`: a warning, per record state). Do **not** remove `readonly`
150+
> instead; that loses the field's edit protection.
151+
152+
> **Elevate the write, not the flow.** A `screen` flow stays `runAs: 'user'`; a
153+
> step that must write a `readonly` field moves into a dedicated `runAs: 'system'`
154+
> flow called from a `subflow` node — raising the whole flow silently elevates
155+
> every other write in it.
170156
>
171157
> **Elevation stops at identity assignment.** A `runAs: 'system'` flow's
172158
> insert, update, delete, transfer, restore or purge of `sys_user_position`,
@@ -180,9 +166,9 @@ variables: [
180166
> https://objectstack.ai/docs/permissions/system-context#1-authorization-and-scoping).
181167
>
182168
> **A `runAs: 'system'` sweep must pin its organization.** System context has no
183-
> trigger user, so nothing narrows the query: a scan or rollup with no
184-
> organization predicate reads and writes across every tenant. The tenant column
185-
> is platform-injected — filter on it, never re-declare it per object.
169+
> trigger user, so a scan or rollup with no organization predicate reads and
170+
> writes across every tenant; filter on the platform-injected tenant column, never
171+
> re-declare it per object.
186172
187173
> **Who may START a flow: `requiredPermissions: ['…']` on the flow** (ADR-0066
188174
> D4) — every door that starts it by name (trigger route, action of type `flow`,
@@ -202,13 +188,11 @@ variables: [
202188
203189
### Filter operands (`config.filter`)
204190

205-
- **Each operand is a value slot** — a literal, or a CEL value envelope
206-
evaluated before the query (`filter: { id: { dialect: 'cel', source:
207-
'record.id' } }`). A whole-string **query placeholder** (`{current_user_id}`,
208-
`{today}`, `{current_year_start}`) passes through verbatim for the query
209-
engine to expand; a flow variable never shadows a placeholder. Only `filter`
210-
gets this hand-off — in a text slot `{{ current_year_start }}` is a nonsense
211-
reference.
191+
- **Each operand is a value slot** — a literal or a CEL envelope evaluated
192+
before the query (`filter: { id: { dialect: 'cel', source: 'record.id' } }`).
193+
A whole-string **query placeholder** (`{current_user_id}`, `{today}`,
194+
`{current_year_start}`) passes through verbatim for the query engine; a flow
195+
variable never shadows it, and only `filter` gets this hand-off.
212196
- **An operand that evaluates to `null` refuses the node** (not
213197
`fault`-routable): a dropped condition does not narrow a query, it widens
214198
it, so a routed `delete_record` would empty the object while the run reports
@@ -258,13 +242,10 @@ write them right the first time:
258242
❌ `fields: { ref: { dialect: 'cel', source: 'newRec' } }` → writes the whole record object
259243
✅ `source: 'newRec.id'`
260244

261-
3. **`script` nodes call a registered function — that is all they do.** Set
262-
`config.function` to a function registered via
263-
`defineStack({ functions: { my_fn: (ctx) => … } })`. It is **required**: an
264-
empty `script` node refuses at execute; an unregistered function fails loudly.
265-
266-
There is no other dispatch form: `notify` for delivery, `connector_action`
267-
for a vendor system, a function for logic.
245+
3. **`script` nodes call a registered function — nothing else.** `config.function`
246+
(**required**: an empty node refuses at execute, an unregistered name fails
247+
loudly) names one registered via `defineStack({ functions: { my_fn: (ctx) => … } })`;
248+
`notify` delivers, `connector_action` reaches a vendor, a function computes.
268249

269250
**A flow `function` is a PURE compute step — it does NOT read/write the
270251
database.** It receives `ctx.input` and **returns** a value; `config.outputVariable`
@@ -298,29 +279,53 @@ write them right the first time:
298279
} });
299280
```
300281

301-
4. **Conditions are bare CEL — the stdlib is what you may call bare.** The table
302-
is **objectstack-formula**'s `CEL_STDLIB_FUNCTIONS`, the bare-callable public
303-
subset; receiver methods (called on a value, never bare) are not in it.
304-
An UNKNOWN function (`PRIOR()`, a typo'd name) and a `{…}`-wrapped field ref
305-
both **fail the build**: a brace is a template, not CEL — write `record.x`,
306-
not `{record.x}`.
307-
308-
5. **`notify` reports SUCCESS when the `messaging` capability is absent.** The
309-
executor logs `no messaging service registered` and returns success with
310-
`output: { delivered: 0, failed: 0, skipped: true }` and `metrics.acted: 0` —
311-
a green run that delivered nothing. Declare `messaging` in `requires`.
282+
4. **Conditions are bare CEL; only the stdlib (`CEL_STDLIB_FUNCTIONS`,
283+
**objectstack-formula**) is callable bare.** An unknown function (`PRIOR()`, a
284+
typo) and a `{…}`-wrapped field ref both **fail the build** — write `record.x`.
312285

313286
---
314287

315288
→ State Machines & Approvals: [references/state-machines-and-approvals.md](./references/state-machines-and-approvals.md) — its state-introspection route is `GET /api/v1/meta/object/:name/state/:field?from=:state`.
316289

317290
---
318291

292+
## Assignment rules — route a new record to a queue or a person
293+
294+
The third automation family: `assignmentRules` on the stack routes each inserted
295+
row of an `ownership: 'user'` object whose `owner_id` and `queue` are both still
296+
empty — once, after every `beforeInsert` hook, never on update, never refusing the
297+
insert (ADR-0142 D5). Entries are ordered; the first whose `criteria` (CEL over
298+
`record.*`) match decides:
299+
300+
<!-- os:check -->
301+
```typescript
302+
import { defineAssignmentRule } from '@objectstack/spec/automation';
303+
304+
export const caseIntake = defineAssignmentRule({
305+
name: 'case_intake',
306+
object: 'crm_case',
307+
entries: [
308+
{ name: 'urgent', criteria: 'record.priority == "urgent"',
309+
assignTo: { type: 'queue', value: 'escalations' } }, // no strategy: parked for a claim
310+
{ name: 'rest', assignTo: { type: 'queue', value: 'support_intake' },
311+
strategy: 'least_loaded', load: 'record.status != "closed"' }, // or 'round_robin'; `load` only with least_loaded
312+
],
313+
});
314+
```
315+
316+
`strategy` is for a queue target only; `assignTo: { type: 'user', value: '<id>' }`
317+
names one owner. Queues are declared in `queues` (objectstack-data); an approval
318+
for the same people is `approvers: [{ type: 'queue', value: 'support_intake' }]`
319+
([Approver Types](./references/state-machines-and-approvals.md#approver-types)).
320+
⛔ Never a `beforeInsert` hook that picks an owner, nor a round-robin counter field.
321+
322+
---
323+
319324
## Triggers — Event-Driven Automation
320325

321-
A `record_change` flow fires automatically on a data event. There is **no
322-
standalone trigger object and no top-level `trigger` / `event` key** — the
323-
binding lives entirely in the flow's **`start` node `config`**.
326+
A `record_change` flow fires on a data event; there is **no standalone trigger
327+
object and no top-level `trigger` / `event` key** — the binding is the `start`
328+
node's `config`.
324329

325330
### Prerequisite — declare the capabilities your nodes need
326331

@@ -405,25 +410,22 @@ packaged spec, `auth.credentialRef` (the customer's administrator supplies the
405410
value) — dispatched by `connector_action`; an inbound vendor callback is an
406411
`api` flow. Never a plugin, never an `http` node holding a credential. Recipes,
407412
credential hand-off, loader limits: [references/integration-recipes.md](./references/integration-recipes.md).
408-
Platform code only when one holds — and then it is the platform's, filed as
409-
a card; the app still writes none: the protocol is not HTTP; the work writes
410-
identity objects (users, units, positions); it processes files or binaries
411-
locally; one fixed action contract must span several vendors; the callback's
412-
signature is outside the `api` door's HMAC. A document the loader cannot take
413-
yet (YAML, Swagger 2.0, `$ref`, OAuth, request signing) is a gap named against
414-
its platform card, never a reason to write code.
413+
Platform code only when one holds — and then it is the platform's, filed as a
414+
card, the app still writing none: the protocol is not HTTP; the work writes
415+
identity objects (users, units, positions); it processes files locally; one fixed
416+
action contract spans several vendors; the callback's signature is outside the
417+
`api` door's HMAC. A document the loader cannot take yet (YAML, Swagger 2.0,
418+
`$ref`, OAuth, request signing) is a gap named against its platform card, never a
419+
reason to write code.
415420

416421
---
417422

418423
## CRM Automation Blueprint
419424

420-
Default approach — model the business lifecycle in Flow/Approval metadata
421-
first (integrations: above) — in this CRM-style structure:
422-
423-
| Automation Type | Typical Location | Pattern |
424-
|:--|:--|:--|
425-
| Flow (screen / approval / scheduled) | `src/flows/*.flow.ts` | explicit `variables`, `nodes` + `edges`; approval config in the approvals reference |
426-
| Flow registry | `src/flows/index.ts` | Export `allFlows: Flow[]` and register centrally in `defineStack({ flows })` |
425+
Model the business lifecycle in Flow / Approval metadata first (integrations:
426+
above): flows in `src/flows/*.flow.ts` with explicit `variables`, `nodes` +
427+
`edges` (approval config: the approvals reference), exported as `allFlows: Flow[]`
428+
from `src/flows/index.ts` and registered centrally in `defineStack({ flows })`.
427429

428430
---
429431

0 commit comments

Comments
 (0)