deadman: a dead-man's switch for n8n

Your automation ran every night for four months. Then the scheduler stopped queueing it, or the credential behind it lapsed, and it produced nothing for thirty hours, and nothing on your instance told you so. An error workflow only fires when something ran and failed. Nothing ran, so there was no error to catch. deadman is a small outside observer that reads the absence.

It reads your n8n instance's own execution history over its public REST API and answers one question per workflow: did anything start when it should have? One program file, Python 3.11 or newer, standard library only. No packages, no daemon, no database, nothing installed inside n8n. It runs from cron and exits. It is read only: every call it makes is a GET, and it never starts, stops, retries or edits an execution.

The three things it can tell you apart

classwhat happenedwhy n8n's own error workflow cannot see it
SILENCEa workflow due on a schedule produced no execution inside every + gracenothing ran, so there is no failed execution to alert on. This is the thirty-hours-nobody-noticed case.
ERRORa new failed execution appeared (n8n statuses error, crashed)it can see this one. deadman reports it without you wiring an error workflow into every board, and it dedupes so one failure is reported once.
PROBE-FAULTthe n8n API did not answer, or answered 401 or 403, or answered garbagethis is not about your workflows at all. It is what a rotated API key or a lapsed Cloud trial looks like.

The order is deliberate. PROBE-FAULT is decided first and it suspends every workflow verdict. If deadman cannot read your instance it says "could not read n8n" and exits 2. It does not report forty seven workflows as dead because its own key expired. Reporting someone else's outage as your workflows' death is the most expensive lie a monitoring tool can tell.

Proof, and how to repeat every number on this page

Measured on Python 3.12.3 with pytest 9.1.1, on 2026-11-14, from the copy served at /src/ on this site.

checkthe exact commandwhat it printed
the rule suite, under pytest python3 -m pytest test_deadman.py -q 51 passed in 0.13s
the same suite through its own runner, which counts assertions rather than tests python3 test_deadman.py deadman rules: 51 passed, 0 failed, 270 assertions
the one-command check: compiles, runs the suite, runs the shipped demo, checks idempotence, checks a bad config ./selfcheck.sh RESULT PASS (6/6 steps)

On a machine whose login session carries a display, a bare test run is refused by this project's own guard so a test can never reach someone's screen. The run above is the same command inside that box.

What the shipped demo prints, with no n8n instance and no API key

$ python3 deadman.py --config config.example.toml --simulate demo_fixture.json \
    --now 2026-11-13T12:00:00Z --state /tmp/dm_state.json
2026-11-13T12:00:00Z ALERT error:PLACEHOLDER_WORKFLOW_ID 1 new failed execution(s): 4412 (statuses error)
2026-11-13T12:00:00Z ALERT silence:PLACEHOLDER_SECOND_WORKFLOW_ID no execution in 1200s (expected every 900s + 300s grace); last seen: id 4401 at 2026-11-13T09:00:00Z (status success)

Two failure classes, one line each, exit code 1. Run it twice and the second run sends nothing new: an alert fires once when it opens, re-fires only after cooldown_s, and clears with exactly one RECOVERED notice. State is one small JSON file written atomically, so killing the process mid-run cannot corrupt it.

Exit codes, which are the whole contract under a monitor

codemeaning
0every workflow due started, or the probe fault is already known and reported
1at least one alert is open (silence or a new failed execution)
2probe fault: the instance could not be read, or the config is unusable. Workflow verdicts are suspended, not guessed

The source, in the open

What it does not do