ObjectStackObjectStack

Hook

Hook protocol schemas

Hook Lifecycle Events Defines the interception points in the ObjectQL execution pipeline.

Source: packages/spec/src/data/hook.zod.ts

TypeScript Usage

import { HookContextSchema, HookEvent } from '@objectstack/spec/data';
import type { HookContext } from '@objectstack/spec/data';

// Validate data
const result = HookContextSchema.parse(data);

HookContext

Properties

PropertyTypeRequiredDescription
idstringoptionalUnique execution ID for tracing
objectstring
eventEnum<'beforeFind' | 'afterFind' | 'beforeInsert' | 'afterInsert' | 'beforeUpdate' | 'afterUpdate' | 'beforeDelete' | 'afterDelete'>
inputRecord<string, any>Mutable input parameters
resultanyoptionalOperation result (After hooks only)
previousRecord<string, any>optionalRecord state before operation
dispatch{ mode: Enum<'record' | 'per-row'>; index: integer; scope: Record<string, any> }optionalHow this hook call relates to the caller's write (engine-produced)
session{ userId?: string; actor?: string; organizationId?: string; accessToken?: string; … }optionalCurrent session context
provenance{ flowRunId?: string; attributedUserId?: string }optionalServer-stamped write provenance (never client-supplied, never an authorization input)
transactionanyoptionalDatabase transaction handle
qlanyObjectQL Engine Reference
apianyoptionalCross-object data access (IScopedContext — object(name) + transaction(cb))
user{ id?: string; name?: string; email?: string; organizationId?: string }optionalCurrent user info shortcut

Nested Shape: HookContext.dispatch

PropertyTypeRequiredDescription
modeEnum<'record' | 'per-row'>'record' = this call is the caller's whole write; 'per-row' = one of N dispatches for one write
indexinteger0-based position in the per-row fan-out; always 0 when mode is "record"
scopeRecord<string, any>Scratch shared by every dispatch of one caller write, across both phases (same object identity)

Nested Shape: HookContext.session

PropertyTypeRequiredDescription
userIdstringoptional
actorstringoptionalService-principal label for audit attribution when the caller is not a real user (e.g. svc:flow:<flowName>)
organizationIdstringoptionalActive organization ID (blessed developer-facing name)
accessTokenstringoptional
isSystembooleanoptionalTrue when the call was made with an elevated system context (engine self-writes)
skipTriggersbooleanoptionalTrue when record-change automation (flow triggers) must be suppressed for this write — e.g. package seed replay. Lifecycle hooks still run.
skipAutomationsbooleanoptionalTrue when metadata-bound automation hooks must be suppressed for this write — e.g. data import with "run automations" unchecked, or import undo. Implies skipTriggers; code-registered system hooks (audit, security) still run.
positionsstring[]optionalPosition names held by the caller (ADR-0090 D3; formerly roles), copied from ExecutionContext.positions. For hook READS only — e.g. tailoring a message, or branching a business rule the hook runs through its own ctx.api channel. Authorization is decided by the security service on the ExecutionContext (permissions / positions / derived posture); this is NOT an authorization input and a hook must not gate a write by testing it. A hook context carries no services key, so the sharing service cannot be called from one either — the sharing gates already ran inside the engine before the hook chain.
preserveAuditbooleanoptionalTrue when this write is a historical import that must KEEP its caller-supplied updated_at/updated_by (and the readonly audit family) instead of being stamped with the import instant. Server-set, opt-in, absent on normal writes; read by the built-in audit hook. A stamping policy, not an authorization input.
rolesneveroptional[REMOVED] HookContext.session.roles was removed in @objectstack/spec 17.0.0 (ADR-0049 D2) — it was declared, read by two dead exemption branches, and never produced: ObjectQL's buildSession() builds the session field by field and has never written roles, so every read resolved undefined and a guard keyed on it was dead code that merely LOOKED like an authorization decision. Delete the key. To gate a hook on the caller, read ctx.session.userId / ctx.session.isSystem; to judge PRIVILEGE, ask the security service, which evaluates the ADR-0095 vocabulary on the execution context — capability grants (permissions), placements (positions) and the derived posture — never a role-name string comparison (ADR-0090 D3 bans the role spelling outright). Nothing to migrate: a HookContext is built per operation by the engine and never stored, so no metadata source carries this key. NOTE an ACTION body's ctx.session is a different object and still carries its own roles array today; that surface is tracked separately and is not what this key was.

Nested Shape: HookContext.provenance

PropertyTypeRequiredDescription
flowRunIdstringoptionalId of the automation flow run performing this write, when it originates from a flow data node. Lets a hook recognize the run that OWNS state that run itself opened — the approvals record lock exempts the run holding the pending request.
attributedUserIdstringoptionalThe real human credited for a write whose authorization subject was the SYSTEM — e.g. the admin whose better-auth update-member-role call the identity adapter executes as isSystem. ATTRIBUTION ONLY: the audit writer records it as sys_audit_log.user_id; no security middleware reads it, and it never becomes the subject the write is authorized as.

Nested Shape: HookContext.user

PropertyTypeRequiredDescription
idstringoptional
namestringoptional
emailstringoptional
organizationIdstringoptionalActive organization ID of the acting user (equals session.organizationId)

HookEvent

Allowed Values

  • beforeFind
  • afterFind
  • beforeInsert
  • afterInsert
  • beforeUpdate
  • afterUpdate
  • beforeDelete
  • afterDelete

On this page