n8n Error Workflows, die wirklich warnen: Deduplizierung, hängende Sperren und die Freitagsmail
Teil 6 von 6 der Serie Die Follow-up-Maschine
- Überblick, Angebote nachfassen: eine Follow-up-Maschine, die sich meldet, wenn sie ausfällt
- Architektur, n8n-Workflow-Beispiel aus dem Echtbetrieb: eine Architektur, die Wiederholungen übersteht
- Registrieren, EspoCRM-Webhooks in n8n: Ereignisname, Signatur und eine Phase, die es nicht gab
- Auswerten, Ein stündlicher n8n-Workflow, der sich nicht selbst überholt: Sperren, Merge-Nodes und leere Läufe
- Anreichern, KI-Zusammenfassungen aus CRM-E-Mails mit n8n und Mistral: Prompt, Parser und das Komma
- Warnungen, n8n Error Workflows, die wirklich warnen: Deduplizierung, hängende Sperren und die Freitagsmail
Alle 6 Teile
- Überblick, Angebote nachfassen: eine Follow-up-Maschine, die sich meldet, wenn sie ausfällt
- Architektur, n8n-Workflow-Beispiel aus dem Echtbetrieb: eine Architektur, die Wiederholungen übersteht
- Registrieren, EspoCRM-Webhooks in n8n: Ereignisname, Signatur und eine Phase, die es nicht gab
- Auswerten, Ein stündlicher n8n-Workflow, der sich nicht selbst überholt: Sperren, Merge-Nodes und leere Läufe
- Anreichern, KI-Zusammenfassungen aus CRM-E-Mails mit n8n und Mistral: Prompt, Parser und das Komma
- Warnungen, n8n Error Workflows, die wirklich warnen: Deduplizierung, hängende Sperren und die Freitagsmail
Das ist der letzte Teil der Serie zur Follow-up-Maschine. Die Teile 3 bis 5 behandeln die Workflows, die die eigentliche Verarbeitung übernehmen. Dieser Teil behandelt die zwei, die mir sagen, ob sie tatsächlich ausgeführt wird: „Followup 0: Alerts“ und „Followup 3: Report“.
Beide folgen der Regel vom Anfang des Projekts: Ein System, das still stehen bleibt, ist schlimmer als keines, weil man ihm weiter vertraut.
Ein inaktiver Error Workflow hat auf n8n 2.31.7 nicht ausgelöst
In n8n kann ein Workflow in seinen Einstellungen einen Error Workflow angeben. Scheitert eine Ausführung, startet n8n diesen Error Workflow mit den Details. Die vier übrigen Workflows verweisen alle auf „Followup 0: Alerts“.
Der erste echte Fehler war ein geplanter Lauf von Evaluate am 4. August. Der Error Workflow war korrekt eingetragen. Er erzeugte keine einzige Ausführung.
n8n Error Workflows. Laut Dokumentation von n8n muss ein Workflow mit Error Trigger nicht aktiviert werden. In meiner Version, 2.31.7, wurde ein inaktiver Error-Workflow nicht gestartet.
Für neuere Versionen habe ich das nicht geprüft. Das Aktivieren hat es behoben, und es hat keinen Nachteil: Ein Error Trigger hat weder Zeitplan noch Webhook, ein aktiver Error Workflow tut also nichts, bis etwas scheitert.
Um genau diese Art Fehler geht es in diesem Teil.
Der Alarm war installiert, eingerichtet und stumm.
Einmal warnen, nicht jede Stunde
Der Alert-Workflow ist kurz:
On Error (Error Trigger)
→ Log + dedup (Postgres)
├─→ Should alert? ──ja───→ Dry run? ──nein──→ Send alert (Outlook)
│ └─nein─→ No alert (deduped)
└─→ Release stale claim (Postgres)
Die Deduplizierung steckt in derselben Abfrage, die den Fehler protokolliert:
insert into run_log (workflow, run_id, started_at, finished_at, status, error_count, notes)
values (
$1::jsonb ->> 'workflow', gen_random_uuid(), now(), now(), 'failed', 1,
jsonb_build_object(
'error', $1::jsonb ->> 'error',
'alerted', not exists (
select 1 from run_log
where workflow = $1::jsonb ->> 'workflow'
and status = 'failed'
and (notes ->> 'alerted')::boolean
and started_at > now() - interval '6 hours'
)
)
)
returning (notes ->> 'alerted')::boolean as should_alert;
Jeder Fehler wird festgehalten. Eine E-Mail geht nur raus, wenn für denselben Workflow in den letzten sechs Stunden keine Warnung verschickt wurde.
Der Grund ist praktisch. Evaluate läuft 13-mal am Tag, Enrich noch einmal 13-mal. Ein abgelaufener CRM-Zugang lässt jeden dieser Läufe scheitern. Ohne Deduplizierung wären das bis zu 26 Warnungen am Tag, und die vernünftige menschliche Reaktion darauf ist ein Mailfilter. Danach landet auch der nächste echte Fehler im Filter.
Der Parameter ist ein einziges JSON-Objekt, aus dem in Teil 5 beschriebenen Grund: Fehlermeldungen enthalten Kommas, und der Postgres-Node von n8n zerlegt seine Parameterkette daran.
Die Sperre eines abgestürzten Laufs lösen
Teil 2 beschreibt die Laufsperre: ein Eintrag in run_log mit Status running, der andere Läufe 55 Minuten lang blockiert. Ein normal beendeter Lauf gibt sie frei. Ein abgestürzter Lauf erreicht seinen letzten Node nie, also bleibt seine Sperre.
Die Folge: Jeder Lauf desselben Workflows, der innerhalb der nächsten 55 Minuten startete, erkannte die Sperre und beendete sich ohne weitere Verarbeitung. Beim nächsten geplanten Lauf 60 Minuten später war die Sperre gerade abgelaufen. Bei einem manuellen Neustart direkt nach der Fehlerbehebung nicht: Der gescheiterte Lauf hielt seine Sperre weiter, der sofortige zweite Versuch wurde deshalb übersprungen, und erst der dritte lief wieder. Beim Abnahmetest, mit Läufen, die von Hand im Minutenabstand gestartet wurden, kostete ein Fehler so regelmäßig einen zusätzlichen Versuch.
Der Error Workflow weiß, welcher Workflow gescheitert ist, also löst er jetzt die Sperre:
update run_log
set status = 'failed',
finished_at = now(),
notes = coalesce(notes, '{}'::jsonb) || jsonb_build_object('released_by', 'alerts')
where workflow = $1::jsonb ->> 'workflow'
and coalesce($1::jsonb ->> 'workflow', '') <> ''
and status = 'running'
returning id;
Dieser Zweig läuft parallel zum Warnzweig, also auch dann, wenn die Deduplizierung die E-Mail unterdrückt. Er ist sicher, weil n8n eine gescheiterte Ausführung beendet, bevor der Error Workflow startet: Es wird nichts freigegeben, was noch läuft. Das 55-Minuten-Fenster bleibt als Absicherung für den einen Fall, den der Error Workflow nicht sieht: n8n selbst stirbt mitten im Lauf.
Der Bericht, der immer kommt
„Followup 3: Report“ hat zwei Zeitpläne an einem Auslöser:
| Zeitplan | Inhalt | Versand auch ohne Einträge? |
|---|---|---|
| Dienstag bis Freitag, 8:45 | Heute fällige Nachfass-Termine mit Kontext und nächstem Schritt | Nein |
| Freitag, 15:45 | Deals mit Fortschritt, festgefahrene Deals, nächste Woche, diese Woche geschlossen, dazu eine Statuszeile | Ja |
Die Freitagsmail wird auch dann verschickt, wenn nichts passiert ist. Eine leere Übersicht ist keine überflüssige Nachricht. Sie ist der Beweis, dass die Maschine lebt, und sie kommt, ohne dass ich danach suchen muss. Vorausgesetzt, der Lauf selbst kommt durch: Wie der Abschnitt weiter unten zeigt, verschickt er bei einem Datenbankausfall gar nichts.
Ihre Fußzeile ist die Statusprüfung:
export function healthFooter(h: Health): { line: string; critical: boolean } {
if (!h.lastOkWf2At || h.now.getTime() - h.lastOkWf2At.getTime() > 24 * 3_600_000) {
return {
critical: true,
line: `⚠ WF2 has not run successfully in the last 24 hours (last ok: ${h.lastOkWf2At?.toISOString() ?? 'never'}). ${h.openWatches} open watches.`,
};
}
return {
critical: false,
line: `Health: last WF2 ok ${h.lastOkWf2At.toISOString()} · ${h.partialRunsThisWeek} partial run(s) this week · ${h.openWatches} open watches`,
};
}
Steht critical auf true, wandert diese Zeile vom Ende der E-Mail an den Anfang.
Einen unvollständigen Bericht verschickt der Workflow außerdem nie. Scheitert eine Abfrage, scheitert der Lauf, und der Error Workflow übernimmt. Ein Bericht, dem still ein Abschnitt fehlt, untergräbt das Vertrauen in den Bericht, und dann erfüllt er seinen Zweck nicht mehr.
Der ruhige Morgen, der wie ein toter Workflow aussah
Der tägliche Zweig hatte seine eigene Variante des Leerlauf-Fehlers aus Teil 4. An einem Morgen ohne fällige Termine gab der Compose-Schritt null Einträge aus. Die Sende-Nodes wurden übersprungen, das war richtig. Aber der Merge-Node vor „Log run“ wartete auf Daten aus dem Versandzweig und wurde deshalb nicht weiter ausgeführt. Es entstand kein Eintrag in run_log.
Ein gesunder, ruhiger Morgen und ein ausgefallener Workflow sahen in run_log also gleich aus. Genau diese beiden Zustände sollen die Statusprüfungen auseinanderhalten.
Die Lösung ist dasselbe Muster wie in Teil 4. Compose gibt statt nichts einen Eintrag { skip: true } aus, und ein IF-Node „Anything to send?“ leitet ihn an den Sende-Nodes vorbei direkt zum Merge-Node. „Log run“ zählt die versendeten Einträge dann mit einer Absicherung, weil der Verweis auf einen nicht ausgeführten Node einen Fehler wirft. Die Absicherung sieht so aus:
{{ $('Send').isExecuted ? $items('Send').length : 0 }}
Was der Warnpfad trotzdem nicht melden kann
Sehen Sie sich den Alert-Workflow noch einmal an. Als Erstes schreibt er nach Postgres. Beide Postgres-Nodes darin nutzen dieselbe Datenbank und denselben Zugang wie jeder andere Workflow der Maschine. Keiner hat einen Fehlerausgang. Der Alert-Workflow selbst hat keinen Error Workflow.
Ist Postgres also ausgefallen oder funktionieren die Zugangsdaten nicht mehr, läuft die Kette so: Evaluate scheitert am ersten Datenbank-Node, der Error Workflow startet, „Log + dedup“ scheitert nach drei Versuchen, also einem Aufruf und zwei Wiederholungen, und der Error Workflow bricht ab. „Send alert“ läuft nie. Der Freitagsbericht braucht ebenfalls Postgres und schickt dann absichtlich gar nichts statt eines unvollständigen Berichts.
Die Lücke. Der eine Ausfall, den die Architektur für fatal erklärt, ist genau der, den der Warnpfad nicht melden kann. Das einzige verbleibende Signal ist die ausbleibende Freitagsmail.
Gefunden habe ich das beim Schreiben dieses Beitrags, durch Lesen des Workflows, nicht durch einen Ausfall. Die Lösung wäre klein: „Log + dedup“ bekommt einen Fehlerausgang, der direkt zu „Send alert“ führt, mit einem Betreff, der sagt, dass die Deduplizierung nicht verfügbar ist. Damit fällt die Begrenzung auf eine Warnung alle sechs Stunden während eines Datenbankausfalls weg, denn sie steckt genau in der Abfrage, die dann scheitert. Dafür hat der Warnpfad überhaupt eine Chance, etwas zu verschicken. Für den Ausfall, der alles anhält, halte ich das für den richtigen Tausch. Gebaut ist er zum Zeitpunkt dieses Beitrags nicht.
Die allgemeine Lehre ist die, auf die diese Serie immer wieder hinausläuft.
Ein Alarm, der von dem abhängt, was er überwacht, kann dessen Ausfall unter Umständen nicht melden.
Kleinere Dinge, die echte Zeit gekostet haben
- Die Workflow-Namen sind Teil der Funktionslogik. Betreffzeilen der Warnungen und Einträge in
run_logwerden aus dem Anzeigenamen abgeleitet, also „Followup 2: Evaluate“ und so weiter. Wer einen Workflow umbenennt, bricht still die Freigabe der Sperre, die die Nummer aus diesem Namen liest. Eine Anmerkung zur Schreibweise: Im laufenden System trennt ein langer Gedankenstrich die Nummer vom Wort, in dieser Serie steht der Lesbarkeit zuliebe überall ein Doppelpunkt. - Node-IDs müssen echte UUIDs sein. Die ersten Workflows hatten kurze, selbst vergebene Node-IDs wie
w4. Der n8n-Editor findet einen Node aus einem Link über den Anfang seiner ID, also landete ein Link aufw4beiw401. Jeder Node hat jetzt eine ID auscrypto.randomUUID(). - Die Workflows auch direkt in n8n dokumentieren. Alle fünf Workflows hatten keine Beschreibung und keine Notizen, also zeigte der Editor fünf Namen und Arbeitsflächen mit teils über 40 Nodes, ohne ein Wort zum Zweck. Jeder hat jetzt eine Beschreibung und eine Notiz „Purpose“, erzeugt aus derselben Quelle wie die schriftliche Dokumentation.
- Erzeugte Dokumentation findet veraltete Einstellungen. Der Generator für diese Beschreibungen hat zwei übrig gebliebene Test-Zeitpläne in den gespeicherten Workflow-Dateien gefunden, die im Live-System schon rückgängig gemacht waren.
Was man aus dieser Serie mitnehmen kann
- Vor dem Bauen entscheiden, welches System die maßgeblichen Daten liefert.
- Jeden Workflow so bauen, dass er zweimal laufen darf.
- Den Lauf testen, in dem nichts passiert.
- Mit Daten testen, die wie echte Daten aussehen.
- Die Maschine regelmäßig beweisen lassen, dass sie lebt, auch wenn sie nichts zu sagen hat.
- Prüfen, dass der Alarm nicht von dem abhängt, was er überwacht.
Die Serie beginnt mit dem Überblick darüber, was die Maschine tut. Wenn in Ihrem Betrieb ein Ablauf auf dieselbe Weise liegen bleibt, erzählen Sie mir davon.