Setting up error workflow upon AI agent tool failure

Hello! I am having trouble to set up an error workflow when an AI agent tool fails. Because when a tool fails, the AI agent node itself doesn’t fail, so the execution doesn’t fail either. And it seems there are no properties within intermediateSteps that assure whether a tool has failed. Is there any workaround for this?

Hi @xmateusx14

I think this is a limitation of the AI Agent node

Error workflows in n8n are triggered only when a node fails, and the AI Agent node does not fail when a tool fails because tool errors are treated as part of the agent’s reasoning, not as execution errors.

As a solution i think you should handle it manullay , and you can do it with IF node or with code node ( try catch mechanism ) ,

for example , instead of letting tools fail, you can make them always return a structured JSON response like { "success": false, "error": "your error message" }, then let the AI Agent read this output and, after the agent, check the result with an IF node to detect failures and manually route the execution to an error branch

Hey @ayoub_ghozzi

It is really unfortunate n8n has such a limitation. But I’ve managed to think another way to catch tool errors. By setting up the agent structured output parser so the AI model analyzes and returns whether a tool has failed. Sure, it opens up the possibility of hallucinating on this matter as well, but at least it is a workround within n8n.

You say the tool itself can be set to always return a structured output parser, but i see no such option in its settings.

Hey everyone! I found a practical way to handle this.

If you are using an HTTP Request node as a tool for your AI Agent, you can prevent the workflow from crashing by using the ‘Never Error’ toggle.

Here is how to set it up:

  1. In the HTTP Request node, go to the Options section

  2. Add the Response option

  3. Toggle ‘Never Error’ to ON

Normally, if a tool fails, n8n stops the entire execution. By turning on ‘Never Error’, the node stays ‘green’ even if the API returns a 400 or 500 error. The error message is then passed back to the AI Agent as a regular string.

The Agent can then ‘read’ the error (e.g., ‘Missing field: email’) and, if instructed in its System Prompt, it can autonomously correct its input and try to call the tool again.

Keep in mind that this will significantly increase the number of interactions, as the agent will use more loops to analyze and fix the errors autonomously.

@Scribble’s “Never Error” tip is solid for HTTP Request tools. for other tool types (Code node, sub-workflow tools, etc.) a similar pattern works: wrap the tool logic in a try/catch inside a Code node and return a structured error object instead of throwing:

