Skip to content

Hooks

Hooks run at lifecycle stages around collection and global operations. Use them to validate, transform, audit, or short-circuit writes. This page is the reference for the hook map, the context object, and the execution model. For how to register hooks from a plugin, see Plugins.

Hook stages

There are eight stages, run in this order for each operation:

StageRuns beforeCommon use
beforeOperationAny operation beginsAuth checks, logging, early aborts
beforeValidateValidation runsMutate input before schema validation
beforeSaveThe write hits the databaseLast-chance transformation
afterSaveThe write completesSide effects, cache invalidation
afterPublishA publish operation completesSyndication, webhook fan-out
afterDeleteA delete completesCleanup, audit logging
afterReadA read completesShape output, attach metadata
afterErrorAn operation throwsError logging, alerting

The runtime calls hooks in registration order: collection hooks first, then plugin hooks, then global hooks. Hooks registered at the same stage run sequentially in the order they were added.

HookContext

Every hook receives a HookContext object with the operation metadata and the document data.

type HookContext = {
type: "collection" | "global";
key: string; // collection or global key
stage: HookStageName; // the current stage
operation: "create" | "update" | "delete" | "read" | "publish";
data: Record<string, unknown>; // the incoming document data
original?: Record<string, unknown> | null; // prior data on update/delete
extensions?: Record<string, unknown>;
logger?: unknown;
meta?: {
requestId?: string;
auth?: unknown;
[key: string]: unknown;
};
};

For update and delete operations, original holds the document state before the change. For create and read, original is null.

HookFunction and HookResult

A hook is a function that takes the context and returns a result. The return type controls what happens next.

type HookFunction = (context: HookContext) => HookResult | Promise<HookResult>;
type HookResult = void | HookAbortResult | HookMutationResult;

void

Return nothing (or undefined) to let the operation continue unchanged. This is what most read-only hooks do.

Mutate (HookMutationResult)

Return { data, meta } to replace the document data and merge metadata before the operation continues. The runtime applies your data to the context before the next stage runs.

type HookMutationResult = {
data?: Record<string, unknown>;
meta?: Record<string, unknown>;
};

Use this in beforeValidate or beforeSave to transform the document before it reaches validation or the database.

Abort (HookAbortResult)

Return { abort: true, reason?, data?, meta? } to stop the operation. The runtime rejects the request with an error. The optional reason becomes the error message.

type HookAbortResult = {
abort: true;
reason?: string;
data?: Record<string, unknown>;
meta?: Record<string, unknown>;
};

Use this in beforeOperation or beforeValidate to reject a write that passes schema validation but violates a business rule.

Registering hooks

Hooks can be registered in three places:

  1. On a collection or global definition, under the hooks key.
  2. On a plugin definition, under the hooks key.
  3. Dynamically in a plugin’s setup function, using registerHook(stage, fn).
defineCollection({
key: "posts",
fields: [/* ... */],
hooks: {
beforeValidate: [
(ctx) => {
// Auto-generate a slug from the title if none is set
if (!ctx.data.slug && ctx.data.title) {
return { data: { ...ctx.data, slug: slugify(ctx.data.title) } };
}
},
],
afterSave: [
async (ctx) => {
// Send to an analytics pipeline
await analytics.track("post_saved", { id: ctx.data.id });
},
],
},
});

The runtime merges hook maps from collections, globals, and plugins. When multiple hooks target the same stage, they run in order: the collection’s hooks, then each plugin’s hooks in plugin registration order.

Execution model

The runner iterates hooks at a stage sequentially. Each hook receives the context as updated by the previous hook. If a hook returns a mutation, the mutated data carries forward to the next hook. If a hook aborts, the runner stops and the operation fails.

After all mutation stages complete (beforeOperation, beforeValidate, beforeSave), the runtime performs the write. Then afterSave, afterPublish, or afterDelete runs. afterRead runs on read operations. afterError runs when any operation throws, giving you a place to log or react.

The hook runner is an advanced surface. The hook registration shape (hooks: { stage: [fn] }) and HookContext are stable for application and plugin authors. The runner internals in @timbl/core are internal.

See also

  • Plugins: how plugins register hooks and the full plugin API
  • Configuration: registering hooks on collections and globals
  • API Tiers: which hook surfaces are stable vs. advanced