Zum Inhalt springen

This page is available in English. Switch to English

Arbeit, die jede Woche wiederkommt? Muss kein Mensch machen. Mehr zu KI und Automatisierung

Automatisierung

n8n Error Workflows, die wirklich warnen: Deduplizierung, hängende Sperren und die Freitagsmail

Teil 6 von 6 der Serie Die Follow-up-Maschine

Eine breite polierte Metallscheibe mit feinen konzentrischen Ringen, die sich von der Mitte ausbreiten.

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:

ZeitplanInhaltVersand auch ohne Einträge?
Dienstag bis Freitag, 8:45Heute fällige Nachfass-Termine mit Kontext und nächstem SchrittNein
Freitag, 15:45Deals mit Fortschritt, festgefahrene Deals, nächste Woche, diese Woche geschlossen, dazu eine StatuszeileJa

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 Datenbank fällt aus. Der Alarm verstummt.D6 / FOLLOW-UP MACHINEDie Datenbank fällt aus. Der Alarm verstummt.Aktuelle Verkabelung bei Datenbankausfall. Gestrichelte Umleitung: nur vorgeschlagen. Watchful Loop: Alerts On ErrorError TriggerLog + dedupPostgres · FollowupShould alert?3 Versuche → Ausführung endet1 Aufruf + 2 WiederholungenRelease stale claimPostgres · Followupdieselbe Datenbankabhängigkeitbei diesem Fehler nicht erreichtjaneinDry run?No alert(deduped)janeinLog dry-run alertSend alertOutlookVORGESCHLAGEN / NICHT GEBAUTDie Warnung erreicht Outlook nicht.Beide Postgres-Nodes haben keinen Fehlerausgang.Alerts hat keinen eigenen errorWorkflow.Auch der Freitagsbericht braucht Postgres. Die ausbleibende E-Mail kann das einzige Signal sein.Die vorgeschlagene Umleitung umgeht die Deduplizierung per Datenbank; sie garantiert keine Zustellung.8 verarbeitende Nodes dargestellt. Der 9. JSON-Node ist die Notiz „Purpose“.
Fällt Postgres aus, endet der Error-Workflow an seinem eigenen Datenbankzugriff. Die gestrichelte Umleitung ist vorgeschlagen und gehört nicht zum aktuellen Workflow.

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_log werden 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 auf w4 bei w401. Jeder Node hat jetzt eine ID aus crypto.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

  1. Vor dem Bauen entscheiden, welches System die maßgeblichen Daten liefert.
  2. Jeden Workflow so bauen, dass er zweimal laufen darf.
  3. Den Lauf testen, in dem nichts passiert.
  4. Mit Daten testen, die wie echte Daten aussehen.
  5. Die Maschine regelmäßig beweisen lassen, dass sie lebt, auch wenn sie nichts zu sagen hat.
  6. 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.