JustFlows

Документация пока доступна только на английском языке. Остальная часть сайта соответствует вашему языку.

Hooks

Typed actions, gates, and filters — the complete core hook list and how to register them.

10 минута чтения

Hooks are how you change what Justflows does without changing Justflows. Write the plugin in plugins/<name>/ (copy plugins/hello-world) and register hooks in activate(). Names autocomplete from @justflows/sdk.

I want to…Use a…Names look like
React after something happenedactioncontent.published
Stop something before it commitsgatecontent.beforeCreate
Change a value on the way throughfiltercontent.render

Actions

Observe only. Cannot cancel. If your handler throws, Justflows logs it against your plugin and continues — a broken analytics plugin must not block publishing. Treat payloads as read-only. Async handlers are awaited in order.

ActionWhen
app.starting / app.started / app.stoppingProcess lifecycle
content.created / updated / deleted / published / unpublishedContent
comments.createdA visitor or moderator comment was stored as pending or approved: { siteId, contentId, commentId, parentId, status }. Spam and trash do not fire it, and it carries no email, IP address, or comment text.
media.uploaded / media.deletedMedia
user.created / updated / deletedUsers (needs users:read)
auth.login / logout / loginFailedAuth (needs auth:hook)
workspace.created / suspended / reactivated / deletedMultisite workspace lifecycle. No database password in the payload.
workspace.purgedA suspended or deleted workspace was removed for good. A notification only.
site.createdA site was added to a workspace
plugin.installed / activated / deactivated / uninstalledPlugins
theme.installed / theme.activatedThemes
request.before / request.afterHTTP
page.viewedA visitor received a public page: status below 400, not a preview, not the static exporter's crawl. Fired after the response is sent, with { siteId, path, locale?, contentId?, userAgent, referrer?, host }
site.underConstruction.viewedUnpublished site hit
cache.revalidatedAfter selective cache revalidate
webhook.deliveryAfter each outgoing webhook attempt (status, response, error)
staticExport.completed / staticExport.deployA static / edge export run finished; deploy fires right after so a plugin can push the directory to a CDN
tenancy.workspaceCreatedWith workspace.created, plus the first administrator's email. Listening needs platform:tenancy.
quota.updatedA platform operator or plugin saved limits. Listening needs platform:tenancy.
domain.added / activated / failed / removedCustom domain lifecycle
email.receivedAn inbound message was accepted for this site (needs mail:hook)

Gates

Run before commit. Call event.cancel("Human-readable reason") to abort. Fail closed: if your gate throws, the operation is aborted and attributed to your plugin. Priority order; first cancellation wins.

  • content.beforeCreate / beforeUpdate / beforeDelete / beforePublish
  • media.beforeUpload / media.beforeDelete
  • workspace.beforeCreate / beforeSuspend / beforeReactivate / beforeDelete / beforePurge, site.beforeCreate
  • domain.beforeAdd (refuse a custom domain before it is added)
ts
ctx.hooks.gate("media.beforeUpload", (event) => {
  if (event.sizeBytes > 10_000_000) {
    event.cancel("Files must be under 10 MB.");
  }
});

Filters

You must return a value. On core hooks TypeScript enforces it. If a custom filter returns undefined, the previous value is kept and a warning is logged. Throws skip your filter and keep the last good value.

