From 4dd793d31d116a23e6015fc06935d7c96815f1b6 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Fri, 31 Jul 2026 10:37:59 +0800 Subject: [PATCH 1/2] docs(skills): hook examples author with defineHook(), not bare `: Hook` literals (#4274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #4273 shipped defineHook() and pointed docs + error prescriptions at it, but skills/objectstack-data — the first thing an AI author reads — still taught the bare-literal form in 18 places across SKILL.md, rules/hooks.md, rules/validation.md, and references/data-hooks.md. Mechanical rewrite: `const x: Hook = {…}` → `const x = defineHook({…})` (imports switched to the value import), plus the factory-over-bare-literal note in rules/hooks.md and references/data-hooks.md, phrased identically to the schema.mdx note from #4273. Every rewritten block was scanned for unknown top-level keys — HookSchema is strict, so a stray key inside a defineHook() example would teach a crash — none found; the os:check gate covers the tagged blocks (198 examples green). Closes #4274 Co-Authored-By: Claude Fable 5 --- skills/objectstack-data/SKILL.md | 10 +-- .../objectstack-data/references/data-hooks.md | 73 ++++++++++--------- skills/objectstack-data/rules/hooks.md | 13 +++- skills/objectstack-data/rules/validation.md | 6 +- 4 files changed, 56 insertions(+), 46 deletions(-) diff --git a/skills/objectstack-data/SKILL.md b/skills/objectstack-data/SKILL.md index 04660ccaca..c077881bff 100644 --- a/skills/objectstack-data/SKILL.md +++ b/skills/objectstack-data/SKILL.md @@ -396,9 +396,9 @@ Implement business logic at data operation lifecycle points: ```typescript -import { Hook, HookContext } from '@objectstack/spec/data'; +import { defineHook, HookContext } from '@objectstack/spec/data'; -const accountHook: Hook = { +export default defineHook({ name: 'account_defaults', object: 'account', events: ['beforeInsert'], @@ -408,9 +408,7 @@ const accountHook: Hook = { } ctx.input.created_at = new Date().toISOString(); }, -}; - -export default accountHook; +}); ``` The `handler` above is the inline (in-process) form. The **preferred**, @@ -433,7 +431,7 @@ Mirror these CRM-style patterns when designing enterprise metadata objects: | Capability gating | `src/objects/*.object.ts` | Use `enable` flags (`trackHistory`, `apiMethods`, `files`, `feeds`, `activities`) per object | | Index + validation pairing | `src/objects/*.object.ts` | Keep `indexes[]` aligned to common filters and enforce invariants with `validations[]` | | Relationship constraints | `src/objects/*.object.ts` | Use `lookup` + `lookupFilters` (`[{ field, operator, value }]`) for constrained child selection | -| Lifecycle automation | `src/objects/*.hook.ts` | Use a lifecycle **hook** (a `Hook` object registered via `defineStack({ hooks })`) or a top-level `record_change` flow for field updates triggered by record changes. There is **no** object-level `workflows[]` field — authoring one is a build error (#1535). | +| Lifecycle automation | `src/objects/*.hook.ts` | Use a lifecycle **hook** (authored with `defineHook()`, registered via `defineStack({ hooks })` or the `*.hook.ts` convention scan) or a top-level `record_change` flow for field updates triggered by record changes. There is **no** object-level `workflows[]` field — authoring one is a build error (#1535). | | State transitions | `src/objects/*.object.ts` | Prefer explicit `state_machine` validation rules (one per state field) — there is **no** separate `stateMachines` map | For metadata authoring, keep expressions in CEL (`P\`...\``, `F\`...\``, diff --git a/skills/objectstack-data/references/data-hooks.md b/skills/objectstack-data/references/data-hooks.md index 866b473cb2..229cbb731c 100644 --- a/skills/objectstack-data/references/data-hooks.md +++ b/skills/objectstack-data/references/data-hooks.md @@ -78,13 +78,18 @@ ObjectStack provides **8 lifecycle events** organized by operation type: ## Hook Definition Schema -Every hook must conform to the `HookSchema`: +Every hook must conform to the `HookSchema`. Author it with `defineHook()` — +preferred over a bare `: Hook` literal (the same rule as `defineDatasource`): +it validates when the module is imported, so constraint-level mistakes a bare +annotation can't catch — a non-`snake_case` `name`, a misspelled key routed +through a spread — fail while you author instead of at deploy, and the export +carries defaults already materialized (#4269). ```typescript import { P } from '@objectstack/spec'; -import { Hook, HookContext } from '@objectstack/spec/data'; +import { defineHook, HookContext } from '@objectstack/spec/data'; -const myHook: Hook = { +const myHook = defineHook({ // Required: Unique identifier (snake_case) name: 'my_validation_hook', @@ -122,7 +127,7 @@ const myHook: Hook = { maxRetries: 3, backoffMs: 1000, }, -}; +}); ``` ### Key Properties Explained @@ -420,9 +425,9 @@ is **not** in the partial patch) and flip that position to `filled`: ```typescript -import type { Hook } from '@objectstack/spec/data'; +import { defineHook } from '@objectstack/spec/data'; -const fillPositionOnHire: Hook = { +const fillPositionOnHire = defineHook({ name: 'fill_position_on_hire', object: 'candidate', events: ['afterUpdate'], @@ -441,7 +446,7 @@ const fillPositionOnHire: Hook = { capabilities: ['api.read', 'api.write', 'log'], }, onError: 'log', -}; +}); export default fillPositionOnHire; ``` @@ -675,7 +680,7 @@ handler: async (ctx: HookContext) => { ### 1. Setting Default Values ```typescript -const setAccountDefaults: Hook = { +const setAccountDefaults = defineHook({ name: 'account_defaults', object: 'account', events: ['beforeInsert'], @@ -693,13 +698,13 @@ const setAccountDefaults: Hook = { ctx.input.owner_id = ctx.session.userId; } }, -}; +}); ``` ### 2. Data Validation ```typescript -const validateAccount: Hook = { +const validateAccount = defineHook({ name: 'account_validation', object: 'account', events: ['beforeInsert', 'beforeUpdate'], @@ -719,13 +724,13 @@ const validateAccount: Hook = { throw new Error('Annual revenue cannot be negative'); } }, -}; +}); ``` ### 3. Preventing Deletion ```typescript -const protectStrategicAccounts: Hook = { +const protectStrategicAccounts = defineHook({ name: 'protect_strategic_accounts', object: 'account', events: ['beforeDelete'], @@ -747,13 +752,13 @@ const protectStrategicAccounts: Hook = { throw new Error(`Cannot delete account with ${oppCount} active opportunities`); } }, -}; +}); ``` ### 4. Data Enrichment ```typescript -const enrichLeadScore: Hook = { +const enrichLeadScore = defineHook({ name: 'lead_scoring', object: 'lead', events: ['beforeInsert', 'beforeUpdate'], @@ -782,13 +787,13 @@ const enrichLeadScore: Hook = { ctx.input.score = score; }, -}; +}); ``` ### 5. Triggering Workflows ```typescript -const notifyOnStatusChange: Hook = { +const notifyOnStatusChange = defineHook({ name: 'notify_status_change', object: 'opportunity', events: ['afterUpdate'], @@ -810,13 +815,13 @@ const notifyOnStatusChange: Hook = { // }); } }, -}; +}); ``` ### 6. Creating Related Records ```typescript -const createAuditTrail: Hook = { +const createAuditTrail = defineHook({ name: 'audit_trail', object: ['account', 'contact', 'opportunity'], events: ['afterInsert', 'afterUpdate', 'afterDelete'], @@ -836,13 +841,13 @@ const createAuditTrail: Hook = { } : undefined, }); }, -}; +}); ``` ### 7. External API Integration ```typescript -const syncToExternalCRM: Hook = { +const syncToExternalCRM = defineHook({ name: 'sync_external_crm', object: 'account', events: ['afterInsert', 'afterUpdate'], @@ -867,13 +872,13 @@ const syncToExternalCRM: Hook = { console.error('Failed to sync to external CRM', error); } }, -}; +}); ``` ### 8. Multi-Object Logic ```typescript -const cascadeAccountUpdate: Hook = { +const cascadeAccountUpdate = defineHook({ name: 'cascade_account_updates', object: 'account', events: ['afterUpdate'], @@ -887,13 +892,13 @@ const cascadeAccountUpdate: Hook = { ); } }, -}; +}); ``` ### 9. Conditional Execution ```typescript -const highValueAccountAlert: Hook = { +const highValueAccountAlert = defineHook({ name: 'high_value_alert', object: 'account', events: ['afterInsert'], @@ -904,7 +909,7 @@ const highValueAccountAlert: Hook = { console.log(`🚨 High-value account created: ${ctx.result.name}`); // Send alert to sales leadership }, -}; +}); ``` ### 10. Data Masking (Read Operations) @@ -916,7 +921,7 @@ const highValueAccountAlert: Hook = { > subscription covers both `find` and `findOne`. ```typescript -const maskSensitiveData: Hook = { +const maskSensitiveData = defineHook({ name: 'mask_pii', object: ['contact', 'lead'], events: ['afterFind'], // fires for findOne too — no separate afterFindOne @@ -942,7 +947,7 @@ const maskSensitiveData: Hook = { } } }, -}; +}); ``` --- @@ -1013,16 +1018,16 @@ export const onEnable = async (ctx: { ql: ObjectQL }) => { ```typescript // src/objects/account.hook.ts -import { Hook, HookContext } from '@objectstack/spec/data'; +import { defineHook, HookContext } from '@objectstack/spec/data'; -const accountHook: Hook = { +const accountHook = defineHook({ name: 'account_logic', object: 'account', events: ['beforeInsert', 'beforeUpdate'], handler: async (ctx: HookContext) => { // Validation logic }, -}; +}); export default accountHook; @@ -1250,7 +1255,7 @@ const validators = [ validateWebsite, ]; -const composedHook: Hook = { +const composedHook = defineHook({ name: 'validation_suite', object: 'account', events: ['beforeInsert', 'beforeUpdate'], @@ -1259,13 +1264,13 @@ const composedHook: Hook = { await validator(ctx); } }, -}; +}); ``` ### Conditional Hook Execution ```typescript -const conditionalHook: Hook = { +const conditionalHook = defineHook({ name: 'enterprise_only', object: 'account', events: ['afterInsert'], @@ -1277,7 +1282,7 @@ const conditionalHook: Hook = { // Enterprise-specific logic }, -}; +}); ``` --- diff --git a/skills/objectstack-data/rules/hooks.md b/skills/objectstack-data/rules/hooks.md index 679db3b267..2118134270 100644 --- a/skills/objectstack-data/rules/hooks.md +++ b/skills/objectstack-data/rules/hooks.md @@ -28,9 +28,9 @@ The canonical reference includes: ```typescript import { P } from '@objectstack/spec'; -import { Hook, HookContext } from '@objectstack/spec/data'; +import { defineHook, HookContext } from '@objectstack/spec/data'; -const hook: Hook = { +const hook = defineHook({ name: 'my_hook', // Required: unique identifier object: 'account', // Required: target object(s) events: ['beforeInsert'], // Required: lifecycle events @@ -40,9 +40,16 @@ const hook: Hook = { priority: 100, // Optional: execution order async: false, // Optional: background execution (after* only) condition: P`record.status == 'active'`, // Optional: conditional execution (CEL) -}; +}); ``` +Prefer `defineHook()` over a bare `: Hook` literal (the same rule as +`defineDatasource`): it validates when the module is imported, so +constraint-level mistakes a bare annotation can't catch — a non-`snake_case` +`name`, a misspelled key routed through a spread — fail while you author +instead of at deploy, and the export carries defaults already materialized +(#4269). + ### Logic: `body` (preferred) or `handler` (deprecated) A hook's logic comes from **either** an inline `handler` function **or** a diff --git a/skills/objectstack-data/rules/validation.md b/skills/objectstack-data/rules/validation.md index 85523797d6..0050fabf17 100644 --- a/skills/objectstack-data/rules/validation.md +++ b/skills/objectstack-data/rules/validation.md @@ -235,9 +235,9 @@ Calling an external API, hitting another object, or running arbitrary code is lifecycle hook and throw on failure — the typed, supported extension point: ```typescript -import { Hook, HookContext } from '@objectstack/spec/data'; +import { defineHook, HookContext } from '@objectstack/spec/data'; -const taxIdCheck: Hook = { +const taxIdCheck = defineHook({ name: 'tax_id_external_check', object: 'account', events: ['beforeInsert', 'beforeUpdate'], @@ -246,7 +246,7 @@ const taxIdCheck: Hook = { throw new Error('Invalid tax ID'); } }, -}; +}); ``` ## Validation Properties From c0af9948d0a271a6e203ba25bc8c6eceffcef086 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Fri, 31 Jul 2026 10:40:49 +0800 Subject: [PATCH 2/2] =?UTF-8?q?chore:=20empty=20changeset=20=E2=80=94=20do?= =?UTF-8?q?cs-only=20PR,=20releases=20nothing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same convention as #4279: the changeset gate wants an explicit statement of release intent, and skills/ ships in no npm package. Co-Authored-By: Claude Fable 5 --- .changeset/skills-definehook-examples.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/skills-definehook-examples.md diff --git a/.changeset/skills-definehook-examples.md b/.changeset/skills-definehook-examples.md new file mode 100644 index 0000000000..9d536cf87f --- /dev/null +++ b/.changeset/skills-definehook-examples.md @@ -0,0 +1,6 @@ +--- +--- + +docs-only: skills/objectstack-data hook examples author with `defineHook()` +instead of bare `: Hook` literals, closing the #4269 authoring-validation loop +on the skill surface (#4274). Releases nothing.