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
| Hook | Payload | Context | Transforms |
|---|
before:init | undefined | HookContextBase | no |
after:init | undefined | HookContextBase | no |
before:start | undefined | HookContextBase | no |
after:start | undefined | HookContextBase | no |
before:stop | undefined | HookContextBase | no |
Registry
| Hook | Payload | Context | Transforms |
|---|
registry:contentTypes | ContentTypeDefinition[] | HookContextBase | yes |
registry:afterReload | { synced: boolean; spaceId?: string | null } | HookContextBase | no |
Content write path
| Hook | Payload | Context | Transforms |
|---|
content:beforeValidate | ContentWriteInput | ContentHookContext | yes |
content:beforeCreate | ContentWriteInput | ContentHookContext | yes |
content:afterCreate | ContentRecord | ContentHookContext | no |
content:beforeUpdate | ContentWriteInput | ContentHookContext | yes |
content:afterUpdate | ContentRecord | ContentHookContext | no |
content:beforeDelete | { id: string } | ContentHookContext | no |
content:afterDelete | { id: string; record: ContentRecord } | ContentHookContext | no |
content:afterDeleteMany | ContentRecord[] | ContentManyHookContext | no |
content:beforePublish | ContentRecord | ContentHookContext | no |
content:afterPublish | ContentRecord | ContentHookContext | no |
content:beforeUnpublish | { id: string } | ContentHookContext | no |
content:afterUnpublish | { id: string } | ContentHookContext | no |
content:afterUnpublishMany | ContentRecord[] | ContentManyHookContext | no |
Content read path
| Hook | Payload | Context | Transforms |
|---|
content:beforeRead | { id: string } | ContentBeforeReadHookContext | no |
content:beforeList | ContentListRequest | ContentBeforeListHookContext | no |
content:afterRead | ContentRecord | null | ContentHookContext | yes |
content:afterReadMany | ContentRecord[] | ContentReadManyHookContext | yes |
content:afterList | ContentRecord[] | ContentReadManyHookContext | yes |
Field-level
| Hook | Payload | Context | Transforms |
|---|
field:beforeValidate | unknown | FieldHookContext | yes |
field:afterRead | unknown | FieldHookContext | yes |
Content types
| Hook | Payload | Context | Transforms |
|---|
contentType:beforeCreate | ContentTypeInput | HookContextBase | yes |
contentType:afterCreate | ContentTypeDefinition | HookContextBase | no |
contentType:beforeUpdate | ContentTypeInput | ContentTypeUpdateHookContext | yes |
contentType:afterUpdate | ContentTypeDefinition | HookContextBase | no |
contentType:afterDelete | ContentTypeDefinition | HookContextBase | no |
Assets
| Hook | Payload | Context | Transforms |
|---|
asset:beforeUpload | { filename: string; mimeType: string; size: number } | HookContextBase | yes |
asset:afterUpload | { id: string } | HookContextBase | no |
asset:afterDelete | { id: string; spaceIds: string[]; size: number } | HookContextBase | no |
Spaces
| Hook | Payload | Context | Transforms |
|---|
space:beforeCreate | SpaceCreateInput | HookContextBase | no |
space:beforeLocalesChange | { spaceId: string; locales: string[]; previous: string[] } | HookContextBase | no |
space:afterCreate | { spaceId: string; url: string } | HookContextBase | no |
space:afterUpdate | { spaceId: string; url: string; previousUrl: string } | HookContextBase | no |
space:afterDelete | { spaceId: string; url: string } | HookContextBase | no |
API hosts
| Hook | Payload | Context | Transforms |
|---|
apiHost:afterCreate | { id: string; spaceId: string; hostname: string } | HookContextBase | no |
apiHost:afterDelete | { id: string; spaceId: string; hostname: string } | HookContextBase | no |
| Hook | Payload | Context | Transforms |
|---|
menu:beforeCreate | { spaceId: string; name: string; machineName: string } | HookContextBase | no |
menu:afterWrite | { id: string; spaceId: string } | HookContextBase | no |
menu:afterDelete | { id: string; spaceId: string } | HookContextBase | no |
Members
| Hook | Payload | Context | Transforms |
|---|
member:beforeGrant | { spaceId: string; userId: string; role: string; previous: string | null } | HookContextBase | no |
member:afterGrant | { spaceId: string; userId: string; role: string; previous: string | null; mail?: boolean } | HookContextBase | no |
API keys
| Hook | Payload | Context | Transforms |
|---|
apiKey:beforeIssue | { userId: string; name: string; spaceIds: string[] | null; permissions: string[] | null; expiresAt: Date | null } | HookContextBase | no |
Redirects
| Hook | Payload | Context | Transforms |
|---|
redirect:beforeCreate | { spaceId: string; locale: string | null; fromPath: string; source: RedirectSource } | HookContextBase | no |
Mail
| Hook | Payload | Context | Transforms |
|---|
mail:afterSend | { spaceId: string | null; kind: string; recipients: number; transport: 'instance' | 'account' } | HookContextBase | no |
Requests
| Hook | Payload | Context | Transforms |
|---|
request:served | RequestServed | HookContextBase | no |
Cache
| Hook | Payload | Context | Transforms |
|---|
cache:purge | { tags: string[] } | HookContextBase | no |