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 }
content.created / updatedCanonical content changed{ contentId, siteId, type? }
content.published / unpublishedPublishing state changed{ contentId, siteId }
content.deletedContent moved to trash{ contentId, siteId, lastInTranslationGroup? }
content.revisionSavedWorking revision saved{ contentId, siteId, revisionId }
media.uploaded / deletedMedia library changed{ siteId, mediaId, url? }
user.created / updated / deletedUser account changed{ userId }
auth.login / auth.logoutAuthentication completed{ userId, email }
plugin.activated / deactivatedPlugin state changed{ pluginId, version, siteId? }
theme.installed / activatedTheme state changed{ themeId, version, siteId? }
request.before / request.afterHTTP request lifecycle{ method, path, statusCode?, durationMs? }

Gates

HookPurposeValue / payload
content.beforeCreateValidate or cancel content creation{ input }
content.beforeUpdateValidate the proposed revision{ contentId, siteId, revision? }
content.beforePublishBlock publishing with a human-readable reason{ contentId, siteId, revision? }
content.beforeDeleteValidate deletion{ contentId, siteId }
media.beforeUploadValidate uploads before storage{ siteId, filename, mimeType, sizeBytes }
media.beforeDeleteValidate media deletion{ siteId, mediaId }
typescript
ctx.hooks.gate("content.beforePublish", (event) => {
  if (!event.revision?.excerpt?.trim()) {
    event.cancel("Add an excerpt before publishing.");
  }
});

Filters

HookPurposeValue / payload
content.input / content.outputTransform API contentRecord<string, unknown>
content.blocksTransform the block tree before renderingblock tree
content.renderTransform rendered public HTMLstring
comments.renderReplace the public comment thread markupstring
navigation.itemsAdd or change navigation itemsNavigationItem[]
header.templates / resolve / configContribute and customize site headersHeaderConfig
admin.menuContribute admin navigationAdminNavItem[]
plugin.settings / settings.writeRead or intercept plugin settingsRecord<string, unknown>
http.responseHeadersAdd response headersRecord<string, string>
html.head / analytics.headContribute safe head markupstring
theme.cssAppend plugin CSS to the theme cascadestring
seo.sitemapPathsAdd public sitemap pathsstring[]
openapi.documentExtend the public OpenAPI documentOpenApiDocument
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.