FilterTransforms
content.input / content.outputContent records in/out
content.renderPublic HTML string (may be async)
media.metadataMetadata object
navigation.itemsMenu tree (runs after the host resolves a menu, visibility rules applied)
menu.design.presetsOne-click menu layout presets a plugin/theme ships (static data; ids "<pluginId>:<slug>")
menu.visibility.evaluateAnswer an item-level custom visibility condition your plugin owns (seeded false, fail-closed)
http.responseHeadersHeader map (must be sync)
html.headExtra <head> HTML (public pages await it, so a handler may be async). The PublicPageContext carries the page's locale, title, content type and fields, description, canonical URL, share image, origin, and whether it is indexable.
html.titleThe page's <title>, seeded with the page title. Not applied when the page is not indexable.
html.canonicalThe absolute URL in <link rel="canonical">; return "" to drop it. Not applied when the page is not indexable.
site.identity{ siteTitle, tagline } per locale, seeded from the site's identity settings
plugin.settings.schemaA plugin's settings form for this site: drop fields or fill a select's options (check pluginId)
admin.bodyHTML added at the end of every administration screen (may be async)
analytics.headTracking markup for a public page's <head>, seeded with "" (must be sync). A tag plugin appends below priority 100; a consent plugin rewrites it at the default priority
security.cspSourcesExtra https:// origins for this response's Content-Security-Policy (script-src, frame-src, connect-src, img-src, …), seeded with {}. 'unsafe-inline', *, paths, and http: are dropped; frame-ancestors cannot be changed
email.routeWhich site files an inbound message, seeded with the mailbox owner's site id; return siteId to claim it for the open site (needs mail:hook)
operatorKey.ensureCalled with hooks.apply: ask the host to create a missing 32-byte key in the installation .env. The name must belong to the calling plugin; the value is never returned or stored in the database
theme.cssPlugin CSS appended to cached /theme.css (may be async)
seo.sitemapPathsArray of sitemap path strings
site.underConstruction.renderUnder-construction HTML (must be sync)
comments.renderRendered public comments block HTML (may be async)
header.templates / header.resolve / header.configContribute named header designs, own a page's header per request, or adjust the resolved HeaderConfig before render
webhook.eventTypesArray of deliverable webhook event names
webhook.payloadWebhook JSON body before it is signed and sent
staticExport.routes / staticExport.assetsSeed paths crawled by the exporter, and same-origin asset URLs the scanner cannot discover
staticExport.formActionOverride the <form action> written into exported HTML for a dynamic endpoint
staticExport.excludePaths the static exporter leaves to the live app ({ path, match: "prefix" | "exact" }[])
theme.layoutScopesCustomizer layout sections (content and wide width) for a public prefix a plugin owns
permalinks.typeBasesDefault permalink bases for a plugin's own content types; stored values win
media.placeholderReplace or clear (null) the placeholder image for a kind (must be sync)
mcp.toolsNo longer listed or run. Offer a plugin route to agents with the mcp route option instead (see Bring your own AI).
account.sectionsServer-rendered sections on the account page
widgets.areasAdd widget areas with default widgets and content types
quota.effectiveLimitLower a stored limit; it can never raise one (needs platform:tenancy)

SYNC_FILTERS: http.responseHeaders and site.underConstruction.render — handlers must not be async. html.head, html.title, html.canonical, and site.identity are awaited. content.blocks, content.render, and theme.css may be async.

Options and ownership

  • priority — lower runs earlier (default 100).
  • once — auto-dispose after first dispatch.
  • id — stable label in diagnostics.
  • network: true — run this handler on every site while the plugin is active on the main site, also on sites that never activated it (needs platform:network; see Network-wide handlers).
  • Plugins may emit only hooks under their own manifest id (justflows.acme-seo.scoreCalculated).
  • Declaration merging on ActionEventMap / GateEventMap / FilterValueMap types your own names.

Network-wide handlers

On a multisite installation a handler normally runs only on the sites that activated the plugin. Register it with { network: true } to run it on every site while the plugin is active on the main site. That needs the sensitive platform:network permission; without it, registering throws. Only the handlers you mark run network-wide: routes, jobs, and every other handler stay on the sites that activated the plugin. Inside a network handler, ctx.settings and ctx.data still belong to the request's site; read the main site's choices with ctx.network.mainSiteSettings.get(key) and its secrets with ctx.network.mainSiteSecrets, and link to a site with ctx.network.site(siteId?), which returns its public url. Files the handler's markup loads from /ext/<plugin id>/ are served on every site.

ts
ctx.hooks.filter("admin.body", async (html) => {
  const tag = await ctx.network?.mainSiteSettings.get<string>("adminTag");
  if (!tag || !/^[A-Z0-9-]+$/.test(tag)) return html;
  return `${html}<script src="/ext/acme.tags/tag.js" data-tag="${tag}"></script>`;
}, { network: true });

Header and comment hooks

header.templates contributes named headers that appear in the per-page picker, build()-rendered at request time. header.resolve lets a plugin own a page's header per request; header.config adjusts the resolved header before render. comments.render restyles or replaces the rendered comments block. New SDK types: HeaderConfig, HeaderTemplate, HeaderBuildContext, HeaderResolveContext, CommentsBlockRenderContext, PublicComment. Outgoing webhooks add two filters — webhook.eventTypes to register event names and webhook.payload to reshape a body — plus the webhook.delivery action after every attempt; see Webhooks. Access-policy changes emit access-change hooks; a plugin registers a runtime capability with ctx.capabilities.register() and declares cookies with ctx.cookies.declare() (reading the resolved site registry with ctx.cookies.list()). The synchronous analytics.head filter lets Cookie consent defer third-party tags until consent is granted. For menus, menu.design.presets contributes one-click layout presets to the designer's design panel and menu.visibility.evaluate resolves a plugin-defined item condition — an unrecognized or deactivated condition id hides the item. See docs/HOOKS.md → Contributing a menu design preset in the Community Edition repository.

Note

Longer narrative and cleanup rules: docs/HOOKS.md in the Community Edition repository.