Ingénieur Workflow n8n
Quand déclencher
- Éditer ou construire du JSON de workflow n8n
- Déboguer n8n : "le nœud ne s'exécute pas", "élément apparié indisponible", "les données statiques ne persistent pas"
- Opérations en masse (renommer, relancer, supprimer) via l'API REST n8n
- Patterns de nœuds Telegram, Gmail, HTTP Request, Code, Switch, Merge
- Comportement des déclencheurs planifiés / fuseau horaire / cron dans n8n
- Comportement de l'interface utilisateur n8n v2.x (Publier/Dépublier, Archiver)
Règles strictes
- Auditer avant d'éditer. Lire le JSON complet du workflow → présenter les résultats → confirmer → appliquer en une seule passe. Ne jamais patcher à l'aveugle.
- Tester via un déclencheur planifié. L'exécution manuelle réinitialise
$workflow.staticData. La preuve de déduplication nécessite une vraie exécution cron. - Vérifier le résultat réel. Consulter le journal d'exécution et l'état final (le message est arrivé, la ligne existe). Un nœud qui renvoie
ok: trueest un bon signe, pas une preuve. Le décalage de synchronisation de l'interface utilisateur trompe les observateurs. - Faire correspondre le mode du nœud Code à la signature de retour. Un mauvais mode + un mauvais retour = un traitement partiel silencieux.
Référence rapide – les bugs les plus courants
| Symptôme | Cause racine | Correctif |
|---|---|---|
La requête HTTP vers localhost:4000 renvoie un refus/timeout | n8n en mode pont Docker | Utiliser la passerelle hôte : souvent 172.17.0.1 sur le pont par défaut (vérifier avec docker network inspect bridge), ou mapper host.docker.internal:host-gateway dans extra_hosts |
| Le message Telegram n'arrive jamais, aucune erreur | Délai de résolution DNS IPv6 | NODE_OPTIONS=--dns-result-order=ipv4first dans docker-compose |
new Date() renvoie UTC malgré cron en EST | Conteneur manquant la variable d'env TZ | Définir à la fois TZ ET GENERIC_TIMEZONE sur votre fuseau horaire |
parse_mode: Markdown échoue silencieusement ou déforme le message | * ou _ non appariés dans la sortie IA | Utiliser parse_mode: HTML + assainir avant l'envoi |
| Erreur 401 sur le nœud HTTP Request LiteLLM | En-tête d'authentification manquant | Ajouter Authorization: Bearer {{ $env.LITELLM_API_KEY }} |
ECONNABORTED: timeout of 15000ms sur un appel Claude | Timeout par défaut de 15s | Définir options.timeout: 90000 |
PUT vers /api/v1/workflows/:id retourne 400 | Champs supplémentaires dans le corps | Réduire à : name, nodes, connections, settings, staticData uniquement |
| Le renommage d'un nœud casse silencieusement le workflow | Les références de connexion pointent vers l'ancien nom | Remplacer_tout l'ancien nom dans les clés nodes[].name ET connections{} |
| Élément apparié indisponible après une agrégation Code | La lignée d'élément N→1→M est perdue | Définir executeOnce: true, itérer $('UpstreamNode').all() explicitement |
| La déduplication des données statiques "ne fonctionne pas" lors d'une exécution manuelle | L'exécution manuelle réinitialise les données statiques | Tester via un cron planifié uniquement |
Nœud Code – les règles mode/retour
Chaque nœud Code a un paramètre mode. Une inadéquation = bugs silencieux.
| Mode | Pattern d'accès | Retour |
|---|---|---|
runOnceForAllItems (par défaut) | const items = $input.all(); | return items.map(...) — doit être un tableau |
runOnceForEachItem | $json est l'élément courant | return { json: ... } — un seul objet, PAS un tableau |
Symptômes d'inadéquation :
runOnceForAllItems+ accès à$json→ seul le premier élément est traité silencieusementrunOnceForEachItem+ renvoi d'un tableau → erreur de validation"A 'json' property isn't an object"
Pattern de déduplication (état inter-exécutions)
const store = $getWorkflowStaticData('global');
store.jobIds = store.jobIds || [];
const newItems = items.filter(item => !store.jobIds.includes(item.json.id));
// Persister les nouveaux IDs, limiter à 500 pour éviter une croissance incontrôlée
store.jobIds = [...store.jobIds, ...newItems.map(i => i.json.id)].slice(-500);
return newItems;
Critique : staticData persiste uniquement à travers les exécutions planifiées. L'exécution manuelle le réinitialise. Vérifiez la déduplication en observant deux exécutions planifiées consécutives, pas en cliquant deux fois sur Exécuter.
Nettoyage du cache par exécution
// Au début du premier nœud Code — nettoyer les caches transitoires mais préserver l'état inter-exécutions
const store = $getWorkflowStaticData('global');
for (const k of Object.keys(store)) {
if (k.startsWith('cjob_') || k.startsWith('cprep_') || k === 'pendingJobs') {
delete store[k];
}
}
// NE JAMAIS supprimer jobIds — la déduplication inter-exécutions en dépend
Pattern d'observabilité du pipeline
Définir des compteurs à chaque nœud Code majeur. Lire à l'étape de formatage. Ajouter au message.
// Étape 1 — Construire les Tickers
const store = $getWorkflowStaticData('global');
store.pipeline = { ticker_count: tickers.length, fixed_count: fixed.length };
// Étape 2 — Calculer les Signaux
store.pipeline = store.pipeline || {};
store.pipeline.signal_count = results.length;
// Final — Formater le message
const pl = store.pipeline || {};
const pf = (n) => (n === undefined || n === null) ? '?' : String(n);
const pipelineBlock =
'\n\n📊 Pipeline\n' +
'Construire Tickers : ' + pf(pl.ticker_count) + '\n' +
'Signaux : ' + pf(pl.signal_count);
Problème d'élément apparié + solution de contournement
Symptôme : Après un nœud d'agrégation (N éléments → 1 prompt → 1 réponse LLM → divisé en M éléments), en aval $('UpstreamNode').item.json génère l'erreur Paired item data for item from node 'X' is unavailable.
Solutions de contournement (par ordre de préférence) :
-
Définir
executeOnce: truesur le nœud Code en aval, itérer explicitement :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; -
Passer les IDs via les données statiques ; les rechercher explicitement en aval.
-
pairedItem.sourceOverwrite— fragile, dernier recours.
Patterns Telegram
Mode HTML – seulement 7 balises autorisées
<b>, <i>, <u>, <s>, <a>, <code>, <pre> — toute autre chose génère une erreur 400 Bad Request: Unsupported start tag.
Triple couche de défense pour le contenu généré par IA
- Prompt : indiquer explicitement à l'IA quelles balises sont autorisées
- Assainir : supprimer les balises non prises en charge dans le code avant l'envoi
- Échapper : échapper HTML le contenu utilisateur/IA (
&→&,<→<,>→>) avant de l'intégrer
Supprimer le pied de page d'attribution n8n
"additionalFields": {
"parse_mode": "HTML",
"appendAttribution": false,
"disable_web_page_preview": true
}
Jeton dans .env vs identifiant
Préférer le motif variable d'env + requête HTTP plutôt que le nœud Telegram intégré + identifiant :
- Portable lors des migrations
- Plus facile à faire pivoter (
nano .env+ redémarrage du conteneur) - Pas de risque de verrouillage par clé de chiffrement de base de données
- Motif :
POST https://api.telegram.org/bot{{ $env.BOT_TOKEN }}/sendMessage - Compromis : le jeton se trouve dans l'URL de la requête, il peut donc apparaître dans les données d'exécution, les logs proxy et les messages d'erreur. Désactiver la sauvegarde des données d'exécution pour ce workflow, ou utiliser l'identifiant Telegram intégré si d'autres personnes peuvent lire vos logs.
Patterns Gmail OAuth
| Problème | Résolution |
|---|---|
invalid_grant ... refresh token revoked | OAuth en mode "Test" expire ~7 jours. Ré-authentifier via l'interface utilisateur, ou publier l'application en Production. |
payload.headers vides (De/Sujet vierge) | Par défaut simple: true supprime les en-têtes. Définir options.simple: false. |
| Alertes de sécurité / reçus manquants | Ne pas exclure -category:promotions -category:social -category:updates — Gmail achemine 90% des e-mails pertinents là-bas. Utiliser uniquement is:unread newer_than:Nd. |
| Nécessité de marquer comme lu sur plusieurs comptes | Nécessite des identifiants par compte. Un seul identifiant Gmail ne peut pas opérer sur les IDs de messages d'un autre compte. |
Patterns API REST (opérations en masse)
Auth : En-tête X-N8N-API-KEY: <key>.
| Point de terminaison | Notes |
|---|---|
GET /api/v1/workflows | Renvoie tout (paginé). Filtrer côté client. |
GET /api/v1/workflows/:id | JSON complet du workflow. |
PUT /api/v1/workflows/:id | Strict : uniquement name, nodes, connections, settings, staticData. settings lui-même a une liste d'autorisation (voir ci-dessous). Supprimer tout le reste d'un aller-retour GET puis PUT. |
DELETE /api/v1/workflows/:id | Supprime réellement (l'interface utilisateur ne propose que l'archivage). Utiliser pour le nettoyage en masse. |
POST /api/v1/workflows/:id/activate | Identique à Publier dans l'interface utilisateur v2.x. |
Liste d'autorisation du champ settings (corps PUT)
executionOrder, timezone, saveExecutionProgress, saveDataErrorExecution, saveDataSuccessExecution, saveManualExecutions, callerPolicy, callerIds, errorWorkflow, executionTimeout
Le type du nœud est immuable après la création
Un PUT qui modifie node.type renvoie 200 mais le type reste le même. Pour convertir (par ex. telegram → httpRequest) : remplacer l'intégralité de l'objet nœud (nouvel UUID pour id, même name). Les connexions sont acheminées par nom, donc un remplacement préservant le nom est sûr.
Spécificités de l'interface utilisateur n8n v2.x
| Ancien (v1.x) | Nouveau (v2.x) |
|---|---|
| Bascule Activer | Publier (bouton vert, en haut à droite) / Dépublier |
| Supprimer le workflow | Archiver uniquement (utiliser l'API REST pour une vraie suppression) |
Nœud HTTP Request – indispensables pour les appels LLM
{
"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:'<votre-alias-modele>',messages:[{role:'user',content:$json.prompt}],temperature:0.3}) }}",
"options": {
"timeout": 90000,
"retry": { "tries": 2, "waitBetweenTries": 5000 }
}
}
Le conteneur n8n n'a pas curl
Pour les tests HTTP dans le conteneur, utiliser le module intégré https de Node via docker exec :
docker exec n8n node -e "const https=require('https'); /* requête */"
Pré-vérification SSH (avant tout heredoc sur VPS)
ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=10 <utilisateur>@<hôte>
# Vérifier l'invite : [<utilisateur>@<hôte> ...]$
# Alors SEULEMENT coller le heredoc
Coller un heredoc bash dans PowerShell après une connexion SSH = centaines d'erreurs d'analyse.
Pattern de suite d'évaluation (logique uniquement)
Les tests avec entrées simulées via la fonctionnalité Évaluations de n8n détectent les régressions logiques sans consommer de crédits API. Réexécuter AVANT ET APRÈS toute modification de la logique de filtrage, des listes de mots-clés ou des règles de validation.
Anti-patterns
| Anti-pattern | Que faire à la place |
|---|---|
| "Exécute-le à nouveau, vois si ça marche" | Lire d'abord le journal d'exécution de bout en bout |
| Patcher un nœud à la fois sur plusieurs allers-retours | Auditer le fichier complet → script de patch groupé → vérifier en une seule passe |
Utiliser parse_mode: Markdown pour la sortie IA | parse_mode: HTML + assainisseur |
| Coder en dur les clés API / ID de chat dans le JSON du workflow | Référencer {{ $env.KEY }}, stocker dans <fichier-secret> (chmod 600) |
| Renommer des nœuds sans mettre à jour les connexions | remplacer_tout sur le nom du nœud dans les clés nodes[].name et connections{} |
| Exécution manuelle pour tester la déduplication | Attendre le déclencheur planifié |
| Timeout par défaut de httpRequest pour les appels LLM | Définir 90000ms minimum |
Mélanger runOnceForAllItems et $json | Faire correspondre le mode au pattern d'accès |