Aller au contenu

Automatiser une variable grammaticale avec une IA dans n8n

Il y a des bugs qui se voient tout de suite : un email qui ne part pas, une erreur 500. Et il y a ceux qui se cachent dans le texte lui-même, invisibles tant que personne ne relit la phrase à voix haute. C'est cette deuxième catégorie qui a occupé cette session : une variable de personnalisation vide dans des campagnes Lemlist, et le correctif construit pour la remplacer, un petit sous-workflow n8n qui appelle un LLM pour écrire... trois mots de grammaire française.

Ça a l'air trivial. Ça ne l'était pas complètement, et la façon dont ça a dérapé (deux fois) est sans doute plus intéressante que la solution elle-même.


Le problème : une phrase à trous

Sur les campagnes commerciales sortantes (Lemlist), chaque email est personnalisé avec des variables du type {{firstName}}, {{companyName}}. Rien d'original. Mais certaines phrases ont besoin de plus qu'un nom brut :

"Comment sécurisez-vous les communications {{companyDu}} ?"

Le rendu attendu : "Comment sécurisez-vous les communications de la MACIF ?" ou "...d'Allianz France ?" ou encore "...de CNP Assurances ?". {{companyDu}} n'est pas le nom de l'entreprise : c'est la préposition et le nom, prêts à être insérés tels quels. Sans cette variable remplie, l'email part avec un trou monstrueux dans la phrase, ou pire, avec la variable brute non substituée sous les yeux du prospect.

Jusqu'à récemment, cette variable était calculée à la main, avec l'aide d'un assistant IA en session, à chaque import de leads depuis Apollo vers Lemlist. Ce travail manuel a disparu avec l'automatisation complète de cette étape : formulaire de création de lead et enrichissement automatique via Apollo, tous deux pilotés directement par des workflows n8n, sans plus aucune session manuelle entre les deux. Résultat : plus personne ne calcule companyDu à la main, et les nouveaux leads arrivent dans les campagnes avec ce champ vide.

Le déclic

Ce n'est pas un audit qui a repéré le problème en premier : c'est un aperçu de campagne dans Lemlist qui affichait littéralement {{companyDu}} non résolu dans le sujet d'un email de test. Le genre de détail qu'on ne voit qu'en relisant, jamais en codant.


L'enquête : ne pas faire confiance à un seul exemple

La première tentative de correctif s'est basée sur un seul cas déjà résolu manuellement : trois leads (MACIF, Allianz France, CNP Assurances) pour lesquels quelqu'un avait renseigné companyDu à la main dans une conversation précédente. Une règle de grammaire a été extrapolée à partir de ces trois exemples, un sous-workflow construit, testé, livré.

Erreur de méthode. Après un signalement invitant à regarder comment la variable est réellement utilisée dans les campagnes, la bonne démarche s'est imposée : lire le contenu réel des séquences d'emails plutôt que de se fier à un souvenir de conversation.

Un audit des 32 campagnes actives sur le compte Lemlist (via l'API, en cherchant tous les motifs {{company...}} dans les sujets et corps d'email) a suivi. Résultat : il n'existe pas une, mais deux variables de ce type, toujours utilisées en binôme dans les mêmes séquences.

Variable Signifie Exemple de phrase Rendu attendu
{{companyDu}} "de/du/de la/d'" + société "les communications {{companyDu}}" "les communications de la MACIF"
{{companyAu}} "à/au/à la" + société "voir si ça s'applique {{companyAu}}" "voir si ça s'applique à la MACIF"

