SDK reference
Hooks
Hooks let plugins observe completed work, stop operations before they happen, or transform values as they move through Justflows. The public names and payloads are versioned in @justflows/sdk.
Actions, gates, and filters
Actions
Run after something happened. Return values are ignored. Use them for indexing, notifications, logging, and synchronization.
Gates
Run before a mutation. Call event.cancel(reason) to stop it with a message suitable for the operator.
Filters
Receive a value and must return the next value. Handlers form a predictable pipeline ordered by priority.
const unsubscribe = ctx.hooks.action(
"content.published",
async (event, hookContext) => {
await updateSearchIndex(event.contentId);
},
{ priority: 100, id: "acme.search.index" }
);
// Optional manual cleanup. The host also cleans up on deactivate.
unsubscribe();Ordering and cleanup
Lower priorities run first; the default is 100. Handlers at the same priority keep registration order. Set once: true for one dispatch. Registration returns an unsubscribe function, and the runtime automatically removes a plugin’s handlers when it deactivates.
Hook context
The second handler argument carries correlation and identity—not secrets or server internals.
interface HookContext {
siteId?: string;
requestId?: string;
source?: "http" | "job" | "cli" | "system";
actor?: { userId?: string; role?: string };
}Actions
| Hook | Purpose | Value / payload |
|---|---|---|
| app.starting / app.started | Application lifecycle | { version } |
| app.stopping | Application shutting down | {} |
| content.created / content.updated | Content row created or updated | { contentId, siteId, type?, translationGroupId? } |
| content.published / content.unpublished | Publishing state changed | { contentId, siteId, type?, translationGroupId? } |
| content.deleted | Content moved to trash | { contentId, siteId, type?, translationGroupId?, lastInTranslationGroup? } |
| content.revisionSaved / revisionDiscarded / revisionRestored | A working revision was saved, discarded, or restored | { contentId, siteId, revisionId, source?, actorId? } |
| media.uploaded | Media file stored | { siteId, mediaId, url } |
| media.deleted | Media file removed | { siteId, mediaId } |
| user.created / updated / deleted | User account changed | { userId } |
| user.accessChanged | A user's role assignment changed | { userId, roleId } |
| access.roleCreated / roleUpdated / roleDeleted | A custom role changed | { roleId } |
| auth.login / auth.logout | Authentication completed | { userId, email } |
| auth.loginFailed | A sign-in attempt failed | { email, reason } |
| plugin.installed / activated / deactivated / uninstalled | Plugin lifecycle | { pluginId, version, siteId? } |
| plugin.deleteData | Fired once a deleted plugin's deleteData() has finished | { pluginId, version, siteId? } |
| theme.installed / activated | Theme lifecycle | { themeId, version, siteId? } |
| core.updated | Justflows core was upgraded | { fromVersion, toVersion, source } |
| webhook.delivered | An outbound webhook attempt finished | { deliveryId, endpointId, event, attempt, status, responseStatus, responseBody, error } |
| request.before / request.after | HTTP request lifecycle | { method, path, statusCode?, durationMs? } |
| site.underConstruction.viewed | A visitor saw the under-construction splash | { siteId } |
| cache.revalidated | Selective cache revalidation completed | { trigger, objects, siteId? } |
| staticExport.completed | A static / edge export run finished | { ok, mode, outDir, publicUrl, pages, assets, bytes, pruned, durationMs, errors } |
| staticExport.deploy | Push the exported directory to object storage or a CDN | { outDir, publicUrl, manifest, summary } |
| email.queued / sent / failed | System email delivery lifecycle | { messageType, recipient, transport, status, attempt, detail?, templateKey?, locale? } |
Gates
| Hook | Purpose | Value / payload |
|---|---|---|
| content.beforeCreate | Validate or cancel content creation | { input: { siteId, type?, title, slug?, excerpt?, fields? } } |
| content.beforeUpdate | Validate the proposed revision | { contentId, siteId, revision?, revisionId? } |
| content.beforePublish | Block publishing with a human-readable reason | { contentId, siteId, revision?, revisionId? } |
| content.beforeDelete | Validate deletion | { contentId, siteId } |
| media.beforeUpload | Validate uploads before storage | { siteId, filename, mimeType, sizeBytes } |
| media.beforeDelete | Validate media deletion | { siteId, mediaId } |
| email.beforeSend | Cancel a rendered system email before it queues or sends | { messageType, recipient, transport, templateKey?, locale? } |
ctx.hooks.gate("content.beforePublish", (event) => {
if (!event.revision?.excerpt?.trim()) {
event.cancel("Add an excerpt before publishing.");
}
});Filters
| Hook | Purpose | Value / payload |
|---|---|---|
| webhook.eventTypes | Add event names administrators may subscribe to | string[] |
| webhook.payload | Shape JSON-safe event data before the host signs it | unknown |
| content.input / content.output | Transform API content | Record<string, unknown> |
| content.blocks | Transform the stored block tree before rendering | block tree |
| content.render | Transform rendered public HTML | string |
| comments.render | Replace the public comment thread markup | string |
| content.revision | Canonical live/working snapshot behind a revision gate or filter | { title, slug, excerpt, blocks, fields } |
| media.metadata | Transform stored media metadata | Record<string, unknown> |
| navigation.items | Add or change navigation items (runs after the host resolves a menu, visibility rules applied) | NavigationItem[] |
| menu.design.presets | Contribute one-click menu layout presets to the visual designer (static data, ids "<pluginId>:<slug>") | MenuDesignPreset[] |
| menu.visibility.evaluate | Answer an item-level custom visibility condition your plugin owns (seeded false, fail-closed) | boolean |
| header.templates / header.resolve / header.config | Contribute, take over, or adjust site headers | HeaderTemplate[] / HeaderConfig | null / HeaderConfig |
| admin.menu | Contribute admin navigation | AdminNavItem[] |
| plugin.settings | Overlay values on the plugin settings screen | Record<string, unknown> |
| plugin.settings.write | Intercept a settings save to persist domain rows and drop keys | Record<string, unknown> |
| openapi.document | Extend the public OpenAPI document | OpenApiDocument |
| http.responseHeaders | Add response headers (must run synchronously) | Record<string, string> |
| html.head / analytics.head | Contribute safe head markup (sync; html.head context includes the page locale) | string |
| theme.css | Append plugin CSS to the theme cascade | string |
| comments.spamBackend | Score a comment submission with an external spam service (host thresholds still apply) | SpamCheckBackend | null |
| search.backend | Take over indexing and querying for site search with a plugin-owned engine | SearchBackend | null |
| seo.sitemapPaths | Add public sitemap paths | string[] |
| staticExport.routes | Add or remove seed paths before the export crawl | string[] |
| staticExport.formAction | Override the <form action> written for a dynamic endpoint | string |
| staticExport.assets | Add same-origin asset URLs the scanner cannot discover | string[] |
| site.underConstruction.render | Replace the under-construction page markup (must run synchronously) | string |
| email.sender / email.subject / email.html / email.text | Adjust final outgoing system-email fields | EmailSender | string |
ctx.hooks.filter(
"content.output",
(content) => ({ ...content, enhancedBy: ctx.pluginId }),
{ priority: 120 }
);Publish plugin-owned hooks
A plugin may emit actions, gates, and filters under its own manifest ID—for example acme.shop.order.created. Core namespaces cannot be spoofed. Export the payload types from your own package so integrators do not have to guess their shape.