Tip: treat every AI Agent / LLM node output as untrusted input (one Code node that stops 2am executions dying)

Sharing a small pattern that saved me a lot of 3am debugging once I started wiring AI Agent and Basic LLM Chain nodes into real workflows: never let a downstream node consume the model’s text output directly. Treat every LLM response as untrusted, possibly-malformed input — because eventually it will be.

The failure mode is boring but common. You ask the model for JSON, 99% of runs return clean JSON, and then one run returns json\n{...}\n with a code fence, or a trailing “Here’s your data:” preamble, or valid-looking JSON with a hallucinated extra field. Your next node does JSON.parse(), throws, and the whole execution dies — often on a schedule at night where nobody sees it until the data’s already stale.

The pattern I settled on is a tiny Code node between the agent and everything downstream that does three things, in order:

  1. Strip fences + preamble. Grab the substring from the first { (or [) to the last matching } (or ]) before parsing. Kills the code-fence and “Sure, here you go” cases in one line.
  2. Parse inside try/catch, and on failure return a structured error item{ ok: false, raw: <original text> } — instead of throwing. Then an IF node routes ok:false to a fallback branch (retry the agent once, or push to a review queue) rather than 500-ing the run.
  3. Validate the shape you actually need, not just “is it JSON”. Check the two or three fields your downstream nodes read. A parse that succeeds but is missing email is still a failure for your purposes — catch it here, not three nodes later where the error message is useless.
// Code node, Run Once for Each Item
const text = $json.output ?? $json.text ?? '';
const start = Math.min(...['{','['].map(c => text.indexOf(c)).filter(i => i >= 0));
const end   = Math.max(text.lastIndexOf('}'), text.lastIndexOf(']'));
try {
  const obj = JSON.parse(text.slice(start, end + 1));
  if (obj.email == null) return { json: { ok: false, reason: 'missing_email', raw: text } };
  return { json: { ok: true, data: obj } };
} catch (e) {
  return { json: { ok: false, reason: 'parse_error', raw: text } };
}

The mindset shift that made everything more reliable: the boundary between “model” and “workflow” is a trust boundary, same as an incoming webhook. Parse defensively there, route failures explicitly, and your 2am executions stop silently dying. Retry logic and structured-output parsers help too, but this one Code node is the cheapest 80% and works on any n8n version.

(Disclosure: I build automation tooling and use AI assistance to write up notes like this — happy to expand any part if it’s useful.)

1 Like