Agent skill

Prefect Webhooks Skill

Receive and authenticate Prefect webhooks. Use when setting up an endpoint for Prefect automation notifications sent by the Custom Webhook notification block, checking the secret header you configured, debugging 401s, or handling flow run Failed or Completed notifications. Prefect requests are unsigned.

Install this skill

npx skills add hookdeck/webhook-skills --skill prefect-webhooks


Prefect automations can notify an HTTP endpoint through the Custom Webhook notification block (this skill is about that outbound notification, not Prefect Cloud's inbound webhooks). The request is not signed. You authenticate it with a secret header that you add in the block's headers; your endpoint compares that header with the value it has stored.

A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Use HTTPS and rotate the credential if it leaks.

When to Use This Skill

  • How do I receive Prefect webhooks from an automation?
  • How do I authenticate Prefect Custom Webhook notifications?
  • Does Prefect sign webhook payloads? (No: use a secret header)
  • How do I handle a flow run Failed or Completed notification?
  • Why does my Prefect webhook endpoint return 401?

Verification (core)

const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyPrefectRequest(
  headers,
  secret = process.env.PREFECT_WEBHOOK_SECRET,
  headerName = process.env.PREFECT_WEBHOOK_HEADER || 'Authorization'
) {
  if (!secret) return false;                        // never accept when unconfigured
  const received = headers[headerName.toLowerCase()];
  return Boolean(received) && safeEqual(received, secret);  // WHOLE value, no Bearer parsing
}
  • The header name is whatever you typed in the block's headers, so read it from PREFECT_WEBHOOK_HEADER (default Authorization) and look it up case-insensitively.
  • PREFECT_WEBHOOK_SECRET is the exact, whole header value, including any Bearer prefix you typed. Do not strip or parse the prefix.
  • Python: hmac.compare_digest on bytes (encode both sides; str raises on non-ASCII).
  • Missing header or mismatch: 401. Authenticate before parsing the body.
  • The block's method is configurable (POST by default): accept POST, PUT, PATCH and GET, and handle an empty body. In Express, read the body with express.raw({ type: '*/*' }).

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Payload and Events

Prefect adds no event field and no event type. The body is the block's json_data or form_data template with {{subject}}, {{body}} and {{name}} substituted (plus any {{secret}} values from the block's secrets). The examples assume this illustrative template; use your own:

{"subject": "{{subject}}", "body": "{{body}}", "name": "{{name}}"}
Notification (illustrative)Source
flow run Failed notificationAn automation fires on a flow run entering Failed
flow run Completed notificationAn automation fires on a flow run entering Completed

Which notification you got depends on the automation that triggered it; add your own field to the template (or match on subject) to tell them apart.

Environment Variables

PREFECT_WEBHOOK_HEADER=Authorization            # header name you set in the block
PREFECT_WEBHOOK_SECRET=Bearer your-shared-secret  # exact whole header value

Local Development

Prefect Cloud can't reach localhost. Tunnel with the Hookdeck CLI (no account required):

npx hookdeck-cli listen 3000 prefect --path /webhooks/prefect

Use the printed URL as the Custom Webhook block's URL.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 5, 2026

View on GitHub →