Guide to Prefect Webhooks: Features and Best Practices
Prefect webhooks let your workflow orchestration talk to the rest of your stack. When a flow run changes state, or a metric such as lateness or duration crosses a threshold, a Prefect automation can call your own endpoint, so you can open an incident, post to a chat tool Prefect has no block for, kick off a downstream job or record the run in your own systems.
This guide covers how outgoing webhooks work in Prefect, how to build the request with a Custom Webhook block, how to authenticate it with a secret header, and the practices that keep a Prefect receiver reliable.
What are Prefect webhooks?
Prefect uses the word "webhook" for two different things. Prefect Cloud webhooks are inbound: they receive requests from other services and turn them into Prefect events. This guide is about the other direction: outgoing notifications sent through a Custom Webhook notification block.
A notification block is a saved configuration that Prefect uses to send messages. Prefect has dedicated blocks for services such as Slack, and the Custom Webhook block for everything else. When an automation's trigger fires, its notification action sends a message through the block you chose. Prefect fills in a subject and body describing what happened, and the block turns them into an HTTP request using a template you write.
Because you write the template, Prefect sends exactly what you configure, plus a User-Agent: Prefect Notifications header. It adds no event type, no envelope and no signature. To authenticate the request, add a secret header to the block.
Prefect webhook features
| Feature | Details |
|---|---|
| Configuration | A Custom Webhook notification block, used by an automation's notification action or by notify() in your code |
| Method | GET, POST (default), PUT, PATCH or DELETE |
| Body | json_data (JSON) or form_data (a dict sent form-encoded, or a raw string). Not both |
| Other fields | Query params, headers, cookies |
| Placeholders | {{subject}}, {{body}}, {{name}} and any key in secrets, in every string field |
| Secrets | A secrets dictionary, stored as secret values and substituted into placeholders |
| Prefect-set header | User-Agent: Prefect Notifications |
| Signature and timestamp | None |
| Event type | None. Defined by your template |
| Timeout | 10 seconds by default, configurable on the block |
| Failure handling | A non-2xx response raises an error. Retries are not documented |
| Source IPs | Not published. A self-hosted Prefect server sends from wherever it runs |
What Prefect sends
Here is a block that sends JSON to your own endpoint and authenticates with a header:
{
"name": "ops-webhook",
"url": "https://example.com/webhooks/prefect",
"method": "POST",
"json_data": {
"source": "prefect",
"subject": "{{subject}}",
"body": "{{body}}",
"block": "{{name}}"
},
"headers": {
"X-Prefect-Secret": "{{webhook_secret}}"
},
"secrets": {
"webhook_secret": "<your long random secret>"
}
}
When the automation fires, Prefect replaces {{subject}} and {{body}} with the notification's subject and body text, {{name}} with the block's name, and {{webhook_secret}} with the stored secret. A subject that is not set is sent as the string null. Prefect checks the placeholders when the block is created, and rejects any that are not subject, body, name or a key in secrets.
The subject and body come from the automation's notification action, which you write with Jinja templating. Prefect does not add a structured flow run ID or state to the request on its own, so put what your receiver needs into that message: {{ flow_run.id }}, {{ flow_run.name }} and {{ flow_, for example, or the whole run with {{ flow_run | tojson }}. The block then places that text into your json_data template, where it arrives as a string that your receiver parses.
Inspect Prefect webhooks as they arrive. Point a Custom Webhook block at a Hookdeck Inspect URL to see exactly what your template renders, with no account or setup required.
Setting up Prefect webhooks
Generate a long random secret first, for example with openssl rand -hex 32.
- In Prefect, create a Custom Webhook notification block.
- Set Webhook URL to your HTTPS endpoint, and leave the method as
POSTunless you need another. - Write the JSON Data template with the fields your receiver needs.
- Under Custom Secret Values, add your secret, for example as
webhook_secret. - Under Headers, add a header such as
X-Prefect-Secretwith the value{{webhook_secret}}. - Save the block, then create or edit an automation and choose it in the notification action.
You can create the same block in Python with CustomWebhookNotificationBlock(...) and .save(), and send a test message with block.notify("test").
To route through Hookdeck, create a Hookdeck source with the Prefect type and use its URL as the block's URL. The source asks for an API Key Header and an API Key: enter the header name and the secret's actual value, not the {{webhook_secret}} placeholder. The source pre-selects POST but leaves the method open, because the block lets you choose. If your block uses GET, PUT, PATCH or DELETE, add that method to the source's allowed HTTP methods as well.
Securing Prefect webhooks
Read the header you chose, compare its whole value with your stored secret in constant time, and reject the request if either is missing. Accept the method your block uses.
const crypto = require('crypto');
const express = require('express');
const HEADER_NAME = (process.env.PREFECT_WEBHOOK_HEADER || 'x-prefect-secret').toLowerCase();
const SECRET = process.env.PREFECT_WEBHOOK_SECRET;
function safeEqual(received, expected) {
const a = Buffer.from(received, 'utf8');
const b = Buffer.from(expected, 'utf8');
// timingSafeEqual throws on unequal lengths, so check first.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const app = express();
app.post('/webhooks/prefect', express.raw({ type: '*/*' }), (req, res) => {
const received = req.get(HEADER_NAME);
if (!SECRET || typeof received !== 'string' || !safeEqual(received, SECRET)) {
return res.sendStatus(401);
}
let notification;
try {
notification = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
// Fields are whatever your json_data template defines,
// e.g. notification.subject and notification.body.
res.sendStatus(200);
});
The same check in Python:
import hmac
import os
HEADER_NAME = os.environ.get("PREFECT_WEBHOOK_HEADER", "X-Prefect-Secret")
SECRET = os.environ.get("PREFECT_WEBHOOK_SECRET", "")
def verify_prefect_request(headers) -> bool:
if not SECRET:
return False # never accept when unconfigured
received = headers.get(HEADER_NAME) # Starlette and Flask headers are case-insensitive
if not received:
return False
# Compare bytes: compare_digest raises TypeError on a non-ASCII str.
return hmac.compare_digest(received.encode("utf-8"), SECRET.encode("utf-8"))
A static header proves the sender knows the secret. It does not cover the body, and there is no timestamp, so a captured request can be replayed and will still pass. Use HTTPS, keep the secret in the block's secrets rather than typing it into the headers directly, and rotate it in the block and in your receiver together if it might have leaked.
Make Prefect webhooks production-ready. Hookdeck Event Gateway checks the secret header you configure, stores every notification durably, and retries delivery to your service, which the Custom Webhook block does not do for you.
Prefect webhook limitations and pain points
One attempt, then an error
The Problem: If your endpoint is down or slow when an automation fires, the notification is lost, and the only trace is an error in Prefect.
Why It Happens: The Custom Webhook block makes a single request with a 10-second default timeout and raises an error on a non-2xx response. Prefect does not document retries for failed notifications.
Workarounds:
- Respond quickly and keep the endpoint highly available.
- Watch for the
prefect.automation.action.failedevent, which Prefect emits when an automation action fails, and alert on it. - Reconcile with the Prefect API, for example by querying recent failed flow runs, so a missed notification is caught.
How Hookdeck Can Help: Hookdeck accepts each request as soon as it arrives and stores it, then delivers to your service with retries you control. You can replay any notification after a fix, so an outage on your side no longer loses alerts.
No structure unless you add it
The Problem: Your receiver gets free text and has to work out which flow run failed and why.
Why It Happens: Prefect fills {{subject}} and {{body}} with notification text. The block adds no event type, run ID or state of its own.
Workarounds:
- Use Jinja in the automation's message to include
flow_run.id,flow_run.state.nameand anything else you need, or serialise the run withtojsonand parse it on arrival. - Add constant fields in
json_data, such as"source": "prefect"and an environment name, so several blocks can share one endpoint.
How Hookdeck Can Help: Hookdeck transformations can parse and reshape the body before it reaches your service, and filters can route on any field your template includes.
The secret is only as good as the header
The Problem: Nothing proves a request body came from Prefect unchanged.
Why It Happens: There is no signature, only the static header you configure.
Workarounds:
- Use a long random secret, sent only over HTTPS.
- Treat notifications as signals, and confirm anything consequential through the Prefect API before acting.
How Hookdeck Can Help: Hookdeck's Prefect source compares the header you name with the exact value you enter. Hookdeck answers with a 200 either way, but requests that fail the check are recorded as failed verification and never reach your service.
Best practices
Store the secret in secrets
Reference it as {{webhook_secret}} in the header. That keeps it out of the plain block configuration and makes rotation a single change.
Template for your receiver
Include fixed fields that identify the sender and environment alongside {{subject}} and {{body}}. Your receiver should never have to guess which automation sent a request.
Use POST with JSON
Unless the receiving service demands otherwise, keep the default POST and use json_data. It is the simplest thing to authenticate, parse and route.
Make handlers idempotent
An automation can fire more than once for related state changes. Deduplicate on a run ID you include in the message, plus the state. See our guide to webhook idempotency.
Acknowledge fast and process asynchronously
Respond inside the block's timeout and do the real work afterwards. See why to process webhooks asynchronously.
Conclusion
Prefect's outgoing webhooks are Custom Webhook notification blocks: a request you design, filled with the notification's subject and body, with no signature and no event type. Store a long random secret in the block's secrets, send it in a header, compare it whole in constant time over HTTPS, add the structure your receiver needs to the template, and plan for the fact that a failed notification is not retried for you.
Hookdeck Event Gateway checks the header, keeps every notification, and retries and replays delivery to your service, so a brief outage does not cost you the alert about a failed flow.
Get started with Hookdeck for free and handle Prefect webhooks reliably in minutes.