Directus Flows for Webhook-Driven Revalidation
This guide, part of Directus Data Layer Patterns, connects Directus to the frontend’s cache. When an editor publishes, updates or deletes an item, a Directus flow sends a signed request to the frontend, which verifies it and revalidates exactly the affected pages. The guide covers the flow’s trigger and operations, filtering out draft noise, signing the request, the receiving handler, and how to monitor the whole chain.
Directus replaced its older webhooks feature with flows, which are more flexible: a trigger, such as an event on a collection, followed by a chain of operations, such as conditions, scripts and HTTP requests. That flexibility means a revalidation flow has to be designed deliberately. A naive flow fires on every save of every item, including drafts and system collections, and calls the frontend without authentication.
The Problem
A publisher’s Directus instance had a flow that called the frontend’s revalidation endpoint on every items.update event, for every collection. Editors saving drafts triggered hundreds of revalidations an hour for pages whose public content had not changed, and updates to internal collections, such as user preferences, also triggered calls. The endpoint accepted any request, and when a staging instance was pointed at the production frontend by mistake, staging edits revalidated production pages with staging data briefly visible.
How the Flow Should Work
Trigger. An event hook on items.create, items.update and items.delete, scoped to the collections that produce public pages. Choose a non-blocking action, so the editor’s save does not wait for the frontend.
Filter. A condition operation that continues only when the item is published, or when its status changed from published to something else, which also affects the public site. Draft saves stop here.
Sign. A script operation builds a small payload, collection, keys, event and a timestamp, and computes an HMAC-SHA256 signature with a shared secret available to the flow.
Send. A request operation posts the payload to the frontend with the signature in a header.
Verify and revalidate. The frontend verifies the signature and timestamp, then revalidates cache tags for the collection and keys.
Implementation
The script operation in the flow receives the trigger data and returns the payload and signature. Directus runs script operations in a sandbox with access to the data passed in; the secret is read from the flow’s environment through a preceding operation or configured as a flow variable, depending on your Directus version and setup.
// Flow script operation: "sign payload"
module.exports = async function (data) {
const trigger = data.$trigger;
const payload = {
collection: trigger.collection,
keys: trigger.keys ?? [trigger.key],
event: trigger.event, // e.g. "articles.items.update"
status: trigger.payload?.status ?? null,
ts: Math.floor(Date.now() / 1000),
};
const body = JSON.stringify(payload);
const crypto = require("crypto");
const signature = crypto.createHmac("sha256", data.$env.REVALIDATE_SECRET).update(body).digest("hex");
return { body, signature };
};
The request operation then posts {{sign_payload.body}} with the header x-revalidate-signature: {{sign_payload.signature}} and content-type: application/json. Allow the flow access to the secret environment variable through Directus’s configuration for flows, and keep the secret different per environment.
The frontend handler verifies before doing anything.
// app/api/revalidate/route.ts
import { createHmac, timingSafeEqual } from "node:crypto";
import { revalidateTag } from "next/cache";
const MAX_AGE = 120;
export async function POST(req: Request) {
const raw = await req.text();
const given = Buffer.from(req.headers.get("x-revalidate-signature") ?? "", "hex");
const expected = createHmac("sha256", process.env.REVALIDATE_SECRET!).update(raw).digest();
if (given.length !== expected.length || !timingSafeEqual(given, expected)) return new Response("invalid signature", { status: 401 });
const p = JSON.parse(raw) as { collection: string; keys: (string | number)[]; ts: number };
if (Math.abs(Date.now() / 1000 - p.ts) > MAX_AGE) return new Response("stale", { status: 401 });
for (const key of p.keys) revalidateTag(`${p.collection}:${key}`);
revalidateTag(`${p.collection}:list`);
return Response.json({ revalidated: p.keys.length }, { status: 202 });
}
Fetches in the frontend’s data layer carry matching tags, articles:42 for a single article and articles:list for listings, so revalidation reaches exactly the right cache entries.
Deletions and status changes
When an item is deleted, the trigger carries the keys but no payload; the flow should still send the request, and the condition must allow delete events through. When a published item is moved back to draft, the public page must disappear, so the condition should also pass updates where the status changed away from published. Checking the previous status requires reading the item before the update, which a filter-type trigger can do; the simpler alternative is to pass all status changes through and let revalidation fetch the current state, which is idempotent.
Revalidating related content
Pages rarely depend on one item alone. An article page shows its author, category and related articles; a category page lists many articles; the homepage shows the latest items from several collections. Decide how far a change should propagate. The simplest robust approach tags every fetch with every item it reads, so an article page’s data carries tags for the article, its author and its category, and a change to the author revalidates every page that displayed that author. With the Directus SDK, collect the keys of related items from the response and add them as tags when caching. Listing pages carry a collection list tag, revalidated on any change in the collection. This keeps the flow itself simple, one request per event with the collection and keys, while the frontend’s tags determine which pages refresh. Avoid encoding relationship knowledge in the flow, which would have to change whenever templates change.
Configuration Reference
| Setting | Recommendation | Why |
|---|---|---|
| Trigger | event hook on routable collections only | No noise from internal collections. |
| Action type | non-blocking | Editors do not wait for the frontend. |
| Condition | published items, status changes, deletes | Draft saves stop in Directus. |
| Signature | HMAC-SHA256 over the body, with timestamp | Forged and replayed requests rejected. |
| Secret | per environment | Staging cannot revalidate production. |
| Tags | collection plus key, and collection list | Precise revalidation. |
Gotchas & Edge Cases
- Bulk edits. Updating hundreds of items at once runs the flow for the batch with many keys. The handler should handle arrays and, for very large batches, revalidate the collection list tag instead of each key.
- Relational changes. Changing an author’s name changes every article showing it. Either tag article pages with the author key too, or add a flow for the authors collection that revalidates the related articles’ tags.
- Flow errors are silent. A failing request operation is logged in the flow’s run history but not shown to editors. Monitor it.
- Clock skew. The timestamp check requires reasonably synchronized clocks between Directus and the frontend servers.
Worked Example
The publisher rebuilt its flow with a scoped trigger, a status condition and signed requests with per-environment secrets. Revalidations fell from several hundred per hour to the number of actual publishes, a few dozen, and the frontend’s data cache hit rate recovered. When a staging instance was again misconfigured to call production, every request failed verification, and nothing on the public site changed.
Monitoring the Flow
The flow is a critical path that fails without visible symptoms: if it breaks, the site simply stops updating. Watch it from both ends. In Directus, the flow’s run log shows each execution and failed operations; alert when failures appear, for example through a second flow that watches for errors or by exporting logs. On the frontend, log each verified revalidation with the collection and key count, and alert when none arrive during working hours despite publishing activity. A synthetic probe that updates a test item on a schedule and checks the public page closes the loop, as described in monitoring webhook delivery.
Rollout Checklist
- Trigger the flow only on routable collections, as a non-blocking action.
- Stop draft saves with a condition; pass publishes, status changes and deletes.
- Sign payloads with a per-environment secret and a timestamp.
- Verify signatures and timestamps before revalidating tags.
- Tag frontend fetches by collection and key, plus list tags.
- Monitor flow failures and revalidation activity.
Frequently Asked Questions
Can the flow call a build hook instead of revalidation?
Yes, for static sites. Add debouncing, because every publish would otherwise trigger a full build, and a burst of edits would queue many builds in a row.
Should one flow handle all collections?
One flow with a scoped trigger is fine. Separate flows per collection help when collections need different conditions or targets, for example a product collection that must also notify a search indexer.
Does the flow slow down saving?
Not when the trigger’s action is non-blocking; the flow runs after the save completes, and editors never notice it.
What about Directus instances without flow access to environment variables?
Store the secret in a restricted settings collection read by the flow, or use a shared secret header set in the request operation; both are better than no verification.