Skip to content

Commit 0e73de8

Browse files
docs(skills): teach one naming rule, every package-authored metadata name starts with its namespace (#30831)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DWyW8k3qL1ZSS5TK1cXcDAs)_ Fixes #30805 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:发布的 authoring skills 现在只教一条命名规则——包里写的每个元数据名都以包的 namespace 开头(`crm_send_email`、`crm_sales_rep`),例外只有字段、对象扩展、容器视图和翻译;全部示例和 evals 里的裸名已改成带前缀。 - 为什么改:落实 #30798 裁决 C 的第二阶段,让 AI 在第三阶段把裸名变成拒收之前就已经按一种写法写名字。 - 风险与回滚:只改 `skills/**` 文档和 evals,不动任何代码、schema 或行为;回滚就是还原这一个 PR。 - 先看:`skills/objectstack-platform/SKILL.md` 第 123 行新加的规则段,以及下方的改名表和保留名单。 - 两分钟验证:`node scripts/check-skills-token-ratchet.mjs` 全绿(每个文件都在原天花板内,靠同文件删减付费);`pnpm --dir ui run check:skill-eval-tokens` 通过。 - 需要你做的:按 Tier H 走受治理审批;下方 Open questions 里有三处需要席位或你拍板的保留项(裸对象名、连接器 id、能力名)。 ## What changes `objectstack-platform/SKILL.md` states the rule once, as a new paragraph right after the object rule: every metadata name a package authors starts with its namespace (flows, actions, hooks, apps, pages, dashboards, reports, datasets, queues, positions, permission sets, skills). The exceptions are fields, object extensions and container views (derived from a prefixed object), and translations (the namespace, an underscore, then the locale). The paragraph is taught as the authoring contract. It also says what origin/main enforces today, read off the code: - `defineStack` refuses a bare object name (`validateObjectNamespacePrefix`, `packages/spec/src/kernel/namespace-prefix.ts`). - `os build` refuses a bare doc name (`docs/namespace-prefix`, an error). - `os lint`'s `naming/namespace-prefix` warns only when one package declares an app, page, dashboard, flow, action, report or dataset name twice (`packages/cli/src/commands/lint.ts` around L280-330). The old sentence in that paragraph said `os lint` warns on every bare UI or automation name whenever a namespace is set. That is false on main, so it is removed. #30804 (open) widens that lint. If it lands first, this paragraph's last sentence must be re-matched to its type list before merge. If this PR lands first, #30804's PR owes that one-sentence update. Every example and eval in `skills/**` that showed or generated a bare non-object name now carries the prefix. Snippets with no manifest use `crm`. Snippets with their own package context keep it: `todo` for the i18n app-todo mirror, which already uses `todo_app`. Cross-references move together: action targets, `dashboardName` / `reportName`, approver and queue values, `permissionSets`, `skills`, `action_` tool names, translation keys and eval `must_contain` tokens. `skills/README.md` "Conventions enforced across skills" now teaches the same rule. It keeps the existing short-object-name convention for snippets, with no object-level `namespace` and no `tableName`. ### Rename table (file → old → new) | File | Old | New | |:--|:--|:--| | objectstack-platform/SKILL.md | position `legal_counsel`, permission set `contracts_editor` | `crm_legal_counsel`, `crm_contracts_editor` | | objectstack-platform/evals/config-plugins-ops.json | eval 1 expects "prefixed object names"; eval 9 queue `support_intake` (×3), position `service_agent` (×2) | "prefixed object and flow names (`crm_account`, `crm_convert_lead`)"; `crm_support_intake`, `crm_service_agent` | | objectstack-ui/rules/actions.md | actions `reassign_lead`, `convert_lead`, `create_campaign`, `print_a3`, `open_invoice_pdf` (and its handler target); flow target `lead_conversion`; app segment `/apps/crm/…` | `crm_reassign_lead`, `crm_convert_lead`, `crm_create_campaign`, `crm_print_a3`, `crm_open_invoice_pdf`; `crm_lead_conversion`; `/apps/crm_enterprise/…` (the app navigation.md declares) | | objectstack-ui/rules/navigation.md | `dashboardName: 'sales_dashboard'`, `reportName: 'opportunities_by_stage'`, page `task_workbench` | `crm_sales_dashboard`, `crm_opportunities_by_stage`, `crm_task_workbench` | | objectstack-ui/rules/pages.md | pages `invoice_print`, `release_notes` | `crm_invoice_print`, `crm_release_notes` | | objectstack-ui/rules/dashboards.md | dashboard `sales_dashboard`; datasets `sales`, `opportunity_metrics` (×3), `order_metrics` (×2), `project_metrics`, `task_metrics` (×2), `employee_metrics`, `contract_metrics` (×2); cube `opportunity`; actionUrl `create_opportunity`, `export_dashboard_pdf`; report `pipeline_coverage_by_quarter` | each with `crm_` (the cube's `sql: 'opportunity'` table stays) | | objectstack-ui/evals/views-apps-actions-pages.json | flow `lead_conversion` (evals 3, 8); key `_actions.log_call` (eval 11) | `crm_lead_conversion`; `_actions.crm_log_call` | | objectstack-ui/evals/report-period-export.json | dataset `opportunity_metrics` (×2) | `crm_opportunity_metrics` | | objectstack-automation/SKILL.md | assignment rule `case_intake`; queues `escalations`, `support_intake` (×2) | `crm_case_intake`; `crm_escalations`, `crm_support_intake` | | objectstack-automation/references/examples-flows.md | flows `escalate_overdue_cases`, `renewal_alert` | `crm_escalate_overdue_cases`, `crm_renewal_alert` | | objectstack-automation/references/integration-recipes.md | flow `stripe_balance_sweep` | `crm_stripe_balance_sweep` | | objectstack-automation/references/state-machines-and-approvals.md | flows `opportunity_discount_approval`, `dynamic_approval`; positions `sales_manager` (×2, one in the `org_membership_level` counter-example), `sales_director`, `manager`, `auditor` | each with `crm_` | | objectstack-automation/evals/flows-triggers-approvals.json | flows `budget_approval` (eval 3), `convert_lead` (eval 9); positions `manager`, `finance_manager` (×2); permission set `contractor_portal`; assignment rule `case_intake`; queues `support_team`, `support_leads` | each with `crm_` | | objectstack-automation/evals/approvals/test-revise-loop.md | flow `budget_approval` | `crm_budget_approval` | | objectstack-ai/SKILL.md | skill `case_management`; action `escalate_case`, tool `action_escalate_case` | `crm_case_management`; `crm_escalate_case`, `action_crm_escalate_case` | | objectstack-ai/evals/skills-tools-knowledge.json | `escalate_case` / `action_escalate_case` (eval 1, `must_contain` too); `close_case` / `action_close_case` (evals 4, 5); agent `support_bot` (eval 5); eval 1 expects "a snake_case `name`" | each with `crm_`; "a namespace-prefixed snake_case `name`" | | objectstack-upgrade/references/examples-upgrade.md | agent `support_bot`; `skills: ['case_management']` | `crm_support_bot`; `['crm_case_management']` | | objectstack-data/SKILL.md | queue `support_intake`; position `service_agent` | `crm_support_intake`; `crm_service_agent` | | objectstack-data/rules/security.md | permission set `sales_user` (×2); position `sales_rep` | `crm_sales_user`; `crm_sales_rep` | | objectstack-data/references/data-hooks.md | hooks `my_validation_hook`, `fill_position_on_hire`, `protect_strategic_accounts`, `audit_trail`, `cascade_account_updates`, `mask_pii`, `h` | `crm_validation_hook`, `crm_fill_position_on_hire`, `crm_protect_strategic_accounts`, `crm_audit_trail`, `crm_cascade_account_updates`, `crm_mask_pii`, `crm_h` | | objectstack-data/references/examples-objects.md | hook `account_defaults` | `crm_account_defaults` | | objectstack-data/rules/validation.md | hook `tax_id_external_check` | `crm_tax_id_external_check` | | objectstack-data/evals/hooks-security-seeds-datasources.json | queue `inside_sales` (×2), position `inside_sales_rep` (eval 8) | `crm_inside_sales`, `crm_inside_sales_rep` | | objectstack-i18n/SKILL.md | `_actions.complete` (the app-todo mirror, namespace `todo`) | `_actions.todo_complete` | | objectstack-i18n/evals/bundles-locales-coverage.json | action `close` (eval 1); report `pipeline_by_stage`, permission set `sales_manager`, position `regional_lead`, picklist `deal_stage` (eval 6); page `home`, flow `expense_claim` (eval 7) | each with `crm_` | | objectui/rules/protocol.md | action `"approve"` | `"crm_approve"` | | skills/README.md | "Short object names; no `namespace`, no `tableName`." | the one rule plus its exemptions, then "Short object names in snippets; no object-level `namespace`, no `tableName`." | Not renamed, because they are exempt as nested names: validation rule names, flow node ids, edge targets, widget ids, dataset dimensions and measures, assignment-rule entries, nav item ids, params, and container view keys. ### Held, not renamed (report per the dispatch's external-caller stop) - **Connector ids** `stripe`, `github_mcp`, `warehouse_api`, `legacy_erp` (integration-recipes.md), `erp_api` (datasources.md), `sap_erp` (upgrade eval 3) and the platform `slack` connector. The id is what credentials and inbound calls name. - **The api flow `carrier_callback`.** Its name is a segment of the inbound URL the carrier calls (`…/hooks/carrier_callback/carrier`). - **The datasource `warehouse`.** Its name keys the `OS_DATASOURCE_WAREHOUSE_PASSWORD` variable. - **Capability names** `view_pii` and `expense.approve_request`. Their grammar is dotted, and how a prefix applies to it is a phase-3 question. - **Function handler names** (`normalize`, `helpdesk.aiTriageStub`, `billing.sync`). They are not a metadata type. **Knowledge source id** `support_kb`. It is not stack metadata. - **Bare object names in snippets** (`account`, `lead`, `task`, …). These are outside this card, which covers non-object names. See Open questions. ### Token ratchet: paid by same-file deletion, no ceiling moved | File | Deleted | Reading (tokens / ceiling) | |:--|:--|:--| | objectstack-platform/SKILL.md | the false `os lint` sentence; two TOC parentheticals that restate the H2 headings of `plugin-development.md` and `operations.md` | 5990 / 6011 | | objectstack-ui/rules/actions.md | "— an action can live in multiple surfaces" (restates "is an array") | 2259 / 2259 | | objectstack-ui/rules/dashboards.md | "the escape hatch escalates when the user wants the full surface" (restates the bullet's lead) | 6252 / 6262 | | objectstack-ui/rules/navigation.md | "Canonical rules:" (the heading already cites objectui ADR-0055) | 2195 / 2196 | | objectstack-ui/rules/pages.md | "hygiene, not uniqueness: two packages coexist on one bare name (ADR-0048 §3.4)". It contradicts the rule this PR teaches, and #30798's audit measured doc reads as first-match without a package. | 5648 / 5669 | | objectstack-automation/SKILL.md | "The third automation family:" (the heading names it) | 6443 / 6449 | | objectstack-automation/references/state-machines-and-approvals.md | the `maxRevisions` row's pointer, which the bullet list below repeats | 5721 / 5730 | | objectstack-data/SKILL.md | the seeds pointer's TOC parenthetical (it restates `seeds.md`'s headings and was already missing one) | 6087 / 6123 | | objectstack-data/references/data-hooks.md | the `afterFind` code comment that repeats the sentence above it | 10056 / 10066 | | objectstack-data/references/examples-objects.md | the intro line that restates its "Lifecycle Hooks" heading | 1741 / 1756 | | objectstack-data/rules/security.md | the comment that repeats the `defineStack({ permissions })` line under it | 3688 / 3698 | | objectstack-data/rules/validation.md | "— the typed, supported extension point" | 2746 / 2756 | Package-wide (all authored and eval files under `skills/**` this PR touches, ceil of bytes over 4): 7633 → 7635 lines, 97336 → 97353 tokens (+17). The +17 is the eval fixtures, which are measured but not ratcheted, plus `skills/README.md`, which is outside the ratchet. Every ratcheted file is at or under its ceiling. ## Why Decision #30798 ruled C (maintainer, verbatim: 「同意 c」). Every package-authored metadata name carries its namespace, in three phases. This is phase 2, the AI-authoring half: the published skills teach the one rule before phase 3 makes a bare name a refusal. ## Tests No code changes, so there are no new unit tests. The gates that read `skills/**` are what judge this diff. Families derived by `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (48), run locally at head `7fd40067`, each exit code recorded as `COMMAND :: exit CODE`: - 47 of 48 exit 0. They include `node scripts/check-skills-token-ratchet.mjs` (and `--self-test`), `pnpm --filter @objectstack/spec run check:skill-examples` (after the `@objectstack/client-react...` closure build), `check:skill-docs`, `check:skill-refs`, `pnpm --filter @objectstack/lint run check:doc-formula-expressions` (after the `@objectstack/lint...` closure build), `pnpm check:skill-top-level-keys`, `check:skill-identifier-liveness`, `check:skill-frame-sync`, `check:skill-compatibility`, `check:nul-bytes`, `check:doc-authoring`, `check:pm-governed-merges` and `pnpm --dir ui exec vitest run scripts/`. - `pnpm check:dual-build-cjs-loads` exited 3 with "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/". Nothing was measured. The gate reads `packages/**/dist`, which this diff does not touch. The PR's CI check-run is the reading for it. - `--ran` reconciliation: "48 derived famil(ies) accounted for — 47 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)", with 0 unrun. - Run outside the derived list: `pnpm --dir ui run check:skill-eval-tokens --self-test` and `pnpm --dir ui run check:skill-eval-tokens` exited 0 ("Every must_contain token is taught by its own skill bundle"). `pnpm lint` exited 0. ## Acceptance notes - **Out of scope, reported to the seat and not fixed here:** `skills/objectui/guides/project-setup.md` L199-221 imports `defineConfig` from `@objectstack/runtime`, which exports no `defineConfig` on main. The snippet also nests `objects` under `apps[]` with the name `my-app`. It carries no `os:check` marker, so no gate compiles it. - Under the rule, an app named exactly `crm` is bare, because `validateObjectNamespacePrefix` checks for the namespace followed by an underscore. That is why the action URL now points at `crm_enterprise`. ## Open questions 1. **Bare object names in skill snippets.** Most snippets use short object names (`account`, `lead`) by the README convention, while ADR-0129 requires the prefix once a namespace is set. Recommendation: a separate card after phase 3, so this PR stays at the card's non-object scope. 2. **Connector, inbound api flow and datasource names** (held above). Recommendation: keep them, and have phase 3 exempt names an external caller or a variable spells, or rename them with a migration note. 3. **Dotted capability names.** Phase 3 should state how the prefix applies (`crm.view_pii` or `crm_view_pii`). ## Verification 1. Check out the branch and run `node scripts/check-skills-token-ratchet.mjs`. Every ratcheted file reads at or under its ceiling, and no ceiling moved. 2. Run `pnpm --dir ui run check:skill-eval-tokens`. It passes, including the renamed `action_crm_escalate_case` token. 3. Read `skills/objectstack-platform/SKILL.md` L123 against `packages/spec/src/kernel/namespace-prefix.ts` and `packages/cli/src/commands/lint.ts`. The enforcement sentence matches main. 4. Run `git grep -nE "name: '(lead_conversion|support_intake|sales_rep|escalate_case|case_management)'" -- skills`. It returns nothing. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (`packages/spec` untouched.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (Not touched.) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English, apart from the fleet's required `## 维护者速读` summary. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016WV4cw4B24PbS5MZbX1r5j --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 18870c9 commit 0e73de8

28 files changed

Lines changed: 129 additions & 115 deletions
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): the authoring skills teach one naming rule, every metadata name a package authors starts with its namespace
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** `objectstack-platform` stated the prefix rule for objects only, and claimed `os lint` warns on bare app, page, dashboard, flow, action, report and dataset names whenever a namespace is set. The examples across the skills named flows, actions, hooks, pages, dashboards, reports, datasets, queues, positions, permission sets, assignment rules, agents and skills bare (`lead_conversion`, `sales_rep`, `support_intake`).
11+
- **Now.** `objectstack-platform` states the rule once, beside the object rule: every metadata name a package authors starts with its namespace (`crm_send_email`, `crm_sales_rep`). The exceptions are fields, object extensions, container views and translations (`<ns>_<locale>`). It also says what is enforced today: `defineStack` refuses a bare object name, `os build` refuses a bare doc name, and `os lint`'s `naming/namespace-prefix` warns only on a name one package declares twice. Every example and eval that showed or generated a bare non-object name now carries `crm_` (or its package's namespace where the snippet has one), cross-references included.
12+
- Connector ids, the inbound api flow whose name is part of its webhook URL, and the datasource whose name keys its password variable keep their names. No schema, key or behaviour changes.