js
try {
  // your tool logic here
  return [{ json: { success: true, result: ... } }]
} catch (e) {
  return [{ json: { success: false, error: e.message } }]
}
```

then in your system prompt tell the agent: "if a tool returns `success: false`, read the `error` field and either retry with corrected input or inform the user what went wrong."

this way the agent stays in control of error recovery rather than the whole workflow crashing — and you can still detect failures downstream by checking `intermediateSteps` for any result where `success` is false.

This is a classic ‘silent failure’ problem with AI agents. @Scribble’s advice for the HTTP Request node is great, but for more complex tools like sub-workflows or the Code node, you’ve got to build ‘resilience by design.’

The best way I’ve found to handle this is to treat your tools like they are always returning a response, even when they fail. Wrap your sub-workflow or Code node logic so it always returns a structured object:

Then—and this is the key—in your System Prompt, explicitly tell the agent:
‘If a tool returns { success: false }, do not stop. Read the error field, attempt to fix the input (e.g., correct a date format or missing field), and retry the tool. If you cannot fix it after 2 attempts, explain the technical error to the user.’

This keeps the agent in the driver’s seat. If you need to trigger a global error workflow for logging or alerts, you can use an IF node immediately after the AI Agent node to check the or the final output for that flag and route it accordingly.

Ugh, my bash-brain just ate the code block in that last reply. Let’s try that again so you can actually see it:

Point being: if you return a JSON object with a success flag, the agent sees the error as data rather than a crash, and it can actually try to recover. You can also use an IF node after the agent to check intermediateSteps for any ‘success: false’ if you want to trigger a separate alert.

Third time is the charm. My bash shell keeps interpreting the code block. Here is the actual JS pattern:

try {
  // Your tool logic here
  return { success: true, data: result };
} catch (error) {
  // Don't let the node fail, return the error to the agent
  return { success: false, error: error.message };
}

(Replace [code] with backticks). If the agent sees { success: false }, it can use its reasoning to retry or fix. This prevents the ‘silent’ workflow stall because the node itself never actually crashes.

Salut @xmateusx14
J’ai trouvé ça sous un angle différent : mon agent a complètement contourné son outil au lieu que l’outil échoue. Calculatrice jointe, le prompt disait explicitement de ne pas calculer en interne. Il l’a fait quand même. Bonne réponse, nœud vert, intermediateSteps vide.

Le structured output parser fonctionne, mais tu demandes au modèle de signaler lui-même son propre échec. J’ai opté pour du déterministe à la place. J’ai construit un nœud Code que tu ajoutes après l’agent. Huit vérifications incluant vide, refus, mauvais JSON, clés manquantes, placeholders, troncature, répétition du prompt, et outils requis qui n’apparaissent jamais dans intermediateSteps. Ajoute contractOk et contractFailures à chaque élément, tu branches sur un nœud IF. Pas d’IA utilisée ni de dépendances.

Ce n’est que pour les défaillances mécaniques, pas les mauvaises réponses. Si tu as une vraie sortie qui casse une vérification, envoie-la et c’est comme ça que j’ai trouvé un faux positif dans ma propre règle de troncature.

Ananya :slight_smile:

Il y a un troisième cas qui se situe entre les deux modes de défaillance de ce fil, et il passe tous les contrôles décrits jusqu’à présent : l’outil s’est exécuté, s’est terminé avec succès et n’a rien renvoyé.

Le mien était une étape de récupération alimentant un agent. Un nœud de filtre en aval avait une condition obsolète restante d’une correction antérieure, et il a réduit 8 lignes correctement récupérées à 0. L’appel d’outil lui-même était correct. Il a renvoyé le succès avec un tableau vide.

Le motif try/catch voit le succès. L’objet structuré voit le succès. Et le contrôle du contrat voit l’outil présent dans intermediateSteps, donc il passe aussi là. L’agent a alors fait la chose tout à fait raisonnable avec un ensemble de résultats vide et a dit qu’il n’avait pas cette information. Tout au vert, rien levé, et la sortie était un refus poli, bien formé et complètement erroné.

Ce que j’ajouterais au motif que les gens décrivent ici : retourner le décompte, pas seulement le drapeau.

try {

  const rows = await lookup(q);

  return { success: true, count: rows.length, data: rows };

} catch (e) {

  return { success: false, error: e.message };

}

Ensuite, le nœud IF vérifie count === 0 ainsi que success === false. Zéro est parfois une réponse légitime, donc vous ne pas échouer brutalement sur celui-ci, vous le consignez et regardez le taux. Un outil de récupération qui passe silencieusement de 5 pour cent vide à 100 pour cent vide est cassé d’une manière qui ressemble à de la prudence.

@Ananya_p_kumar sur votre mise en garde selon laquelle cela ne détecte que les défaillances mécaniques et non les mauvaises réponses : je pense que « vide quand il ne devrait pas être vide » est la seule tranche de mauvaise réponse qui est mécaniquement détectable, à condition que l’outil signale un décompte. Cela vaut peut-être un neuvième contrôle, un drapeau obligatoire d’outil renvoyant zéro ligne, pour les outils qui se déclarent comme retournant des collections. Je serais heureux de vous envoyer un exemple désinfecté si utile.

Ce qui l’a réellement trouvé pour moi cependant, et ce que je recommanderais au-dessus de n’importe quel nœud unique : conservez une poignée d’entrées dont vous connaissez déjà la réponse correcte, et exécutez-les contre la production selon un calendrier. Les journaux vous disent que la machine a fonctionné. Les réponses connues vous disent que c’était correct.

Bonne observation phantomTool vérifie si l’outil est apparu dans intermediateSteps, pas s’il a retourné quelque chose, donc un appel réussi retournant une collection vide ressemble à un retour de dix lignes. C’est le cadrage piloté par contrat qui rend le neuvième contrôle viable : déclarez quels outils retournent des collections, marquez zéro uniquement pour ceux-ci. Il faut que l’outil signale un décompte, donc cela dépend de votre pattern count: rows.length. J’ai ouvert une issue pour cela : emptyCollection check for tools that return collections · Issue #1 · Ananyapkumar/agent-contract · GitHub et un exemple serait très bienvenu. Et en reprenant votre point que vide quand ce ne devrait pas l’être est la tranche détectable mécaniquement du mal, j’avais tracé cette limite trop largement.

Le point soulevé par Adam13y est celui sur lequel il vaut la peine de construire — succès/échec n’est pas la bonne forme pour un résultat d’outil. Ce qui tient mieux est de faire en sorte que chaque outil retourne {status, count, data}, où status est l’un de ok / empty / degraded / failed, puis d’affirmer sur ceux-ci après l’agent plutôt qu’à l’intérieur d’un outil.

La partie qui est généralement omise : pour que le flux d’erreur se déclenche, ce nœud d’assertion doit réellement lever une exception. Un agent qui a retourné une mauvaise réponse confiante est une exécution échouée, et n8n ne la traitera comme telle que si quelque chose en aval lève une exception.

d’accord sur la forme, et la partie lancement est là où j’ai commis une erreur la première fois. J’ai essayé de faire lancer une exception sur vide et c’était miserable. Vide est souvent légitime : personne n’a demandé quelque chose dans le corpus, la récupération retourne correctement rien, l’agent dit correctement qu’il ne sait pas. Lancer une exception là-dessus et tu te réveilles quotidiennement et tu arrêtes de lire tes propres alertes en une semaine. Ce qui a fonctionné était de le diviser. CETTE exécution est-elle incorrecte : lancer, mais seulement à une limite destructrice, où un résultat vide est sur le point d’alimenter une écriture ou un envoi ou un paiement. Le TAUX est-il incorrect : ne pas lancer, surveiller. Une étape de récupération à 4 pour cent vide pendant des mois qui passe à 100 pour cent est cassée, et aucune exécution unique dans cette fenêtre ne semble différente d’une non-correspondance légitime. C’est ce qui a attrapé mon bogue de filtre, et lancer n’aurait jamais pu, car chaque exécution était individuellement défendable. Lancer marque aussi la course comme échouée, ce qui pollue ton taux d’erreur et peut déclencher des tentatives qui réexécutent des effets secondaires. À payer uniquement où l’action est destructrice.

L’objection sur les tentatives est la partie sur laquelle je reviendrais, car c’est réparable plutôt qu’une taxe permanente. Lever une exception n’est coûteux que quand la tentative n’est pas sûre à exécuter deux fois. Clé d’idempotence sur les POST sortants, upsert sur une clé métier au lieu d’un ajout, réclamation de la ligne avant envoi sur tout ce qui quitte le système. Faites ça et réexécuter un flux échoué cesse d’être quelque chose à peser avant de lever une exception, ce qui signifie que vous pouvez être strict à la frontière au lieu de la rationner.}L’autre moitié, c’est que la surveillance des taux a besoin de volume, et beaucoup de ces flux n’en ont pas. À 30 exécutions par jour, une étape de récupération dérivant de 4 pour cent vide à 40 prend plus d’une semaine pour se distinguer du bruit, et c’a été faux tout ce temps. Votre idée de réponse connue couvre exactement ce manque : une entrée canari sur un calendrier vous donne un signal en une seule exécution peu importe à quoi ressemble le trafic. Elles ne sont pas en concurrence. Le taux détecte la dérive lente où il y a du volume à mesurer, les canaris la détectent où il n’y en a pas.}Une chose sur les canaris cependant. Exécutez-les par le flux de production avec les credentials de production, pas une copie. Votre bug vivait dans un nœud de filtre, et un flux de test dupliqué aurait eu un filtre propre et aurait réussi à chaque fois.}Dernier point : tout ce qui marque une branche comme destructive doit être une propriété du flux, pas quelque chose dont vous vous souvenez. Une étiquette sur l’outil, ou simplement la règle que le nœud d’assertion s’assoit immédiatement avant chaque nœud d’écriture, d’envoi et de paiement et nulle part ailleurs. Sinon la vérification stricte finit sur quel que soit le chemin que vous aviez touchée le jour où vous l’avez construit.

C’est juste sur le point des tentatives, et je pense que tu as raison : je le traitais comme un coût fixe alors que c’est un choix de conception. Clé d’idempotence en sortie, upsert sur une clé métier, réserver la ligne avant envoi. Fais ça et les erreurs cessent d’être quelque chose qu’on rationne. Ton point sur l’exécution de canaries en production plutôt que sur une copie est celui que je veux souligner, car il est plus fort qu’il n’y paraît. Un workflow dupliqué ne reçoit pas simplement un filtre propre. Il reçoit de nouvelles credentials, son propre budget de limite de débit, un cache froid, et la configuration que la personne qui l’a cloné avait ce jour-là. Tu finis par tester la conception au lieu de la chose déployée, et la chose déployée est la seule qui sert quelqu’un.

Un piège qui mérite d’être nommé pour quiconque construit ça, parce qu’il m’a eu et ce n’est pas évident. Une canary avec réponse connue peut passer alors que la récupération ne retourne absolument rien. Si la question que tu choisis a une réponse que le modèle connaît déjà, il répondra correctement à partir de ses propres poids et la canary devient verte avec un contexte vide. La canary doit poser une question qui n’est réponse que à partir du corpus. Un fait interne inventé, une ligne de politique, un nombre qui n’existe nulle part ailleurs. Si une question de connaissance générale peut la satisfaire, ce n’est pas la récupération qu’on teste, c’est le modèle.

Sur ton dernier paragraphe, que le marqueur destructif doit être une propriété du workflow plutôt que quelque chose qu’on se souvient. Je suis totalement d’accord, et j’ajouterais aussi que ça doit échouer bruyamment quand c’est absent.

J’en ai trouvé une dans mon propre système ce matin. Une étape d’autoapprentissage derrière un drapeau de config. Le drapeau était mis à vrai il y a un mois. Le fichier qu’il lit n’avait jamais été généré, donc le chargeur a retourné vide, le filtre en aval est devenu une no-op, et chaque exécution loggait une ligne disant qu’il ne pouvait pas lire le fichier et l’ignorait, sans effet. Rien n’a erreur. Le drapeau disait on. Pendant un mois il n’a rien fait, et la ligne de log était assez honnête pour que j’aie cessé de la voir.

Ce qui est exactement ton point. Un garde qui se dégrade silencieusement à off est pire que pas de garde, parce que tu arrêtes de regarder. S’il ne peut pas trouver ce dont il a besoin, il devrait refuser de s’exécuter plutôt que de continuer tranquillement.

Salut @xmateusx14 ouais malheureusement on doit utiliser la solution de contournement, ce que je fais c’est que je fais comme dans l’image ci-dessous :

donc essentiellement dans le paramètre d’agent IA, je fais comme ci-dessous :

Maintenant avec cette option tu vas avoir 2 routes : « Succès » et « Erreur », tu peux envoyer le message d’erreur à slack ou discord…etc ou le passer à un autre sous-workflow qui va gérer l’erreur et continuer le workflow sur la route succès (Veuillez consulter la première image pour voir les deux routes).

Merci

L’utilisation de Continue (using error output) sur l’Agent IA crée uniquement une branche d’erreur lorsque le nœud Agent lui-même génère une exception. Elle ne convertit pas une erreur d’outil que l’agent a consommée en une exécution échouée, elle ne couvre donc pas le cas original.

Gardez le résultat de l’outil déterministe. Retournez un statut typé et un décompte pour les outils de collection. Immédiatement avant tout effet secondaire externe, affirmez l’état que vous exigez et levez une exception si le contrat est rompu. Rendez cet effet secondaire idempotent afin qu’une nouvelle tentative ne puisse pas le dupliquer.

Traitez empty séparément de failed. Empty peut être valide, donc surveillez son taux ou testez-le avec une canary de corpus uniquement. Un outil défaillant, un résultat mal formé ou un appel d’outil requis manquant ne doivent pas être laissés pour que le modèle les signale lui-même.

Deux choses qui posent encore problème après les commentaires ultérieurs.

Continue (using error output) sur le nœud Agent n’ouvre une branche d’erreur que lorsque le nœud Agent lui-même lève une exception. Il ne convertit pas une erreur d’outil déjà consommée par l’agent en une exécution échouée, donc le Workflow d’erreur ne démarre toujours pas. C’est le cas initial.

L’autre lacune est qu’un Agent vert représente trois états différents : exception d’outil avalée, données vides avec succès, et outil ignoré (intermediateSteps vide). La sortie d’outil typée (status + count) plus une assertion immédiatement avant une écriture, un envoi ou un paiement est ce qui vous permet de lever une exception intentionnellement et finalement de déclencher le déclencheur d’erreur. Ne levez pas d’exception à chaque extraction vide.

J’ai rédigé les commentaires ultérieurs sous la forme d’un guide pratique, y compris l’accusé de réception webhook en premier pour qu’une tentative de délai d’expiration ne lance pas l’Agent deux fois : n8n Error Workflow for AI Agent Tool Failures (Why the Agent Stays Green)

Je développe n8nChat, une extension Chrome/Firefox qui place un graphique sur le canevas n8n officiel. Elle ne remplace pas le déclencheur d’erreur. Utile ici uniquement si vous souhaitez générer ces deux graphiques sur le flux de travail que vous avez déjà ouvert.