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 } |
| content.created / updated | Canonical content changed | { contentId, siteId, type? } |
| content.published / unpublished | Publishing state changed | { contentId, siteId } |
| content.deleted | Content moved to trash | { contentId, siteId, lastInTranslationGroup? } |
| content.revisionSaved | Working revision saved | { contentId, siteId, revisionId } |
| media.uploaded / deleted | Media library changed | { siteId, mediaId, url? } |
| user.created / updated / deleted | User account changed | { userId } |
| auth.login / auth.logout | Authentication completed | { userId, email } |
| plugin.activated / deactivated | Plugin state changed | { pluginId, version, siteId? } |
| theme.installed / activated | Theme state changed | { themeId, version, siteId? } |
| request.before / request.after | HTTP request lifecycle | { method, path, statusCode?, durationMs? } |
Gates
| Hook | Purpose | Value / payload |
|---|---|---|
| content.beforeCreate | Validate or cancel content creation | { input } |
| content.beforeUpdate | Validate the proposed revision | { contentId, siteId, revision? } |
| content.beforePublish | Block publishing with a human-readable reason | { contentId, siteId, revision? } |
| content.beforeDelete | Validate deletion | { contentId, siteId } |
| media.beforeUpload | Validate uploads before storage | { siteId, filename, mimeType, sizeBytes } |
| media.beforeDelete | Validate media deletion | { siteId, mediaId } |
ctx.hooks.gate("content.beforePublish", (event) => {
if (!event.revision?.excerpt?.trim()) {
event.cancel("Add an excerpt before publishing.");
}
});Filters
| Hook | Purpose | Value / payload |
|---|---|---|
| content.input / content.output | Transform API content | Record<string, unknown> |
| content.blocks | Transform the block tree before rendering | block tree |
| content.render | Transform rendered public HTML | string |
| comments.render | Replace the public comment thread markup | string |
| navigation.items | Add or change navigation items | NavigationItem[] |
| header.templates / resolve / config | Contribute and customize site headers | HeaderConfig |
| admin.menu | Contribute admin navigation | AdminNavItem[] |
| plugin.settings / settings.write | Read or intercept plugin settings | Record<string, unknown> |
| http.responseHeaders | Add response headers | Record<string, string> |
| html.head / analytics.head | Contribute safe head markup | string |
| theme.css | Append plugin CSS to the theme cascade | string |
| seo.sitemapPaths | Add public sitemap paths | string[] |
| openapi.document | Extend the public OpenAPI document | OpenApiDocument |
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.