‎skills/README.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,12 @@ carries `SKILL.md` alone — `gen:skill-refs` only visits skills listed in its
9595
## Conventions enforced across skills
9696

9797
- **Zod first.** Never invent types — read `node_modules/@objectstack/spec/src/**/*.zod.ts`.
98-
- **Short object names** (`account`, `task`); no `namespace`, no `tableName`.
98+
- **Every non-object name carries the namespace** (`crm_send_email`, `crm_sales_rep`);
99+
a snippet with no manifest uses `crm`. Exempt: fields, object extensions,
100+
container views, translations (`<ns>_<locale>`). The one rule lives in
101+
`objectstack-platform/SKILL.md`.
102+
- **Short object names in snippets** (`account`, `task`); no object-level
103+
`namespace`, no `tableName`.
99104
- **CEL for all expressions** — predicates, conditions, schedules. Use the
100105
`F\`\``, `P\`\``, `cel\`\``, `cron\`\``, `tmpl\`\`` tagged templates from
101106
`@objectstack/spec`. Legacy `OLD` / `NEW` are not CEL — use

‎skills/objectstack-ai/SKILL.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ copilot claiming abilities it did not have.
9494
import { defineSkill } from '@objectstack/spec';
9595

9696
export default defineSkill({
97-
name: 'case_management',
97+
name: 'crm_case_management',
9898
label: 'Case Management',
9999
description: 'Triage, query, and escalate support cases.',
100100
instructions: `
@@ -104,8 +104,8 @@ export default defineSkill({
104104
- Escalated cases must summarise the actions already taken.
105105
`,
106106
// Every name resolves: two platform tools (rung 2) plus one action tool
107-
// materialised from this stack's own `escalate_case` Action (rung 3).
108-
tools: ['query_records', 'get_record', 'action_escalate_case'],
107+
// materialised from this stack's own `crm_escalate_case` Action (rung 3).
108+
tools: ['query_records', 'get_record', 'action_crm_escalate_case'],
109109
triggerConditions: [
110110
{ field: 'objectName', operator: 'eq', value: 'support_case' },
111111
],

‎skills/objectstack-ai/evals/skills-tools-knowledge.json‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
"evals": [
44
{
55
"id": 1,
6-
"prompt": "Add `src/skills/case-triage.skill.ts` for our support app: the assistant should be able to query and read support cases and run our existing `escalate_case` Action on a case. Only activate it when the user is working on the `support_case` object.",
7-
"expected_output": "Emits a `defineSkill` with a snake_case `name`, `label`, `instructions`, and a `tools` list where every name resolves: platform tools `query_records` / `get_record` plus the action tool `action_escalate_case` materialised from the stack's own Action. Routing is a `triggerConditions` entry `{ field: 'objectName', operator: 'eq', value: 'support_case' }` (a string value for `eq`). No `permissions` key on the skill (removed in 17) and no `*.agent.ts` file.",
6+
"prompt": "Add `src/skills/case-triage.skill.ts` for our support app: the assistant should be able to query and read support cases and run our existing `crm_escalate_case` Action on a case. Only activate it when the user is working on the `support_case` object.",
7+
"expected_output": "Emits a `defineSkill` with a namespace-prefixed snake_case `name`, `label`, `instructions`, and a `tools` list where every name resolves: platform tools `query_records` / `get_record` plus the action tool `action_crm_escalate_case` materialised from the stack's own Action. Routing is a `triggerConditions` entry `{ field: 'objectName', operator: 'eq', value: 'support_case' }` (a string value for `eq`). No `permissions` key on the skill (removed in 17) and no `*.agent.ts` file.",
88
"files": [],
99
"assertions": {
10-
"must_contain": ["defineSkill", "tools", "action_escalate_case", "triggerConditions"],
10+
"must_contain": ["defineSkill", "tools", "action_crm_escalate_case", "triggerConditions"],
1111
"must_not_contain": ["permissions:", "defineAgent("]
1212
}
1313
},
@@ -33,8 +33,8 @@
3333
},
3434
{
3535
"id": 4,
36-
"prompt": "Write a `*.tool.ts` so the assistant can call our `close_case` action on a support case.",
37-
"expected_output": "Does not author a tool record: the default path is to opt the Action in with `ai: { exposed: true, description: '<40+ chars>' }` on the object's action, which materialises `action_close_case`, and to name that tool in a skill's `tools`. `defineTool` is only for presentation refinement and cannot make anything executable (no `handler` / `implementation`; `ToolSchema` is strict).",
36+
"prompt": "Write a `*.tool.ts` so the assistant can call our `crm_close_case` action on a support case.",
37+
"expected_output": "Does not author a tool record: the default path is to opt the Action in with `ai: { exposed: true, description: '<40+ chars>' }` on the object's action, which materialises `action_crm_close_case`, and to name that tool in a skill's `tools`. `defineTool` is only for presentation refinement and cannot make anything executable (no `handler` / `implementation`; `ToolSchema` is strict).",
3838
"files": [],
3939
"assertions": {
4040
"must_contain": ["exposed", "description", "action_", "tools"],
@@ -43,7 +43,7 @@
4343
},
4444
{
4545
"id": 5,
46-
"prompt": "Create `src/ai/support-bot.agent.ts` with defineAgent({ name: 'support_bot', tools: ['query_records', 'action_close_case'], knowledge: ['support_kb'] }) and register it under agents.",
46+
"prompt": "Create `src/ai/support-bot.agent.ts` with defineAgent({ name: 'crm_support_bot', tools: ['query_records', 'action_crm_close_case'], knowledge: ['support_kb'] }) and register it under agents.",
4747
"expected_output": "Refuses the agent: the `agent` type is closed to third parties (a stack-authored agent is filtered out, refused by `loadAgent()` and 404s on chat), and `agent.tools` / `agent.knowledge` are protocol-17 tombstones. Re-declares the two tools in a `defineSkill` (registered under `skills`) with a `surface`, and restricts retrieval at the knowledge-source level (`aiExposed`, `where`) rather than on an agent. The runtime's `ask` / `build` agents pick the skill up by surface.",
4848
"files": [],
4949
"assertions": {

‎skills/objectstack-automation/SKILL.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ write them right the first time:
292292

293293
## Assignment rules — route a new record to a queue or a person
294294

295-
The third automation family: `assignmentRules` on the stack routes each inserted
295+
`assignmentRules` on the stack routes each inserted
296296
row of an `ownership: 'user'` object whose `owner_id` and `queue` are both still
297297
empty — once, after every `beforeInsert` hook, never on update, never refusing the
298298
insert (ADR-0142 D5). Entries are ordered; the first whose `criteria` (CEL over
@@ -303,20 +303,20 @@ insert (ADR-0142 D5). Entries are ordered; the first whose `criteria` (CEL over
303303
import { defineAssignmentRule } from '@objectstack/spec/automation';
304304

305305
export const caseIntake = defineAssignmentRule({
306-
name: 'case_intake',
306+
name: 'crm_case_intake',
307307
object: 'crm_case',
308308
entries: [
309309
{ name: 'urgent', criteria: 'record.priority == "urgent"',
310-
assignTo: { type: 'queue', value: 'escalations' } }, // no strategy: parked for a claim
311-
{ name: 'rest', assignTo: { type: 'queue', value: 'support_intake' },
310+
assignTo: { type: 'queue', value: 'crm_escalations' } }, // no strategy: parked for a claim
311+
{ name: 'rest', assignTo: { type: 'queue', value: 'crm_support_intake' },
312312
strategy: 'least_loaded', load: 'record.status != "closed"' }, // or 'round_robin'; `load` only with least_loaded
313313
],
314314
});
315315
```
316316

317317
`strategy` is for a queue target only; `assignTo: { type: 'user', value: '<id>' }`
318318
names one owner. Queues are declared in `queues` (objectstack-data); an approval
319-
for the same people is `approvers: [{ type: 'queue', value: 'support_intake' }]`
319+
for the same people is `approvers: [{ type: 'queue', value: 'crm_support_intake' }]`
320320
([Approver Types](./references/state-machines-and-approvals.md#approver-types)).
321321
⛔ Never a `beforeInsert` hook that picks an owner, nor a round-robin counter field.
322322

‎skills/objectstack-automation/evals/approvals/test-revise-loop.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ This file states only what a grader scores, so the rule has one home.
1111

1212
## Scenario
1313

14-
> Build an autolaunched flow `budget_approval` on the `project` object: when
14+
> Build an autolaunched flow `crm_budget_approval` on the `project` object: when
1515
> `budget` increases past 100000, route to a manager for approval. The manager
1616
> can **approve**, **reject**, or **send the record back to the submitter for
1717
> revision**; after the submitter reworks and resubmits, it returns to the

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@
4343
},
4444
{
4545
"id": 3,
46-
"prompt": "Build an autolaunched flow `budget_approval` on `project`: when `budget` increases past 100000, route to a manager who can approve, reject, or send it back to the submitter for revision; a resubmission returns to the manager. Cap it at two send-backs.",
47-
"expected_output": "The ADR-0044 shape: an `approval` node (`type: 'approval'`, `approvers: [{ type: 'position', value: 'manager' }]`, `lockRecord: true`, `maxRevisions: 2`) with `approve` / `reject` out-edges plus a `revise` out-edge to an `approval_revise` node, and a resubmit edge back into the approval node typed `type: 'back'` — what legalises the cycle for `registerFlow`. The flow lives in `flows` (no `approvals` collection).",
46+
"prompt": "Build an autolaunched flow `crm_budget_approval` on `project`: when `budget` increases past 100000, route to a manager who can approve, reject, or send it back to the submitter for revision; a resubmission returns to the manager. Cap it at two send-backs.",
47+
"expected_output": "The ADR-0044 shape: an `approval` node (`type: 'approval'`, `approvers: [{ type: 'position', value: 'crm_manager' }]`, `lockRecord: true`, `maxRevisions: 2`) with `approve` / `reject` out-edges plus a `revise` out-edge to an `approval_revise` node, and a resubmit edge back into the approval node typed `type: 'back'` — what legalises the cycle for `registerFlow`. The flow lives in `flows` (no `approvals` collection).",
4848
"files": [],
4949
"assertions": {
5050
"must_contain": [
@@ -120,8 +120,8 @@
120120
},
121121
{
122122
"id": 7,
123-
"prompt": "An expense approval only holders of `expense.approve_request` may start; route to the `finance_manager` position; the submitter must never approve their own expense.",
124-
"expected_output": "Flow-level `requiredPermissions: ['expense.approve_request']`, checked at every door that starts the flow (never a run-user positions check inside it); the `approval` node names `approvers: [{ type: 'position', value: 'finance_manager' }]` with `excludeSubmitter: true`, and `onEmptyApprovers` is anything but `auto_approve`, refused beside it.",
123+
"prompt": "An expense approval only holders of `expense.approve_request` may start; route to the `crm_finance_manager` position; the submitter must never approve their own expense.",
124+
"expected_output": "Flow-level `requiredPermissions: ['expense.approve_request']`, checked at every door that starts the flow (never a run-user positions check inside it); the `approval` node names `approvers: [{ type: 'position', value: 'crm_finance_manager' }]` with `excludeSubmitter: true`, and `onEmptyApprovers` is anything but `auto_approve`, refused beside it.",
125125
"files": [],
126126
"assertions": {
127127
"must_contain": [
@@ -155,7 +155,7 @@
155155
},
156156
{
157157
"id": 9,
158-
"prompt": "Build a screen flow `convert_lead` that asks for a company name and a contact name, creates a `crm_account` and a linked `crm_contact`, and ends on a screen linking to both new records.",
158+
"prompt": "Build a screen flow `crm_convert_lead` that asks for a company name and a contact name, creates a `crm_account` and a linked `crm_contact`, and ends on a screen linking to both new records.",
159159
"expected_output": "A `type: 'screen'` flow: a `screen` collecting `company_name` and `contact_name`; two `create_record` nodes with `outputVariable` (`account`, `contact`), the contact linked by `account: { dialect: 'cel', source: 'account.id' }`; a last `screen` of two `type: 'record_link'` fields with `reference` (`crm_account`, `crm_contact`) and `defaultValue: { dialect: 'cel', source: 'account.id' }` / `'contact.id'`, no `required`, `placeholder` or `options`. The `end` carries no `message` (refused when completed), the `description` no markdown link — the links are the `record_link` fields, never the object field type.",
160160
"files": [],
161161
"assertions": {
@@ -190,7 +190,7 @@
190190
},
191191
{
192192
"id": 11,
193-
"prompt": "When a `contractor` is marked `onboarded`, grant its linked user (`contractor.user_id`) the `contractor_portal` permission set; run the flow as system so the grant always lands.",
193+
"prompt": "When a `contractor` is marked `onboarded`, grant its linked user (`contractor.user_id`) the `crm_contractor_portal` permission set; run the flow as system so the grant always lands.",
194194
"expected_output": "Not as system: a system-context insert into `sys_user_permission_set` (as into `sys_user_position`, `sys_invitation` or `sys_member`) is refused with `PERMISSION_DENIED` — elevation does not lift a governed identity write, a system subflow included. The flow runs `runAs: 'user'`: its `create_record` on `sys_user_permission_set` is judged by the delegated-administration gate as the user whose write fired it, recorded as `granted_by`; a user the gate would not let grant the set is refused — the rule, not a defect.",
195195
"files": [],
196196
"assertions": {
@@ -210,7 +210,7 @@
210210
{
211211
"id": 12,
212212
"prompt": "Spread new `crm_case` records evenly across the support team; a `refund_requested` case needs sign-off from the support-leads queue.",
213-
"expected_output": "An assignment rule, not a flow or hook: `defineAssignmentRule({ name: 'case_intake', object: 'crm_case', entries: [{ name: 'all', assignTo: { type: 'queue', value: 'support_team' }, strategy: 'round_robin' }] })` in `assignmentRules` — evaluated once per insert on a still-ownerless row, never on update; no counter field. The sign-off is an `approval` node (`type: 'approval'`) with `approvers: [{ type: 'queue', value: 'support_leads' }]`: the queue's members join the slate at node entry; an empty queue adds nobody.",
213+
"expected_output": "An assignment rule, not a flow or hook: `defineAssignmentRule({ name: 'crm_case_intake', object: 'crm_case', entries: [{ name: 'all', assignTo: { type: 'queue', value: 'crm_support_team' }, strategy: 'round_robin' }] })` in `assignmentRules` — evaluated once per insert on a still-ownerless row, never on update; no counter field. The sign-off is an `approval` node (`type: 'approval'`) with `approvers: [{ type: 'queue', value: 'crm_support_leads' }]`: the queue's members join the slate at node entry; an empty queue adds nobody.",
214214
"files": [],
215215
"assertions": {
216216
"must_contain": [

‎skills/objectstack-automation/references/examples-flows.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import { defineFlow } from '@objectstack/spec';
1717

1818
export const EscalateOverdueCasesFlow = defineFlow({
19-
name: 'escalate_overdue_cases',
19+
name: 'crm_escalate_overdue_cases',
2020
label: 'Escalate Overdue Cases',
2121
type: 'schedule',
2222
status: 'active',
@@ -82,7 +82,7 @@ day, a threshold is never missed.
8282

8383
```typescript
8484
{
85-
name: 'renewal_alert',
85+
name: 'crm_renewal_alert',
8686
label: 'Renewal Alert',
8787
type: 'schedule',
8888
runAs: 'system', // a sweep has no trigger user — elevate explicitly

‎skills/objectstack-automation/references/integration-recipes.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const StripeConnector = defineConnector({
4141
});
4242
// crm_account gains `stripe_available` (currency) and `stripe_synced_at` (datetime).
4343
export const StripeBalanceSweep = defineFlow({
44-
name: 'stripe_balance_sweep', label: 'Stripe balance → house account',
44+
name: 'crm_stripe_balance_sweep', label: 'Stripe balance → house account',
4545
type: 'schedule', runAs: 'system',
4646
nodes: [
4747
{ id: 'start', type: 'start', label: 'Nightly 03:00',

0 commit comments

Comments
 (0)