JustFlows

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

Observe

Actions

Run after something happened. Return values are ignored. Use them for indexing, notifications, logging, and synchronization.

Decide

Gates

Run before a mutation. Call event.cancel(reason) to stop it with a message suitable for the operator.

Transform

Filters

Receive a value and must return the next value. Handlers form a predictable pipeline ordered by priority.

typescript
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.

typescript
interface HookContext {
  siteId?: string;
  requestId?: string;
  source?: "http" | "job" | "cli" | "system";
  actor?: { userId?: string; role?: string };
}

Actions

HookPurposeValue / payload
app.starting / app.startedApplication lifecycle{ version }
app.stoppingApplication shutting down{}
content.created / content.updatedContent row created or updated{ contentId, siteId, type?, translationGroupId? }
content.published / content.unpublishedPublishing state changed{ contentId, siteId, type?, translationGroupId? }
content.deletedContent moved to trash{ contentId, siteId, type?, translationGroupId?, lastInTranslationGroup? }
content.revisionSaved / revisionDiscarded / revisionRestoredA working revision was saved, discarded, or restored{ contentId, siteId, revisionId, source?, actorId? }
media.uploadedMedia file stored{ siteId, mediaId, url }
media.deletedMedia file removed{ siteId, mediaId }
user.created / updated / deletedUser account changed{ userId }
user.accessChangedA user's role assignment changed{ userId, roleId }
access.roleCreated / roleUpdated / roleDeletedA custom role changed{ roleId }
auth.login / auth.logoutAuthentication completed{ userId, email }
auth.loginFailedA sign-in attempt failed{ email, reason }
plugin.installed / activated / deactivated / uninstalledPlugin lifecycle{ pluginId, version, siteId? }
plugin.deleteDataFired once a deleted plugin's deleteData() has finished{ pluginId, version, siteId? }
theme.installed / activatedTheme lifecycle{ themeId, version, siteId? }
core.updatedJustflows core was upgraded{ fromVersion, toVersion, source }
webhook.deliveredAn outbound webhook attempt finished{ deliveryId, endpointId, event, attempt, status, responseStatus, responseBody, error }
request.before / request.afterHTTP request lifecycle{ method, path, statusCode?, durationMs? }
site.underConstruction.viewedA visitor saw the under-construction splash{ siteId }
cache.revalidatedSelective cache revalidation completed{ trigger, objects, siteId? }
staticExport.completedA static / edge export run finished{ ok, mode, outDir, publicUrl, pages, assets, bytes, pruned, durationMs, errors }
staticExport.deployPush the exported directory to object storage or a CDN{ outDir, publicUrl, manifest, summary }
email.queued / sent / failedSystem email delivery lifecycle{ messageType, recipient, transport, status, attempt, detail?, templateKey?, locale? }

Gates

HookPurposeValue / payload
content.beforeCreateValidate or cancel content creation{ input: { siteId, type?, title, slug?, excerpt?, fields? } }
content.beforeUpdateValidate the proposed revision{ contentId, siteId, revision?, revisionId? }
content.beforePublishBlock publishing with a human-readable reason{ contentId, siteId, revision?, revisionId? }
content.beforeDeleteValidate deletion{ contentId, siteId }
media.beforeUploadValidate uploads before storage{ siteId, filename, mimeType, sizeBytes }
media.beforeDeleteValidate media deletion{ siteId, mediaId }
email.beforeSendCancel a rendered system email before it queues or sends{ messageType, recipient, transport, templateKey?, locale? }
typescript
ctx.hooks.gate("content.beforePublish", (event) => {
  if (!event.revision?.excerpt?.trim()) {
    event.cancel("Add an excerpt before publishing.");
  }
});

Filters

HookPurposeValue / payload
webhook.eventTypesAdd event names administrators may subscribe tostring[]
webhook.payloadShape JSON-safe event data before the host signs itunknown
content.input / content.outputTransform API contentRecord<string, unknown>
content.blocksTransform the stored block tree before renderingblock tree
content.renderTransform rendered public HTMLstring
comments.renderReplace the public comment thread markupstring
content.revisionCanonical live/working snapshot behind a revision gate or filter{ title, slug, excerpt, blocks, fields }
media.metadataTransform stored media metadataRecord<string, unknown>
navigation.itemsAdd or change navigation items (runs after the host resolves a menu, visibility rules applied)NavigationItem[]
menu.design.presetsContribute one-click menu layout presets to the visual designer (static data, ids "<pluginId>:<slug>")MenuDesignPreset[]
menu.visibility.evaluateAnswer an item-level custom visibility condition your plugin owns (seeded false, fail-closed)boolean
header.templates / header.resolve / header.configContribute, take over, or adjust site headersHeaderTemplate[] / HeaderConfig | null / HeaderConfig
admin.menuContribute admin navigationAdminNavItem[]
plugin.settingsOverlay values on the plugin settings screenRecord<string, unknown>
plugin.settings.writeIntercept a settings save to persist domain rows and drop keysRecord<string, unknown>
openapi.documentExtend the public OpenAPI documentOpenApiDocument
http.responseHeadersAdd response headers (must run synchronously)Record<string, string>
html.head / analytics.headContribute safe head markup (sync; html.head context includes the page locale)string
theme.cssAppend plugin CSS to the theme cascadestring
comments.spamBackendScore a comment submission with an external spam service (host thresholds still apply)SpamCheckBackend | null
search.backendTake over indexing and querying for site search with a plugin-owned engineSearchBackend | null
seo.sitemapPathsAdd public sitemap pathsstring[]
staticExport.routesAdd or remove seed paths before the export crawlstring[]
staticExport.formActionOverride the <form action> written for a dynamic endpointstring
staticExport.assetsAdd same-origin asset URLs the scanner cannot discoverstring[]
site.underConstruction.renderReplace the under-construction page markup (must run synchronously)string
email.sender / email.subject / email.html / email.textAdjust final outgoing system-email fieldsEmailSender | string
typescript
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.