companyDu apparaît dans 13 campagnes, companyAu dans 3 ; mais partout où companyAu est présent, companyDu l'est aussi dans la même séquence. Ce n'est pas un doublon accidentel : ce sont deux formes de la même analyse grammaticale (le genre implicite du nom d'entreprise), utilisées à des endroits différents de la phrase selon la préposition qu'appelle le verbe.

Point terrain

Ces variables sont-elles répertoriées comme des "champs personnalisés" côté Lemlist (l'outil expose un endpoint pour ça) ? Réponse : non. companyDu/companyAu sont des variables de lead, posées au cas par cas via l'API, pas des champs enregistrés au niveau de l'équipe. Un endroit de plus où l'outil ne dit pas tout : il a fallu aller lire le contenu produit, pas la métadonnée qui décrit la structure.


Pourquoi un LLM, et pas une simple règle

La tentation du développeur : coder une fonction déterministe. if name.startswith(vowel) return "d'" + name. Le problème, c'est le cas des sigles et acronymes prononcés comme des noms communs, la partie la plus fréquente dans les secteurs cibles (mutuelles, banques, assurances, collectivités). "MACIF" se dit "la MACIF" à l'oral (comme "la Sécu"), "CIC" se dit "le CIC". Ce genre implicite n'est pas dans le nom lui-même, il vient de la connaissance culturelle de ce que désigne le sigle. Une regex ne le devine pas ; un LLM généraliste le sait déjà, pour la quasi-totalité des organismes ciblés.

D'où le choix : un agent IA (via OpenRouter, un modèle léger : largement suffisant pour cette tâche, pas besoin d'un modèle de raisonnement pour trois mots de grammaire), avec une sortie structurée forcée (JSON {companyDu, companyAu}) pour éviter tout texte parasite en sortie.

flowchart LR
    A["companyName (entree)"] --> B{"Nom vide ?"}
    B -->|oui| C["companyDu = ''\ncompanyAu = ''"]
    B -->|non| D["Agent IA (OpenRouter)"]
    D --> E["Sortie structuree\n{companyDu, companyAu}"]
    E --> F["Nettoyage / trim"]
    C --> G(("Retour"))
    F --> G

Le sous-workflow saute l'appel au LLM si companyName est vide, pas la peine de payer un appel API pour ne rien produire.


L'écriture du prompt, et sa première erreur de cadrage

Premier jet du prompt système :

"Ta tâche : produire la forme correcte pour introduire un nom d'entreprise après la préposition 'de', comme dans 'le contact de la MACIF'..."

Ça a l'air clair. Ça ne l'est pas. Cette formulation laisse entendre que le mot "de" est déjà écrit dans le template, et que le modèle doit juste produire ce qui vient après. Or ce n'est pas le cas : dans le texte réel, il n'y a aucune préposition en dur, juste {{companyDu}} tout seul, au milieu de la phrase. Si le modèle interprète le prompt littéralement, il risque de renvoyer seulement "la MACIF" (en pensant que le "de" est déjà géré ailleurs), et la phrase finale devient "les communications la MACIF", grammaticalement cassée.

C'est en confrontant cette formulation au texte réel des campagnes que le problème est devenu visible. Le prompt a été réécrit pour lever toute ambiguïté :

Contexte important : dans nos templates d'emails, AUCUNE préposition
n'est écrite en dur avant la variable. Le texte est par exemple
"les communications {{companyDu}}" (et non "de {{companyDu}}").
Cela signifie que TU dois produire la préposition ET le nom ensemble :
la valeur que tu renvoies est insérée telle quelle dans la phrase.

Ne réponds JAMAIS avec seulement le nom de l'entreprise ou
seulement un article sans préposition.

La leçon

Quand on écrit un prompt pour remplir une variable de template, il faut citer le texte réel qui entoure cette variable, jamais le paraphraser. "Après la préposition de" est une paraphrase qui présuppose une structure qui n'existe pas dans les faits. Coller la vraie phrase ("les communications {{companyDu}}") dans le prompt élimine l'ambiguïté à la source.


Le bug qui ne dit pas son nom

Voici la partie la plus instructive de la session. Le prompt corrigé, des tests relancés, tout est passé au vert ("succès" sur chaque exécution). Publication. Message : "c'est corrigé".

Réponse : "Je vois pas de changement."

Le nœud rouvert dans l'éditeur n8n affichait toujours l'ancien texte. Pas un problème de cache navigateur : le vrai contenu du nœud n'avait effectivement jamais changé.

Un chemin JSON relatif au mauvais objet

En creusant, la cause : l'outil utilisé pour éditer les workflows n8n accepte un chemin JSON ("JSON Pointer") pour cibler un champ précis à l'intérieur des paramètres d'un nœud. Ce chemin est relatif à l'objet parameters du nœud, pas au nœud entier. Le chemin utilisé était /parameters/options/systemMessage au lieu de /options/systemMessage.

Résultat concret : au lieu de modifier node.parameters.options.systemMessage (le champ que n8n lit réellement), l'appel a créé une nouvelle clé appelée littéralement parameters, nichée à l'intérieur des paramètres existants :

{
  "parameters": {
    "promptType": "define",
    "options": { "systemMessage": "ANCIEN TEXTE" },
    "parameters": {
      "options": { "systemMessage": "NOUVEAU TEXTE" }
    }
  }
}

Le nouveau texte existait bel et bien dans le workflow, juste au mauvais endroit, dans une clé que n8n ignore silencieusement. Aucune erreur de validation, aucun avertissement. L'outil a répondu "succès" à chaque appel, parce que d'un point de vue strictement technique, il avait bien écrit ce qui lui était demandé, au mauvais chemin.

Un échec silencieux est pire qu'un échec bruyant. Une erreur claire aurait arrêté le processus immédiatement ; celle-ci a laissé passer trois mises à jour successives, dont une qui ajoutait carrément une deuxième variable (companyAu), sans qu'aucune ne prenne effet. Le vrai prompt en production est resté figé sur la toute première version, pendant qu'une itération dessus semblait progresser. Ce même type de piège d'édition de nœud n8n s'est reproduit sur un autre chantier : voir le post-mortem de l'enrichissement Apollo/Lemlist.

La correction : remplacer l'édition ciblée par un remplacement complet et explicite de l'objet de paramètres du nœud, puis relire l'état réel du workflow après coup pour vérifier, pas se contenter de l'absence d'erreur en retour. C'est en comparant le contenu retourné par cette relecture (qui distinguait bien le "brouillon" du nœud et sa "version active publiée") que la structure dupliquée est devenue visible.


Brancher le correctif sans casser l'existant

Une fois le sous-workflow fiable, restait à le connecter aux deux workflows qui créent ou mettent à jour des leads. Contrainte technique à respecter : dans n8n, un nœud "Exécuter un sous-workflow" remplace entièrement les données de l'item en cours par ce que retourne le sous-workflow. Si on l'insère directement dans la chaîne principale, on perd tous les champs qui existaient avant l'appel (email, nom, poste...).

La solution standard : brancher l'appel au sous-workflow en parallèle de la chaîne principale, puis recombiner les deux branches avec un nœud de fusion (position par position) avant de continuer.

flowchart TD
    A["Lead prepare\n(email, nom, societe...)"] --> B["Branche A\n(passthrough)"]
    A --> C["Branche B\nSous-workflow companyDu/Au"]
    B --> D["Fusion (par position)"]
    C --> D
    D --> E["Creation du lead Lemlist"]

Deuxième contrainte, propre à un des deux workflows : le nœud natif Lemlist de création de lead n'accepte aucune variable personnalisée en paramètre : seulement les champs fixes (nom, poste, téléphone...). Il faut donc poser companyDu/companyAu après coup, via un appel HTTP séparé (PATCH /api/leads/{id}/variables). Ce second appel est placé sur une branche latérale, qui ne redescend jamais vers le message de fin de formulaire : de cette façon, si ce PATCH échoue ou traîne, ça ne peut jamais faire planter ni retarder la confirmation envoyée au commercial qui vient de créer le lead.


Comment tester un prompt qu'on ne peut pas vraiment lire

Contrainte plus inattendue : l'outil disponible pour lancer une exécution de test dans n8n ne renvoie que le statut de l'exécution (succès / erreur), jamais le contenu produit par chaque nœud. Impossible donc de lire noir sur blanc ce que le modèle avait répondu pour vérifier la grammaire à l'œil.

Le contournement : la sortie structurée forcée (schéma JSON attendu) agit comme un filet de sécurité indirect. Si le modèle avait renvoyé du texte hors-format, ou oublié un des deux champs demandés, l'exécution aurait échoué au moment du parsing, pas juste produit un résultat de mauvaise qualité en silence. Un statut "succès" constant sur plusieurs entreprises tests (MACIF, Allianz France, CNP Assurances, CIC), y compris sur le cas "nom vide" qui doit sauter l'appel IA (exécution en ~10 millisecondes, signe qu'aucun appel réseau n'a eu lieu), donnait une garantie raisonnable, sans toutefois remplacer une lecture humaine du résultat final dans l'outil.


Ce qui reste à surveiller

  • Deux variables aujourd'hui, peut-être une troisième demain. L'audit n'a trouvé que companyDu et companyAu dans les 32 campagnes actuelles. Si une nouvelle campagne introduit une construction grammaticale différente, il faudra l'ajouter au même sous-workflow plutôt que d'en multiplier des variantes dispersées.
  • Un des deux workflows appelants reste désactivé. Celui qui met à jour les contacts déjà présents dans Lemlist (plutôt que d'en créer de nouveaux) n'a pas encore été validé en écriture réelle : il attend une activation volontaire plutôt qu'automatique.
  • La leçon sur les chemins JSON dépasse ce cas précis. Depuis, chaque modification de nœud n8n est suivie d'une relecture complète de l'état réel du workflow, pas seulement d'une vérification de l'absence d'erreur retournée par l'outil. Une réponse "succès" documente ce que l'outil a tenté d'écrire, pas nécessairement ce qui sera effectivement lu au moment de l'exécution.

Workflows construits et publiés sur l'instance n8n interne, connectés à Lemlist (CRM commercial) et OpenRouter (accès modèle). Les noms d'entreprises cités (MACIF, Allianz France, CNP Assurances, CIC) sont des exemples de test issus d'échanges antérieurs, pas des données extraites d'un système de production.

Pour aller plus loin : Enrichir automatiquement des contacts Lemlist avec Apollo via n8n, les deux workflows qui appellent ce correctif de personnalisation.