Note: This document is a reference pointer. Complete documentation has been moved to the canonical hooks skill.
For comprehensive data lifecycle hooks documentation, see:
→ objectstack-data/references/data-hooks.md
The canonical reference includes:
- All 8 lifecycle events (beforeFind, afterFind, beforeInsert, afterInsert, beforeUpdate, afterUpdate, beforeDelete, afterDelete)
- Complete Hook definition schema
- HookContext API reference
- Registration methods (declarative, programmatic, file-based)
- 10+ common patterns with full examples
- Performance considerations and optimization tips
- Testing strategies (unit and integration)
- Best practices and anti-patterns
import { P } from '@objectstack/spec';
import { Hook, HookContext } from '@objectstack/spec/data';
const hook: Hook = {
name: 'my_hook', // Required: unique identifier
object: 'account', // Required: target object(s)
events: ['beforeInsert'], // Required: lifecycle events
handler: async (ctx: HookContext) => {
// Your logic here
},
priority: 100, // Optional: execution order
async: false, // Optional: background execution (after* only)
condition: P`record.status == 'active'`, // Optional: conditional execution (CEL)
};A hook's logic comes from either an inline handler function or a
metadata-native body. Prefer body for new code — it is what a
metadata-only runtime executes, and it ships as plain JSON inside the build
artifact. handler (inline function) is deprecated; when both are present the
runtime uses body.
// Sandboxed body: `source` is the function body, run in an isolated QuickJS VM.
{
name: 'fill_position_on_hire',
object: 'candidate',
events: ['afterUpdate'],
body: {
language: 'js', // 'js' (sandboxed) | 'expression' (pure CEL)
source: `
if (!ctx.result || ctx.result.stage !== 'hired') return;
// afterUpdate ctx.result is PARTIAL — re-query for the lookup FK.
const rec = await ctx.api.object('candidate').findOne({ where: { id: ctx.result.id } });
if (rec && rec.position_id)
await ctx.api.object('position').update({ id: rec.position_id, status: 'filled' });
`,
capabilities: ['api.read', 'api.write'], // declare every ctx API the body touches
},
}Sandbox essentials (full contract in references/data-hooks.md → Sandboxed Hook Bodies):
ctxexposesinput,previous(undefinedon insert →!ctx.previousdetects create),result(⚠️ partial on afterUpdate — re-query for unwritten fields),user,session,event,object,api,log(ctx.log.info(msg)),crypto(randomUUID).ctx.api.object(n)repo:find/findOne/count/insert/update({ id, ...fields })/upsert/delete. Query key iswhere(object +$-operators) — notfilter: [[…]].capabilities(declare what the body uses, else it throws) — the six legal tokens:api.read,api.write,api.transaction,crypto.uuid,crypto.hash,log.- Cross-object writes obey the target's sharing model — a
public_readtarget rejects the write withFORBIDDEN, and admin is not exempt. - No
console(usectx.log), nofetch(use Connectors), noimport/require/ module-scope helpers — abodymust be self-contained.
| Event | When Fires | Use Case |
|---|---|---|
beforeFind |
Before any read (find and findOne) |
Filter queries, log access |
afterFind |
After any read (find and findOne) |
Transform results, enrich data |
beforeInsert |
Before creating a record | Set defaults, validate |
afterInsert |
After creating a record | Send notifications |
beforeUpdate |
Before updating a record (single or bulk multi:true) |
Validate changes |
afterUpdate |
After updating a record (single or bulk) | Trigger workflows |
beforeDelete |
Before deleting a record (single or bulk multi:true) |
Check dependencies |
afterDelete |
After deleting a record (single or bulk) | Clean up related data |
One read event, one write event per kind.
beforeFind/afterFindfire forfindOnetoo (the event attaches to record materialization, not the method), and the write events fire on bulkmulti:trueoperations as well — the row-scoping predicate is inctx.input.ast. There is nobeforeFindOne,beforeCount,beforeAggregate, or*Manyevent.Don't reach for a hook when a declarative mechanism already fits:
- Read authorization / row filtering → RLS / permission rules, not a
beforeFindhook.- Field masking → field-level metadata (secret/masked fields), not an
afterFindhook.- Delete guards → a
beforeDeletehook (this is the right tool).
See the full documentation for complete examples of:
- Setting Default Values — Auto-populate fields on insert
- Data Validation — Custom validation rules beyond declarative
- Preventing Deletion — Block deletes based on conditions
- Data Enrichment — Calculate and set derived fields
- Triggering Workflows — Fire notifications and integrations
- Creating Related Records — Maintain referential integrity
- External API Integration — Sync with external systems
- Multi-Object Logic — Cascade updates across objects
- Conditional Execution — Use
conditionproperty - Data Masking — PII protection in read operations
Three methods available:
// objectstack.config.ts
export default defineStack({
hooks: [accountHook, contactHook],
});ctx.ql.registerHook('beforeInsert', async (hookCtx) => {
// Handler logic
}, { object: 'account', priority: 100 });// src/objects/account.hook.ts
export default {
name: 'account_logic',
object: 'account',
events: ['beforeInsert'],
handler: async (ctx) => { /* ... */ },
};✅ DO:
- Use
before*for validation,after*for side effects - Set
async: truefor non-critical background work - Use
ctx.apifor cross-object operations - Handle errors gracefully with meaningful messages
- Test hooks in isolation and integration
❌ DON'T:
- Don't perform expensive operations in
before*hooks - Don't create infinite loops (hooks triggering themselves)
- Don't use
object: '*'unless absolutely necessary - Don't throw in
after*hooks unless critical - Don't assume
ctx.sessionexists
- objectstack-data/SKILL.md#lifecycle-hooks — Complete hooks system overview
- objectstack-data/references/data-hooks.md — Full data hooks documentation
- objectstack-platform/references/plugin-hooks.md — Plugin hook system
- objectstack-automation — Flows and Workflows for advanced automation
For complete documentation with detailed examples, context API reference, testing strategies, and performance optimization, see the canonical reference: