Skip to content

Hooks

Hooks are awaited in priority order. On a hook the table marks as transforming, a handler may return a new payload, which replaces the one later handlers and the operation itself see. On the other hooks the return value is ignored: a before... handler refuses the operation by throwing, and after... handlers observe. Register them from a plugin (see Plugins) or with manablox.hooks.on(name, handler, { priority, source }).

Context types: HookContextBase carries the instance and, for request-scoped hooks, the actor and space, and environmentId, the space environment the hook runs for where one applies; ContentHookContext adds the content type; FieldHookContext adds the field. ContentWriteInput is the validated save payload; ContentRecord a stored row. All are exported from @manablox/core.

Unpublishing a document also unpublishes its published descendants: content:afterUnpublish runs once for each of them, after the one for the document itself, while content:beforeUnpublish runs for the document only. Deleting a document with its children runs content:afterDelete the same way, once for each deleted descendant. After the per row hooks, content:afterUnpublishMany and content:afterDeleteMany run once per operation with every affected row, the target document first; their ContentManyHookContext carries spaceId, actor and rootId. Rows may span types, so resolve each through manablox.contentTypes. A handler that queries per row is better placed on these.

Writes that touch several rows commit as one: a duplicated subtree, a folder in every locale, a plan of content types, a space with its basic setup, several new members at once. The after... hooks of such a write, and of every single write, run once it has committed, so a failed write runs none of them. The built-in cleanup (menu entries, tags and the home nomination of deleted documents, asset usages, closing an open approval on publish) runs inside the transaction instead, before the plugin hooks, and commits or rolls back with the write.

Content write hooks carry services in their context: repos and content bound to the write (the type, HookServices, is filled in by @manablox/services). Inside a larger write the before... hooks run while its transaction is open, and services write through it, so what a handler writes commits or rolls back with the write. A handler that writes must use them: services held elsewhere wait for the transaction to end, which blocks on SQLite and holds locks on Postgres. Outside a transaction, and in after... hooks, services are the root ones.

content:beforeRead runs when one document is read by id, before the row loads; its context carries spaceId, actor and published but no type, and a handler throws to refuse the read. content:beforeList runs before a list page, a tree, a level of children, a filter relation or a menu loads; its ContentListRequest payload names the kind of read, the typeIds it is limited to (empty for any type), the locale and, per kind, parentId, fieldId or menu. Its context carries spaceId, actor and published, and a handler throws to refuse the whole read. content:afterList runs once per list page after content:afterReadMany. field:beforeValidate runs on every field of the type and of each block before validation; an absent field arrives as undefined and a returned value supplies it. field:afterRead runs on each top level field of a read row, before content:afterRead and before fields the actor may not read are removed.

The delivery APIs (REST and GraphQL) run the same read hooks on every document they return: lists, lookups by id or permalink, relations, filter relations, children, menu entries and templates. Their context has a null actor, and published is false only in preview. A row a hook drops reads as missing there too. They run content:beforeRead for a document asked for by id (before the row loads) or by permalink (once the path resolves, with the found id), and content:beforeList for lists, children, filter relations and menus. A handler that throws refuses the read: a single read answers 404 content.notFound in REST and a null field with that error in GraphQL; a refused list or relation comes back empty and a refused menu as missing. A thrown ManabloxError keeps its own key and status. Each hook is skipped when no handler is registered, so unused hooks cost nothing.

Rows read from the published projection carry searchText: null (the search vector too): delivery filters on these columns but does not return them. Draft reads (preview and the management API) keep searchText.

contentType:beforeCreate and contentType:beforeUpdate run ahead of validation, so the returned input is validated and stored; ContentTypeUpdateHookContext adds previous, the type before the write. asset:beforeUpload runs after the size and type checks, with the detected mime type; the returned filename is stored, and a handler throws to reject the upload.

space:beforeCreate runs before a space is created or imported, with the input or the export’s space. member:beforeGrant runs before a role is granted or changed, also for the owner of a new or imported space and for the first account, which becomes owner of every space (a space whose handler refuses is skipped and logged); previous is the prior role or null. space:beforeLocalesChange runs when an update changes a space’s locales. apiKey:beforeIssue, menu:beforeCreate and redirect:beforeCreate run before the row is written. redirect:beforeCreate carries source: manual, or auto for the redirect a publish records when a permalink moved; a refused automatic redirect is skipped and logged, and the publish goes on. A handler throws to refuse; a thrown ManabloxError keeps its key and status. A space import runs menu:beforeCreate and redirect:beforeCreate for each row it restores, before it writes anything, and data providers’ checks after them (the workflows plugin runs workflows:beforeEnable there, the webhooks plugin webhooks:beforeCreate), so a refusal leaves no space behind; hooks without a handler are skipped. Plugins declare hooks of their own as <id>:<event>: the workflows plugin’s are in Workflows, the webhooks plugin’s in Webhooks, the website plugin’s in Forms and links.

asset:afterDelete, mail:afterSend, request:served, the space:after* and the apiHost:after* hooks only observe: a handler that throws is logged and the operation goes on. space:afterCreate (a created or imported space), space:afterUpdate (with previousUrl) and space:afterDelete run once the write committed, with the space’s url; apiHost:afterCreate and apiHost:afterDelete likewise, per API host added to or removed from a space, with its hostname. asset:afterDelete runs once an asset and its files are gone; size includes the variants, except for assets removed with their last space, where it is the original only. mail:afterSend runs per sent mail, for notifications and the mail actions of workflows; kind names the sender (notification, or what a plugin that sends mail passes, e.g. workflows) and transport is account for mail sent through an editor’s own account. request:served (RequestServed: surface, spaceId, status, bytes, cached) runs after responses of the delivery APIs (GraphQL and /v1), of /api/v1 reads and GraphQL requests made with an API key (surface management), of media and of plugin modes, whose surface is the mode’s name (website). It never delays the response: bytes is the content-length, else counted while the body streams out, and the hook runs once the body was sent. cached marks answers from the response cache and 304 answers. With no handler registered nothing is measured.

Lifecycle

HookPayloadContextTransforms
before:initundefinedHookContextBaseno
after:initundefinedHookContextBaseno
before:startundefinedHookContextBaseno
after:startundefinedHookContextBaseno
before:stopundefinedHookContextBaseno

Registry

HookPayloadContextTransforms
registry:contentTypesContentTypeDefinition[]HookContextBaseyes
registry:afterReload{ synced: boolean; spaceId?: string | null }HookContextBaseno

Content write path

HookPayloadContextTransforms
content:beforeValidateContentWriteInputContentHookContextyes
content:beforeCreateContentWriteInputContentHookContextyes
content:afterCreateContentRecordContentHookContextno
content:beforeUpdateContentWriteInputContentHookContextyes
content:afterUpdateContentRecordContentHookContextno
content:beforeDelete{ id: string }ContentHookContextno
content:afterDelete{ id: string; record: ContentRecord }ContentHookContextno
content:afterDeleteManyContentRecord[]ContentManyHookContextno
content:beforePublishContentRecordContentHookContextno
content:afterPublishContentRecordContentHookContextno
content:beforeUnpublish{ id: string }ContentHookContextno
content:afterUnpublish{ id: string }ContentHookContextno
content:afterUnpublishManyContentRecord[]ContentManyHookContextno

Content read path

HookPayloadContextTransforms
content:beforeRead{ id: string }ContentBeforeReadHookContextno
content:beforeListContentListRequestContentBeforeListHookContextno
content:afterReadContentRecord | nullContentHookContextyes
content:afterReadManyContentRecord[]ContentReadManyHookContextyes
content:afterListContentRecord[]ContentReadManyHookContextyes

Field-level

HookPayloadContextTransforms
field:beforeValidateunknownFieldHookContextyes
field:afterReadunknownFieldHookContextyes

Content types

HookPayloadContextTransforms
contentType:beforeCreateContentTypeInputHookContextBaseyes
contentType:afterCreateContentTypeDefinitionHookContextBaseno
contentType:beforeUpdateContentTypeInputContentTypeUpdateHookContextyes
contentType:afterUpdateContentTypeDefinitionHookContextBaseno
contentType:afterDeleteContentTypeDefinitionHookContextBaseno

Assets

HookPayloadContextTransforms
asset:beforeUpload{ filename: string; mimeType: string; size: number }HookContextBaseyes
asset:afterUpload{ id: string }HookContextBaseno
asset:afterDelete{ id: string; spaceIds: string[]; size: number }HookContextBaseno

Spaces

HookPayloadContextTransforms
space:beforeCreateSpaceCreateInputHookContextBaseno
space:beforeLocalesChange{ spaceId: string; locales: string[]; previous: string[] }HookContextBaseno
space:afterCreate{ spaceId: string; url: string }HookContextBaseno
space:afterUpdate{ spaceId: string; url: string; previousUrl: string }HookContextBaseno
space:afterDelete{ spaceId: string; url: string }HookContextBaseno

API hosts

HookPayloadContextTransforms
apiHost:afterCreate{ id: string; spaceId: string; hostname: string }HookContextBaseno
apiHost:afterDelete{ id: string; spaceId: string; hostname: string }HookContextBaseno
HookPayloadContextTransforms
menu:beforeCreate{ spaceId: string; name: string; machineName: string }HookContextBaseno
menu:afterWrite{ id: string; spaceId: string }HookContextBaseno
menu:afterDelete{ id: string; spaceId: string }HookContextBaseno

Members

HookPayloadContextTransforms
member:beforeGrant{ spaceId: string; userId: string; role: string; previous: string | null }HookContextBaseno
member:afterGrant{ spaceId: string; userId: string; role: string; previous: string | null; mail?: boolean }HookContextBaseno

API keys

HookPayloadContextTransforms
apiKey:beforeIssue{ userId: string; name: string; spaceIds: string[] | null; permissions: string[] | null; expiresAt: Date | null }HookContextBaseno

Redirects

HookPayloadContextTransforms
redirect:beforeCreate{ spaceId: string; locale: string | null; fromPath: string; source: RedirectSource }HookContextBaseno

Mail

HookPayloadContextTransforms
mail:afterSend{ spaceId: string | null; kind: string; recipients: number; transport: 'instance' | 'account' }HookContextBaseno

Requests

HookPayloadContextTransforms
request:servedRequestServedHookContextBaseno

Cache

HookPayloadContextTransforms
cache:purge{ tags: string[] }HookContextBaseno