En bref
Lorsqu'un workflow n8n demande à un LLM de répondre en JSON, le modèle renvoie parfois sa réponse entourée de balises markdown. Ces balises restent invisibles dans une interface de chat, mais elles rendent la chaîne illisible pour JSON.parse(), qui s'interrompt avec une erreur de type Unexpected token. L'approche la plus prudente combine une contrainte de format côté API (sortie structurée) et un nettoyage par code placé en filet de sécurité.[1][2]
Un workflow n8n appelle un modèle de langage, récupère sa réponse et la transmet au nœud suivant. Pendant des jours, tout se déroule normalement. Puis une exécution échoue sur une erreur de syntaxe, sans qu'aucun paramètre n'ait changé.
Ce scénario est fréquent dès qu'un LLM alimente un traitement automatisé. Il a été identifié lors des tests du node Miravig avec Gemini, puis corrigé. Cet article ouvre une série consacrée à la fiabilité et à la sécurité des workflows n8n qui appellent un LLM. Il explique le bug, la manière de le reconnaître et les corrections disponibles, de la plus simple à la plus robuste.
n8n est une plateforme d'automatisation : un workflow est une chaîne de nœuds, chacun recevant les données du précédent. Un LLM répond par défaut en texte libre, ce qui convient à un lecteur humain mais pas à un nœud qui doit lire des champs précis.
Prenons un exemple concret. Un workflow reçoit des e-mails de clients et doit en extraire le nom et le montant de la facture pour alimenter un CRM. Un texte libre comme « Le client Dupont réclame 1 200 euros » ne s'insère dans aucune colonne. Le même contenu en JSON, un format de données lisible par les machines, se range immédiatement :
{"nom": "Dupont", "montant": 1200}
Le nœud Code de n8n transforme la chaîne reçue en objet grâce à la fonction JSON.parse(). Cette fonction est stricte : elle accepte un JSON pur et refuse tout caractère supplémentaire.
L'erreur affichée varie selon la version de n8n et de Node.js. On rencontre le plus souvent SyntaxError: Unexpected token ou une variante mentionnant un caractère inattendu à la position 0 de la chaîne.
Ce message pointe le nœud Code, pas le nœud LLM. Le réflexe consiste alors à vérifier les identifiants API, le prompt ou les limites de requêtes, alors que la cause se trouve dans le format de la réponse reçue.
Certains modèles entourent leur JSON d'une « clôture » markdown, c'est-à-dire de trois accents graves suivis du mot json :
```json
{"nom": "Dupont", "montant": 1200}
```
Une comparaison aide à saisir la différence. JSON.parse() attend la lettre seule. Le modèle envoie la lettre dans une enveloppe. Un humain ouvre l'enveloppe sans y penser. Une fonction de lecture stricte, elle, rejette le courrier dès le premier caractère.
Le markdown est converti en bloc de code propre. L'enveloppe disparaît.
La chaîne brute est transmise telle quelle. Le premier caractère est un accent grave, JSON.parse() s'arrête.
Les fournisseurs ne documentent pas la raison exacte de ce comportement. L'explication la plus plausible tient à l'usage principal de ces modèles : produire des réponses lisibles dans une interface de chat, où le markdown est converti en bloc de code propre. Le réflexe de mise en forme se retrouve alors dans la réponse d'API.
Pour les fournisseurs, demander du JSON dans le prompt ne constitue pas une garantie. La documentation de Gemini indique que le modèle n'est pas assuré de renvoyer uniquement du JSON lorsque la demande passe par le texte de la consigne.[1]
Trois caractéristiques le rendent difficile à repérer.
Il n'est pas systématique. Selon le modèle, sa version et la formulation de la consigne, la même requête peut produire du JSON pur une fois et une réponse enveloppée la fois suivante. Quelques exécutions de test réussies ne prouvent donc rien.
Personne ne relit la réponse brute. Dans une interface de chat, l'utilisateur voit immédiatement un défaut de format. Dans un workflow, la réponse passe directement au nœud suivant.
L'erreur survient sans témoin. Elle apparaît souvent sur une exécution déclenchée automatiquement, par exemple à la réception d'un e-mail, sans personne devant l'écran.
SyntaxError: Unexpected token : l'exécution s'arrête au nœud Code.```json confirme le diagnostic.Les corrections se distinguent par leur fiabilité et par l'endroit où elles interviennent : dans la consigne, dans le code, chez le fournisseur ou dans n8n.
| Niveau | Principe | Fiabilité | Limite |
|---|---|---|---|
| Consigne dans le prompt | Demander un JSON brut, sans markdown | Moyenne | Aucune garantie, le modèle peut ignorer la consigne |
| Nettoyage par code | Retirer les balises avant JSON.parse() | Bonne | Ne règle pas un JSON tronqué ou mal formé |
| Sortie structurée de l'API | Imposer un schéma côté fournisseur | Élevée | Dépend du modèle et de l'option disponible |
| Structured Output Parser n8n | Valider la réponse dans n8n | Élevée sur une chaîne simple | Peu fiable avec les agents |
Une phrase explicite réduit la fréquence du problème :
Réponds uniquement avec un objet JSON brut. N'utilise aucune balise markdown et n'ajoute aucun texte avant ou après.
Cette consigne diminue les occurrences sans les supprimer. Elle suffit rarement seule.
Un nœud Code placé juste après le LLM retire les balises, puis lit le JSON. La version suivante gère aussi un éventuel texte avant ou après l'objet :
// Le nom du champ ("output" ici) dépend du nœud LLM utilisé
const raw = $input.first().json.output;
// Retire les balises markdown éventuelles
let text = raw.replace(/```(?:json)?/gi, '').trim();
// Conserve ce qui se trouve entre la première et la dernière accolade
const start = text.indexOf('{');
const end = text.lastIndexOf('}');
if (start === -1 || end === -1) {
throw new Error('Aucun objet JSON trouvé dans la réponse du modèle');
}
text = text.slice(start, end + 1);
return [{ json: JSON.parse(text) }];
Cette version possède des limites connues. Elle suppose que la réponse est un objet et non un tableau placé à la racine. Elle ne répare pas une réponse tronquée. Et si le modèle renvoie plusieurs blocs JSON, elle les fusionne dans une seule chaîne invalide. Le message d'erreur explicite permet au moins de repérer rapidement ces cas dans les journaux d'exécution.
Lorsque plusieurs workflows appellent des LLM, il est préférable d'isoler ce nettoyage dans un sous-workflow réutilisable. Une correction ultérieure ne se fait alors qu'à un seul endroit.
Les principaux fournisseurs proposent désormais de contraindre la réponse à un schéma. Chez Gemini, il s'agit de définir responseMimeType sur application/json et de fournir un responseSchema.[1] Chez OpenAI, le paramètre response_format accepte un json_schema. La documentation précise que la sortie structurée garantit le respect du schéma, là où l'ancien mode JSON garantit seulement la validité de la syntaxe.[2]
C'est la solution la plus solide lorsqu'elle est disponible, car le format est imposé en amont plutôt que corrigé en aval.
n8n propose un sous-nœud dédié, le Structured Output Parser, qui valide la réponse selon un schéma JSON. Il s'active en cochant l'option « Require Specific Output Format » sur le nœud d'IA concerné.[3] La documentation de n8n signale qu'il est souvent peu fiable avec les agents, et recommande dans ce cas de confier l'analyse à une chaîne LLM distincte.[3]
Aucun niveau ne couvre tous les cas. La contrainte de format côté API réduit fortement le problème, le nettoyage par code rattrape les écarts restants et l'erreur explicite signale ce qui échappe encore aux deux premiers.
Cette panne a une particularité : elle se voit. Le workflow s'arrête, une erreur s'affiche, quelqu'un intervient. Elle illustre pourtant un principe plus large : dans un workflow automatisé, personne ne relit ce qui circule entre les nœuds.
Le même constat s'applique au contenu des requêtes. Une réponse mal formatée interrompt le traitement. Une donnée personnelle envoyée par erreur au modèle ne déclenche aucune alerte. Le coût d'un tel envoi commence au moment où la donnée quitte le poste de travail, ce que détaille l'article sur le copier-coller dans une IA. La question de la responsabilité juridique et financière se pose ensuite, comme l'explique l'article sur les fuites de données via l'IA. Les articles suivants de la série abordent cette seconde catégorie de pannes, celle qui ne produit aucun message d'erreur.
Pour comprendre pourquoi un appel d'API n'offre pas les mêmes garanties de confidentialité qu'une interface de chat, l'article sur les politiques de ChatGPT, Claude et Gemini détaille les différences de conservation et d'entraînement.
Le modèle a probablement entouré sa réponse de balises markdown. Elles sont invisibles dans une interface de chat, mais incompatibles avec un lecteur JSON strict. Le comportement peut varier d'une exécution à l'autre.
Le nœud a rencontré un caractère qu'il ne s'attendait pas à lire. Dans ce contexte, il s'agit le plus souvent du premier accent grave d'une balise markdown, parfois d'un texte d'introduction ajouté par le modèle.
La consigne dans le prompt aide mais ne garantit rien. Les modes de sortie structurée proposés par les API (schéma JSON imposé) offrent une garantie bien supérieure. Un nettoyage par code reste utile en complément.
Non. Il a été observé avec Gemini lors des tests du node Miravig. Il peut apparaître avec d'autres modèles selon la consigne donnée. Les fournisseurs ne publient pas de liste précise des cas concernés.
Oui. Lorsque la limite de longueur est atteinte, le JSON s'arrête avant l'accolade fermante et devient invalide. Dans ce cas, aucune balise n'est visible, et seule l'augmentation de la limite ou la réduction de la réponse attendue corrige le problème.
Les LLM peuvent entourer une réponse JSON de balises markdown, ce qui fait échouer JSON.parse() dans le nœud suivant. Le comportement n'est pas systématique, d'où un bug qui passe les tests et apparaît en production. L'erreur désigne le nœud Code alors que la cause se trouve dans la réponse du LLM. Combiner une sortie structurée côté API et un nettoyage par code offre la meilleure couverture, avec une limite à garder en tête : aucune de ces protections ne vérifie le contenu des données envoyées au modèle.
[1] Gemini API, sorties structurées et mode JSON
[2] OpenAI, guide des sorties structurées
[3] n8n, Structured Output Parser
Masquer les données sensibles avant l'appel au modèle : le node Miravig pour n8n détecte et masque les données identifiantes dans un workflow, avant l'envoi au LLM. Voir le mode d'emploi →