n8n Workflow Engineer
Wann auslösen
- Bearbeiten oder Erstellen von n8n-Workflow-JSON
- Debuggen von n8n: "Knoten wird nicht ausgeführt", "Paired-Item nicht verfügbar", "Statische Daten werden nicht dauerhaft gespeichert"
- Bulk-Operationen (umbenennen, wiederholen, löschen) über die n8n-REST-API
- Telegram-, Gmail-, HTTP-Request-, Code-, Switch-, Merge-Knoten-Muster
- Geplanter Trigger / Zeitzone / Cron-Verhalten in n8n
- n8n v2.x UI-Verhalten (Veröffentlichen/Veröffentlichung rückgängig machen, Archivieren)
Harte Regeln
- Prüfen vor dem Bearbeiten. Lesen Sie das vollständige Workflow-JSON → präsentieren Sie die Ergebnisse → bestätigen Sie → wenden Sie es in einem Durchgang an. Niemals blind patchen.
- Testen über geplanten Trigger. Manuelles "Ausführen" setzt
$workflow.staticDatazurück. Der Deduplizierungsnachweis erfordert einen echten Cron-Durchlauf. - Überprüfen Sie das tatsächliche Ergebnis. Prüfen Sie das Ausführungsprotokoll und den Endzustand (die Nachricht ist angekommen, die Zeile existiert). Ein Knoten, der
ok: truezurückgibt, ist ein gutes Zeichen, kein Beweis. Die UI-Synchronisationsverzögerung täuscht Beobachter. - Passen Sie den Code-Knoten-Modus an die Rückgabesignatur an. Falscher Modus + falsche Rückgabe = stille Teilverarbeitung.
Kurzreferenz — die häufigsten Fehler
| Symptom | Ursache | Lösung |
|---|---|---|
HTTP-Request an localhost:4000 gibt "refused/timeout" zurück | n8n im Docker-Bridge-Modus | Verwenden Sie das Host-Gateway: oft 172.17.0.1 auf der Standard-Bridge (prüfen mit docker network inspect bridge), oder mappen Sie host.docker.internal:host-gateway in extra_hosts |
| Telegram-Nachricht kommt nie an, kein Fehler | IPv6-DNS-Timeout | NODE_OPTIONS=--dns-result-order=ipv4first in docker-compose |
new Date() gibt UTC zurück, obwohl Cron in EST | Container fehlt die Umgebungsvariable TZ | Setzen Sie sowohl TZ als auch GENERIC_TIMEZONE auf Ihre Zeitzone |
parse_mode: Markdown schlägt still fehl oder verzerrt | Ungepaarte * oder _ in KI-Ausgabe | Verwenden Sie parse_mode: HTML + bereinigen Sie vor dem Senden |
| 401 auf LiteLLM HTTP-Request-Knoten | Fehlender Auth-Header | Fügen Sie Authorization: Bearer {{ $env.LITELLM_API_KEY }} hinzu |
ECONNABORTED: timeout of 15000ms bei Claude-Aufruf | Standard-Timeout 15s | Setzen Sie options.timeout: 90000 |
PUT auf /api/v1/workflows/:id gibt 400 zurück | Zusätzliche Felder im Body | Reduzieren Sie auf: nur name, nodes, connections, settings, staticData |
| Knotenumbenennung unterbricht Workflow stillschweigend | Verbindungsreferenzen beziehen sich auf den alten Namen | Ersetzen Sie (replace_all) den alten Namen sowohl in nodes[].name als auch in connections{}-Schlüsseln |
| Paired-Item nach Code-Aggregation nicht verfügbar | N→1→M hat Item-Abstammung verloren | Setzen Sie executeOnce: true, iterieren Sie explizit mit $('UpstreamNode').all() |
| Deduplizierung mit statischen Daten "funktioniert nicht" bei manuellem Lauf | Manuelles Ausführen setzt statische Daten zurück | Nur über geplanten Cron testen |
Code-Knoten — die Modus-/Rückgaberegeln
Jeder Code-Knoten hat einen mode-Parameter. Nichtübereinstimmung = stille Fehler.
| Modus | Zugriffsmuster | Rückgabe |
|---|---|---|
runOnceForAllItems (Standard) | const items = $input.all(); | return items.map(...) — muss Array sein |
runOnceForEachItem | $json ist das aktuelle Item | return { json: ... } — einzelnes Objekt, KEIN Array |
Symptome einer Nichtübereinstimmung:
runOnceForAllItems+ Zugriff auf$json→ nur das erste Item wird stillschweigend verarbeitetrunOnceForEachItem+ Rückgabe eines Arrays → Validierungsfehler"Eine 'json'-Eigenschaft ist kein Objekt"
Deduplizierungsmuster (workflowübergreifender Zustand)
const store = $getWorkflowStaticData('global');
store.jobIds = store.jobIds || [];
const newItems = items.filter(item => !store.jobIds.includes(item.json.id));
// Neue IDs speichern, auf letzte 500 begrenzen, um unbegrenztes Wachstum zu vermeiden
store.jobIds = [...store.jobIds, ...newItems.map(i => i.json.id)].slice(-500);
return newItems;
Wichtig: staticData bleibt nur über geplante Läufe hinweg bestehen. Manuelles Ausführen setzt es zurück. Überprüfen Sie die Deduplizierung, indem Sie zwei aufeinanderfolgende geplante Läufe beobachten, nicht durch zweimaliges Klicken auf "Ausführen".
Bereinigung des Laufzeit-Caches
// Am Anfang des ersten Code-Knotens — flüchtige Caches bereinigen, aber workflowübergreifenden Zustand erhalten
const store = $getWorkflowStaticData('global');
for (const k of Object.keys(store)) {
if (k.startsWith('cjob_') || k.startsWith('cprep_') || k === 'pendingJobs') {
delete store[k];
}
}
// NIEMALS jobIds löschen — workflowübergreifende Deduplizierung ist darauf angewiesen
Pipeline-Beobachtbarkeitsmuster
Setzen Sie Zähler an jedem wichtigen Code-Knoten. Lesen Sie sie im Formatierungsschritt. Hängen Sie sie an die Nachricht an.
// Stufe 1 — Ticker erstellen
const store = $getWorkflowStaticData('global');
store.pipeline = { ticker_count: tickers.length, fixed_count: fixed.length };
// Stufe 2 — Signale berechnen
store.pipeline = store.pipeline || {};
store.pipeline.signal_count = results.length;
// Final — Nachricht formatieren
const pl = store.pipeline || {};
const pf = (n) => (n === undefined || n === null) ? '?' : String(n);
const pipelineBlock =
'\n\n📊 Pipeline\n' +
'Ticker erstellen: ' + pf(pl.ticker_count) + '\n' +
'Signale: ' + pf(pl.signal_count);
Paired-Item-Problem + Workaround
Symptom: Nach einem Aggregationsknoten (N Items → 1 Prompt → 1 LLM-Antwort → Aufteilung in M Items) wirft der nachgelagerte $('UpstreamNode').item.json den Fehler "Paired item data for item from node 'X' is unavailable".
Workarounds (in der Reihenfolge der Präferenz):
-
Setzen Sie
executeOnce: trueauf den nachgelagerten Code-Knoten, iterieren Sie explizit:const upstream = $('UpstreamNode').all(); const outputs = []; for (let i = 0; i < parsed.length; i++) { outputs.push({ json: { ...parsed[i], original: upstream[i].json } }); } return outputs; -
Übergeben Sie IDs über statische Daten; suchen Sie sie downstream explizit nach.
-
pairedItem.sourceOverwrite— fragil, letzter Ausweg.
Telegram-Muster
HTML-Modus — nur 7 Tags erlaubt
<b>, <i>, <u>, <s>, <a>, <code>, <pre> — alles andere wirft 400 Bad Request: Unsupported start tag.
Drei-Schutzebenen für KI-generierte Inhalte
- Prompt: Sagen Sie der KI explizit, welche Tags erlaubt sind
- Bereinigen: Entfernen Sie nicht unterstützte Tags im Code vor dem Senden
- Escape: HTML-escaped Sie Benutzer-/KI-Inhalte (
&→&,<→<,>→>) vor dem Einbetten
n8n-Attribution-Fußzeile unterdrücken
"additionalFields": {
"parse_mode": "HTML",
"appendAttribution": false,
"disable_web_page_preview": true
}
Token-in-.env vs. Credential
Bevorzugen Sie das Umgebungsvariablen + HTTP-Request-Muster gegenüber dem integrierten Telegram-Knoten + Credential:
- Portierbar über Migrationen hinweg
- Einfacher zu rotieren (
nano .env+ Container-Neustart) - Kein DB-Verschlüsselungsschlüssel-Sperrrisiko
- Muster:
POST https://api.telegram.org/bot{{ $env.BOT_TOKEN }}/sendMessage - Nachteil: Das Token befindet sich in der Request-URL, kann also in Ausführungsdaten, Proxy-Logs und Fehlermeldungen auftauchen. Deaktivieren Sie das Speichern von Ausführungsdaten für diesen Workflow oder verwenden Sie das integrierte Telegram-Credential, wenn andere Ihre Logs lesen können.
Gmail-OAuth-Muster
| Problem | Lösung |
|---|---|
invalid_grant ... refresh token revoked | OAuth im "Test"-Modus läuft nach ~7 Tagen ab. Erneute Authentifizierung über die UI oder App in den Produktionsmodus versetzen. |
Leere payload.headers (Von/Betreff leer) | Standardmäßig entfernt simple: true Header. Setzen Sie options.simple: false. |
| Fehlende Sicherheitswarnungen / Quittungen | Schließen Sie -category:promotions -category:social -category:updates nicht aus — Gmail routet 90% der relevanten E-Mails dorthin. Verwenden Sie nur is:unread newer_than:Nd. |
| Als gelesen markieren über mehrere Konten hinweg | Pro Konto sind separate Credentials erforderlich. Ein Gmail-Credential kann nicht mit den Nachrichten-IDs eines anderen Kontos arbeiten. |
REST-API-Muster (Bulk-Operationen)
Auth: X-N8N-API-KEY: <key> Header.
| Endpunkt | Hinweise |
|---|---|
GET /api/v1/workflows | Gibt alle zurück (paginiert). Client-seitig filtern. |
GET /api/v1/workflows/:id | Vollständiges Workflow-JSON. |
PUT /api/v1/workflows/:id | Streng: nur name, nodes, connections, settings, staticData. settings selbst hat eine Whitelist (siehe unten). Entfernen Sie alles andere bei einem GET-dann-PUT-Roundtrip. |
DELETE /api/v1/workflows/:id | Löscht tatsächlich (die UI bietet nur Archivierung). Für Bulk-Bereinigung verwenden. |
POST /api/v1/workflows/:id/activate | Entspricht dem Veröffentlichen in der v2.x-UI. |
Whitelist für das settings-Feld (PUT-Body)
executionOrder, timezone, saveExecutionProgress, saveDataErrorExecution, saveDataSuccessExecution, saveManualExecutions, callerPolicy, callerIds, errorWorkflow, executionTimeout
Knoten-type ist nach der Erstellung unveränderlich
Ein PUT, das node.type ändert, gibt 200 zurück, aber der Typ bleibt gleich. Zum Konvertieren (z.B. telegram → httpRequest): Ersetzen Sie das gesamte Knotenobjekt (neue UUID für id, gleicher name). Verbindungen werden nach Name aufgelöst, daher ist die namenserhaltende Ersetzung sicher.
n8n v2.x UI-Besonderheiten
| Alt (v1.x) | Neu (v2.x) |
|---|---|
| Aktivieren-Umschalter | Veröffentlichen (grüner Button, oben rechts) / Veröffentlichung rückgängig machen |
| Workflow löschen | Nur archivieren (für echtes Löschen die REST-API verwenden) |
HTTP-Request-Knoten — Must-Haves für LLM-Aufrufe
{
"method": "POST",
"url": "http://172.17.0.1:4000/v1/chat/completions",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{"name": "Content-Type", "value": "application/json"},
{"name": "Authorization", "value": "=Bearer {{ $env.LITELLM_API_KEY }}"}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({model:'<your-model-alias>',messages:[{role:'user',content:$json.prompt}],temperature:0.3}) }}",
"options": {
"timeout": 90000,
"retry": { "tries": 2, "waitBetweenTries": 5000 }
}
}
n8n-Container hat kein curl
Für HTTP-Tests innerhalb des Containers verwenden Sie das integrierte https-Modul von Node über docker exec:
docker exec n8n node -e "const https=require('https'); /* request */"
SSH-Vorbereitung (vor jedem Heredoc auf VPS)
ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=10 <user>@<host>
# Prompt prüfen: [<user>@<host> ...]$
# Erst DANN das Heredoc einfügen
Das Einfügen eines Bash-Heredocs in PowerShell nach einem SSH-Drop = hunderte Parse-Fehler.
Eval-Suite-Muster (nur Logik)
Mock-Input-Tests über die n8n-Evaluierungs-Funktion erkennen logische Regressionen, ohne API-Guthaben zu verbrauchen. Führen Sie sie VOR und NACH jeder Änderung an Filterlogik, Schlüsselwortlisten oder Validierungsregeln erneut aus.
Anti-Pattern
| Anti-Pattern | Stattdessen tun |
|---|---|
| "Führen Sie es nochmal aus, sehen Sie, ob es funktioniert" | Lesen Sie zuerst das Ausführungsprotokoll von Anfang bis Ende |
| Einen Knoten nach dem anderen über viele Roundtrips patchen | Gesamte Datei prüfen → gebündeltes Patch-Skript → in einem Durchgang verifizieren |
parse_mode: Markdown für KI-Ausgabe verwenden | parse_mode: HTML + Bereiniger |
| API-Schlüssel / Chat-IDs im Workflow-JSON hartcodieren | Referenzieren Sie {{ $env.KEY }}, speichern Sie in <secret-file> (chmod 600) |
| Knoten umbenennen, ohne Verbindungen zu aktualisieren | replace_all auf den Knotennamen sowohl in nodes[].name als auch in connections{}-Schlüsseln |
| Manuelles Ausführen zum Testen der Deduplizierung | Auf geplanten Trigger warten |
| Standard-httpRequest-Timeout für LLM-Aufrufe | Minimum 90000ms setzen |
Mischen von runOnceForAllItems und $json | Modus an Zugriffsmuster anpassen |