Notifications
Send a signed JSON webhook when a deploy, a rollback or another change to an environment finishes.
Use notify: to post to a chat, open an incident, or record deploys in another system. It lists webhooks that Dockflow calls when a run that changes an environment ends, such as a deploy, a rollback or a restart.
Each call is an HTTP POST with a JSON body. When you give the webhook a secret, the body is signed with HMAC-SHA256.
Example
version: 1
app: shop
notify:
- url: https://hooks.example.com/dockflow
secret: ${WEBHOOK_SECRET}
- url: https://alerts.example.com/hooks/deploys
on: [failure]
headers:
Authorization: Bearer ${ALERTS_TOKEN}
timeout: 5s
environments:
production: {}The first webhook receives every outcome, signed with the WEBHOOK_SECRET secret (PRODUCTION_WEBHOOK_SECRET in CI for production). The second receives failures only, with an extra header.
Options
| Key | Type | Default | Description |
|---|---|---|---|
url | string | required | An http or https URL |
on | list | both | success, failure, or both: the statuses this webhook receives |
secret | string | none | Signs the body: the request carries X-Dockflow-Signature |
headers | map | none | Extra request headers |
timeout | duration | 10s | How long to wait for the response |
String values are interpolated like the rest of dockflow.yml, so ${WEBHOOK_SECRET} reads a secret. A missing secret stops the command with exit code 3. Secret values are masked wherever Dockflow prints them.
notify: can also be set inside an environment. There it replaces the top-level list for that environment.
When webhooks fire
A webhook fires at the end of a run of the agent on the server, for these operations: deploy, rollback, restart, stop, scale, destroy, and maintenance on and off. It does not fire:
- when the run changed nothing (a deploy whose plan is empty);
- when the run could not start because another run holds the lock;
- for failures before the agent runs: a failed build, a fatal
before_*local orhosthook, an invalid configuration; - for read commands (
status,plan,logs…) and forprune.
When a local after_deploy hook fails and triggers a rollback, two webhooks are sent: one for the deploy (success), and one for the rollback run that follows.
The CLI sends the webhooks, after the agent reports the result. If the CLI is stopped before that, they are not sent.
Request
POST /dockflow HTTP/1.1
Content-Type: application/json
User-Agent: dockflow
X-Dockflow-Event: run.finished
X-Dockflow-Signature: sha256=5d1e9c…Your headers are added after these, and replace one of them when they share a name.
Payload
{
"event": "run.finished",
"app": "shop",
"env": "production",
"server": "main",
"op": "deploy",
"run": "01JBF3K9Q2M8XW4T6Y7Z1N5R0C",
"version": "1.4.0",
"release": 42,
"outcome": "rolled_back",
"status": "failure",
"exit_code": 8,
"duration_ms": 73120,
"error": "service web is unhealthy: …",
"at": "2026-10-26T14:03:12.481Z"
}| Field | Description |
|---|---|
event | Always run.finished |
app, env | The app and the environment |
server | The server's name (main unless the environment names it) |
op | deploy, rollback, restart, stop, scale, destroy, maintenance.on or maintenance.off |
run | The run's ID, for dockflow attach <env> <run-id> |
version | The version label the CLI computed for the run (--version, the CI tag, or the commit). For a deploy, the release's version. For other operations, the version of the local checkout, which is not necessarily what runs (dockflow releases shows what runs) |
release | The release number the run created; absent when it created none |
outcome | success, rolled_back, failed, cancelled, lock_lost or hook_failed (see events) |
status | success when outcome is success, failure otherwise. This is what on filters on |
exit_code | The CLI's exit code for this outcome |
duration_ms | How long the run took |
error | The error message, with secret values masked; absent on success |
at | When the webhook was sent, in UTC |
Verifying the signature
With a secret, the X-Dockflow-Signature header is sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with the secret. To verify it:
- Read the body as bytes, before any JSON parsing.
- Compute the HMAC-SHA256 of those bytes with the same secret.
- Compare it with the header in constant time, and reject the request when they differ.
import { createServer } from 'node:http';
import { createHmac, timingSafeEqual } from 'node:crypto';
const secret = process.env.WEBHOOK_SECRET;
function valid(body, header) {
const expected = 'sha256=' + createHmac('sha256', secret).update(body).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(header ?? '');
return a.length === b.length && timingSafeEqual(a, b);
}
createServer((req, res) => {
const chunks = [];
req.on('data', (c) => chunks.push(c));
req.on('end', () => {
const body = Buffer.concat(chunks);
if (!valid(body, req.headers['x-dockflow-signature'])) {
res.writeHead(401).end();
return;
}
const p = JSON.parse(body);
console.log(`${p.app} ${p.env}: ${p.op} ${p.status} (${p.version})`);
res.writeHead(204).end();
});
}).listen(8000);The payload has no unique delivery ID. To ignore replays, check that at is recent, or keep the run IDs you have already processed.
Failures
Each webhook is called once per run, one after the other.
A webhook that cannot be reached, times out, or answers with a status of 300 or more is reported as a warning (notify.failed in --json output). It is not retried, and it never changes the outcome or the exit code of the run.
Redirects are not followed, so the payload, its signature and your headers reach only the URL you wrote. A webhook that answers with a redirect fails, and the warning names where it pointed: write that URL in url.