Overview
Use hooks safely in Cloud as content rules, or run unrestricted function hooks in self-hosted Dyrected.
Hooks are where content changes get cleaned up, shaped, or reacted to.
The important first decision is runtime: Cloud hooks are content rules Dyrected can serialize and run on the hosted content backend, while self-hosted Dyrected can run arbitrary TypeScript functions inside your server.
By the end of this page, you should know which hook shape fits your runtime and where to put the behavior you need.
Start with the runtime boundary
Use Cloud-safe hooks when the behavior should survive sync:schema and run in Dyrected Cloud. In practice, that means hooks defined as serializable Jexl-style content rules, not arbitrary functions.
Use function hooks when the behavior needs arbitrary server code. Function hooks are self-hosted runtime behavior. Reach for them when the logic needs async work, external packages, database writes after persistence, custom integrations, or side effects that belong inside your application server.
If you are building for Cloud and need asynchronous side effects, do not assume a function hook will run on the hosted backend. Cloud content events and webhooks are the intended model for that work, but they are still coming soon.
The hook surfaces
Dyrected has four hook surfaces:
- Collection hooks for document-level logic on repeatable content.
- Global hooks for document-level logic on singleton data such as site settings.
- Field hooks for logic that belongs to one field value.
- Admin field hooks for reactive form behavior in the dashboard.
Admin hooks improve the editing experience, but they are not a security boundary. If a rule must hold on every API path, enforce it with access control, a server hook, or a Cloud-safe rule that runs on the backend.
Cloud hooks are content rules
In Cloud, hooks are content rules. They are serializable, so Dyrected can send them to Cloud during schema sync and evaluate them on the hosted content backend.
The recommended Cloud path is:
- use declarative Jexl-style strings for simple
beforeRead,afterRead, andbeforeChangetransforms - use declarative Jexl-style
admin.hooks.onChangefor browser-side form reactivity that should survive schema sync - keep asynchronous side effects out of Cloud hooks until Cloud content events and webhooks are public
- keep arbitrary function hooks in self-hosted projects
For the full compatibility table, exact expression contexts, and what sync:schema preserves or strips, read Cloud-safe hooks.
Where each family fits
Collections
Collection hooks wrap the full lifecycle of a document in a collection:
beforeReadafterReadbeforeChangeafterChangebeforeDeleteafterDelete
Reach for them when the logic depends on several fields, the whole document, or the surrounding operation.
Globals
Global hooks work the same way, but globals only support the phases that make sense for a singleton document:
beforeReadafterReadbeforeChangeafterChange
There are no delete hooks because globals are updated in place rather than removed.
Fields
Field hooks stay close to one value:
beforeChangeafterRead
They are a good fit for value-level normalization, masking, formatting, and recursive behavior inside nested fields.
Admin field hooks
Admin field hooks run in the form UI:
admin.hooks.onChangeadmin.hooks.options
Use them to derive a slug, recalculate a total, or update select options as editors change related fields.
admin.hooks.onChange also has a declarative string form, which makes it the recommended option when you want the same form behavior to survive Cloud schema sync.
The recommended decision tree
- Use a Cloud-safe hook when the behavior is a simple content rule that must run in Cloud.
- Use a field hook when one value can be transformed on its own.
- Use a collection or global hook when the logic depends on several fields or the surrounding operation.
- Use an admin hook when the goal is immediate editor feedback in the dashboard.
- Use access control when the question is who can read or write.
- Use a self-hosted function hook when the behavior needs arbitrary TypeScript or server-side side effects.
Runtime rules worth remembering
- Hooks in an array run in order.
- When a
before*hook returns a value, that value becomes the input to the next hook in the chain. - Declarative string hooks run synchronously.
- Mixed arrays of functions and strings still chain in order.
- Throwing inside
beforeRead,beforeChange, orbeforeDeletestops the operation. afterChangeandafterDeleteare for side effects after persistence. Their return values are ignored, and their errors are isolated so a successful write is not turned into a500.- Server hooks receive a database adapter, but only
afterChangeandafterDeleteget a writable one.
What to read next
- Collection hooks for the full document lifecycle on collections.
- Cloud-safe hooks for the Cloud-specific compatibility rules.
- Lifecycle events for content events, webhooks, and self-hosted handlers.
- Global hooks for singleton settings and site-wide config.
- Field hooks for value-level transforms and admin form reactivity.
- Hook context for the
req,user, anddbvalues hooks receive.
Generated reference
The contracts below are generated from the public @dyrected/core exports by @dyrected/knowledge, so every hook signature stays in sync with the package. Use them when you need the exact argument and return types behind the guidance above.
AuthenticatedUser
Base shape of an authenticated user as decoded from the JWT.
The actual shape will include every field on your auth collection — this interface only guarantees the properties that Dyrected always stamps on the token. Extend it in your own codebase for stronger typing:
export interface AuthenticatedUser {
/** The user's document ID in the database. */
sub: string;
/** The user's email address. */
email?: string;
/** Slug of the collection this user was authenticated against. */
collection: string;
/** Array of role strings, if your auth collection has a `roles` field. */
roles?: string[];
/** Any additional fields from the auth collection document. */
[key: string]: unknown;
}| Option | Description |
|---|---|
sub (required) | The user's document ID in the database. |
email (optional) | The user's email address. |
collection (required) | Slug of the collection this user was authenticated against. |
roles (optional) | Array of role strings, if your auth collection has a `roles` field. |
CollectionAfterChangeHook
Runs after a document is created or updated in the database.
export type CollectionAfterChangeHook<TDoc extends object = Record<string, unknown>> = (args: {
doc: TDoc;
previousDoc?: TDoc;
req: HookRequestContext;
user?: AuthenticatedUser;
operation: "create" | "update";
db: DatabaseAdapter;
}) => void | Promise<void>;CollectionAfterDeleteHook
Runs after a document has been deleted from the database.
export type CollectionAfterDeleteHook<TDoc extends object = Record<string, unknown>> = (args: {
id: string;
doc: TDoc;
req: HookRequestContext;
user?: AuthenticatedUser;
db: DatabaseAdapter;
}) => void | Promise<void>;CollectionAfterReadHook
Runs after a document (or list of documents) is fetched from the database, before the response is sent to the client.
export type CollectionAfterReadHook<TDoc extends object = Record<string, unknown>> = (args: {
doc: TDoc;
req: HookRequestContext;
user?: AuthenticatedUser;
db: ReadonlyDatabaseAdapter;
}) => TDoc | Promise<TDoc>;CollectionAfterReadHookEntry
Exported type from @dyrected/core.
export type CollectionAfterReadHookEntry<
TDoc extends object = Record<string, unknown>,
> = CollectionAfterReadHook<TDoc> | DeclarativeHookExpression;CollectionBeforeChangeHook
Runs before a document is created or updated in the database.
export type CollectionBeforeChangeHook<TDoc extends object = Record<string, unknown>> = (args: {
data: Partial<TDoc>;
doc?: TDoc;
req: HookRequestContext;
user?: AuthenticatedUser;
operation: "create" | "update";
db: ReadonlyDatabaseAdapter;
}) => Partial<TDoc> | void | Promise<Partial<TDoc> | void>;CollectionBeforeChangeHookEntry
Exported type from @dyrected/core.
export type CollectionBeforeChangeHookEntry<
TDoc extends object = Record<string, unknown>,
> = CollectionBeforeChangeHook<TDoc> | DeclarativeHookExpression;CollectionBeforeDeleteHook
Runs before a document is deleted from the database.
export type CollectionBeforeDeleteHook<TDoc extends object = Record<string, unknown>> = (args: {
id: string;
doc: TDoc;
req: HookRequestContext;
user?: AuthenticatedUser;
db: ReadonlyDatabaseAdapter;
}) => void | Promise<void>;CollectionBeforeReadHook
Runs before Dyrected queries the database for a list or single-document fetch.
Return a new where query object to override or extend the current filter.
Return undefined (or nothing) to leave the query unchanged.
export type CollectionBeforeReadHook = (args: {
req: HookRequestContext;
query?: Record<string, unknown>;
user?: AuthenticatedUser;
db: ReadonlyDatabaseAdapter;
}) => Record<string, unknown> | void | Promise<Record<string, unknown> | void>;CollectionBeforeReadHookEntry
Exported type from @dyrected/core.
export type CollectionBeforeReadHookEntry =
| CollectionBeforeReadHook
| DeclarativeHookExpression;DeclarativeHookExpression
Exported type from @dyrected/core.
export type DeclarativeHookExpression = string;FieldAdminHooks
Exported type from @dyrected/core.
export type FieldAdminHooks<TValue> = {
admin?: {
hooks?: {
onChange?: FieldAdminOnChangeHook<TValue> | DeclarativeHookExpression;
};
};
};FieldAdminOnChangeHook
Exported type from @dyrected/core.
export type FieldAdminOnChangeHook<TValue = unknown> = (
args: FieldAdminOnChangeHookArgs<TValue>,
) => unknown;FieldAdminOnChangeHookArgs
Exported interface from @dyrected/core.
export interface FieldAdminOnChangeHookArgs<TValue = unknown> {
/** Current field value in the form state. */
value: TValue;
/** Current values for sibling fields at the same nesting level. */
siblingData: Record<string, unknown>;
/** Current values for the entire form. */
data: Record<string, unknown>;
/** Imperative setter for async or derived updates. */
setValue: (value: unknown) => void;
}| Option | Description |
|---|---|
value (required) | Current field value in the form state. |
siblingData (required) | Current values for sibling fields at the same nesting level. |
data (required) | Current values for the entire form. |
setValue (required) | Imperative setter for async or derived updates. |
FieldAdminOptionsHook
Exported type from @dyrected/core.
export type FieldAdminOptionsHook = (
args: FieldAdminOptionsHookArgs,
) => FieldAdminOptionsHookResult | Promise<FieldAdminOptionsHookResult>;FieldAdminOptionsHookArgs
Exported interface from @dyrected/core.
export interface FieldAdminOptionsHookArgs {
/** Current values for sibling fields at the same nesting level. */
siblingData: Record<string, unknown>;
/** Current values for the entire form. */
data: Record<string, unknown>;
}| Option | Description |
|---|---|
siblingData (required) | Current values for sibling fields at the same nesting level. |
data (required) | Current values for the entire form. |
FieldAdminOptionsHookResult
Exported type from @dyrected/core.
export type FieldAdminOptionsHookResult = Array<
string | { label: string; value: unknown }
>;FieldAfterReadHook
Exported type from @dyrected/core.
export type FieldAfterReadHook<
TValue = unknown,
TDoc extends object = Record<string, unknown>,
> = (args: FieldAfterReadHookArgs<TValue, TDoc>) => unknown;FieldAfterReadHookArgs
Exported interface from @dyrected/core.
export interface FieldAfterReadHookArgs<
TValue = unknown,
TDoc extends object = Record<string, unknown>,
> {
/** Raw stored field value before this hook transforms it. */
value: TValue;
/** Full document currently being returned to the caller. */
doc: TDoc;
/** Authenticated user requesting the document, if any. */
user?: AuthenticatedUser;
/** Read-only database adapter for related lookups. */
db: ReadonlyDatabaseAdapter;
}| Option | Description |
|---|---|
value (required) | Raw stored field value before this hook transforms it. |
doc (required) | Full document currently being returned to the caller. |
user (optional) | Authenticated user requesting the document, if any. |
db (required) | Read-only database adapter for related lookups. |
FieldBeforeChangeHook
Exported type from @dyrected/core.
export type FieldBeforeChangeHook<
TValue = unknown,
TDoc extends object = Record<string, unknown>,
> = (args: FieldBeforeChangeHookArgs<TValue, TDoc>) => unknown;FieldBeforeChangeHookArgs
Exported interface from @dyrected/core.
export interface FieldBeforeChangeHookArgs<
TValue = unknown,
TDoc extends object = Record<string, unknown>,
> {
/** Current field value after previous hooks in the chain. */
value: TValue;
/** Existing stored document before the write, if this is an update. */
originalDoc?: TDoc;
/** Full incoming payload being written. */
data: Record<string, unknown>;
/** Authenticated user performing the write, if any. */
user?: AuthenticatedUser;
/** Read-only database adapter for related lookups. */
db: ReadonlyDatabaseAdapter;
}| Option | Description |
|---|---|
value (required) | Current field value after previous hooks in the chain. |
originalDoc (optional) | Existing stored document before the write, if this is an update. |
data (required) | Full incoming payload being written. |
user (optional) | Authenticated user performing the write, if any. |
db (required) | Read-only database adapter for related lookups. |
FieldHook
Exported type from @dyrected/core.
export type FieldHook<TDoc extends object = Record<string, unknown>, TValue = unknown> = FieldBeforeChangeHook<
TValue,
TDoc
>;FieldHooks
Exported type from @dyrected/core.
export type FieldHooks<TValue> = {
hooks?: {
beforeChange?: Array<FieldBeforeChangeHook<TValue> | DeclarativeHookExpression>;
afterRead?: Array<FieldAfterReadHook<TValue>>;
};
};GlobalAfterChangeHook
Runs after the global document is updated. Side-effects only.
export type GlobalAfterChangeHook<TDoc extends object = Record<string, unknown>> = (args: {
doc: TDoc;
previousDoc?: TDoc;
req: HookRequestContext;
user?: AuthenticatedUser;
operation: "update";
db: DatabaseAdapter;
}) => void | Promise<void>;GlobalAfterReadHook
Runs after the global document is fetched, before the response is sent.
export type GlobalAfterReadHook<TDoc extends object = Record<string, unknown>> = (args: {
doc: TDoc;
req: HookRequestContext;
user?: AuthenticatedUser;
db: ReadonlyDatabaseAdapter;
}) => TDoc | Promise<TDoc>;GlobalAfterReadHookEntry
Exported type from @dyrected/core.
export type GlobalAfterReadHookEntry<
TDoc extends object = Record<string, unknown>,
> = GlobalAfterReadHook<TDoc> | DeclarativeHookExpression;GlobalBeforeChangeHook
Runs before the global document is updated.
Operation is always 'update' (globals cannot be created or deleted).
export type GlobalBeforeChangeHook<TDoc extends object = Record<string, unknown>> = (args: {
data: Partial<TDoc>;
doc?: TDoc;
req: HookRequestContext;
user?: AuthenticatedUser;
operation: "update";
db: ReadonlyDatabaseAdapter;
}) => Partial<TDoc> | void | Promise<Partial<TDoc> | void>;GlobalBeforeChangeHookEntry
Exported type from @dyrected/core.
export type GlobalBeforeChangeHookEntry<
TDoc extends object = Record<string, unknown>,
> = GlobalBeforeChangeHook<TDoc> | DeclarativeHookExpression;GlobalBeforeReadHook
export type GlobalBeforeReadHook = CollectionBeforeReadHook;GlobalBeforeReadHookEntry
Exported type from @dyrected/core.
export type GlobalBeforeReadHookEntry = CollectionBeforeReadHookEntry;HookFunction
Exported type from @dyrected/core.
export type HookFunction<TDoc extends object = Record<string, unknown>> = (args: {
data?: Partial<TDoc>;
doc?: TDoc;
user?: AuthenticatedUser;
req?: HookRequestContext;
operation?: "create" | "update" | "delete";
db?: DatabaseAdapter;
[key: string]: unknown;
}) => unknown | Promise<unknown>;HookRequestContext
Minimum HTTP request context passed to every server-side hook and resolver.
The full Web Standard Request is available as raw when you need it, but
most hooks only need query (URL search parameters).
export interface HookRequestContext {
/** Parsed URL query-string parameters, e.g. `{ page: '2', search: 'hello' }`. */
query: Record<string, string>;
/** Incoming HTTP headers, lowercased. */
headers: Record<string, string>;
/** The raw Web Standard `Request` object. Useful for streaming or advanced header inspection. */
raw?: Request;
}| Option | Description |
|---|---|
query (required) | Parsed URL query-string parameters, e.g. `{ page: '2', search: 'hello' }`. |
headers (required) | Incoming HTTP headers, lowercased. |
raw (optional) | The raw Web Standard `Request` object. Useful for streaming or advanced header inspection. |