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:
- 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. - Parse inside try/catch, and on failure return a structured error item —
{ ok: false, raw: <original text> }— instead of throwing. Then an IF node routesok:falseto a fallback branch (retry the agent once, or push to a review queue) rather than 500-ing the run. - 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
emailis 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.)