Dev.to AI 🤖 Ai 👁 0 📖 8 min read

My 12-Point Pre-Launch Checklist for n8n LLM Workflows. I Re-Ran 3 Old Failures and It Caught 21 of 21 Bad Runs

Over the last few weeks I've broken a lot of my own n8n AI workflows on purpose. A lead qualifier that pinged Slack about a "hot lead" whose message was four emojis. An invoice extractor that added tax on top of a total

Over the last few weeks I've broken a lot of my own n8n AI workflows on purpose. A lead qualifier that pinged Slack about a "hot lead" whose message was four emojis. An invoice extractor that added tax on top of a total that already included tax. A support bot that made up 22 answers. A workflow that "succeeded" by posting to a Slack placeholder URL and getting Slack's docs page back.

Every one of those failures turned into a rule I now check before I switch a workflow on. This post is that list: 12 checks, and each one comes from something that actually went wrong on my machine.

A checklist is only worth something if it catches things, though. So this morning I took three of the old failures, rebuilt the version that failed and the version with the checklist item applied, and ran both again, three rounds each.

Short version: the failing versions produced 21 bad runs, and the checklist versions caught all 21. The re-run also turned up one new gap the checklist didn't cover, so I added it.

The setup

  • n8n 1.123.84, fresh install, local Qwen 1.8B through Ollama's OpenAI-compatible endpoint (temperature 0.2, JSON mode)
  • A small local mock that records Slack posts and Error Trigger alerts, so nothing real got pinged
  • Every workflow has an Error Workflow (Error Trigger → alert) attached
  • Three rounds per case, same inputs as the original tests

The 3 re-runs

Re-run Original failure Checklist item Without it With it
1 Slack URL left as the placeholder, run shows green #1 setup guard 3/3 green, 0 messages delivered, 0 alerts 3/3 red with a clear error, alert fired 3/3
2 Junk form input scored as a hot lead #3 input guard 12/12 "Hot lead" Slack pings (scores 75–85) 12/12 rejected with HTTP 422, 0 pings, 0 LLM calls
3 Invoice totals wrong #6 recompute totals in code 6 wrong totals out of 9 runs 6/6 flagged, 0 false flags

Re-run 1: the Slack placeholder that "works"

The original lead qualifier template ships with https://hooks.slack.com/services/REPLACE/WITH/YOUR_WEBHOOK in the Slack node. When I import-tested my own workflows on a fresh install, this was one of the 3 failures: Slack answers that URL with a 302 redirect to its API docs, the HTTP Request node follows the redirect, gets a 200 HTML page, and calls it a success.

I checked it again today with curl. Still 302 → https://api.slack.com/.

Re-run without the guard: 3 of 3 executions green. The Slack node's output was an HTML docs page. Zero messages delivered, zero alerts. If you only look at the execution list, everything is fine.

With the guard, the Config node refuses to run while anything still looks like a placeholder:

const cfg = {
  baseUrl: 'http://127.0.0.1:11434/v1',
  model: 'qwen-local',
  slackWebhook: 'https://hooks.slack.com/services/REPLACE/WITH/YOUR_WEBHOOK',
};
// Fail loudly if setup isn't finished, instead of "succeeding" against a placeholder.
const unset = Object.entries(cfg)
  .filter(([, v]) => !v || /REPLACE|YOUR_|example\.com/i.test(v))
  .map(([k]) => k);
if (unset.length) throw new Error(`Setup not finished: set ${unset.join(', ')} in the Config node`);
return [{ json: { ...$input.first().json, ...cfg }, pairedItem: 0 }];

Result: 3 of 3 executions red with Setup not finished: set slackWebhook in the Config node, and the Error Trigger alert fired every time.

One thing it did not fix: the webhook caller still got HTTP 200 with an empty body all 3 times. That's the same thing I saw in the malformed-input test, and it's why check #10 exists separately.

Re-run 2: emoji-only "hot leads"

From Breaking My Own Agents #1: the lead qualifier happily scores forms whose message is empty, "undefined", emoji only, or just <script>alert(1)</script>. The rest of the form (name, company, budget) looks great, so the model scores it high.

I sent those 4 inputs, 3 rounds each.

Without the input guard: 12 of 12 runs returned HTTP 200 with scores of 75–85, and all 12 sent a "🔥 Hot lead" ping to Slack.

Something I hadn't noticed before: in every one of those 12 answers (and the 3 normal runs from re-run 1), the model set tier: "warm". The workflow pings Slack on score >= 70, so it announced a "hot lead" that the model itself had called warm. That's check #11.

With the input guard (one Code node right after the Webhook, plus an IF and a Respond-422 node), the core of which is:

const t = clean(v); // strip HTML, invisible chars, template syntax
const meaningful = (t.match(/[\p{L}\p{N}]/gu) || []).length;
if (/^(undefined|null|nan|\[object object\])$/i.test(t))
  problems.push(`"${SPEC.field}" is the literal string "${t}"`);
else if (meaningful < SPEC.min)
  problems.push(`"${SPEC.field}" has ${meaningful} letters/digits after cleaning (min ${SPEC.min})`);

12 of 12 rejected with HTTP 422 and a reason like "message" has 0 letters/digits after cleaning (min 10), in about 0.1 seconds, with no LLM call and no Slack ping. The unguarded runs took about 5 seconds each on my CPU.

Re-run 3: tax added twice

From the invoice extractor test, I took the three documents that went wrong most often: an AWS bill, a freelancer invoice with an advance already paid, and a German hosting invoice.

Results, 3 rounds:

  • AWS: model said 62.93, the bill says 54.61. It added the tax again on top. 3/3 wrong.
  • Freelancer: model said 20,000 (the balance due), the invoice total is 30,000. 3/3 wrong.
  • German invoice: the amount was right all 3 times this round, but the model returned it as the string "1.234,55 €". More on that below.

The validation node doesn't ask the model to double-check. It finds the total line in the source text itself and compares:

const KW = /(grand total|order total|total due|amount payable|amount paid|net amount payable|gesamtbetrag|\btotal\b|charged)/i;
const totalLine = doc.split('\n').filter(l => KW.test(l) && !/(sub ?total|item total|balance)/i.test(l)).pop();
// ...
if (docTotal !== null && Math.abs(t - docTotal) > 0.01) reasons.push(`total ${t} != document total ${docTotal}`);

6 of 6 wrong totals flagged for human review, 0 correct runs flagged.

The gap: the German total passed validation as ok, which is right in one sense, because the validator understands German number format and the amount matched. But the output still said "1.234,55 €", and a numeric column in Google Sheets or your accounting tool would get a string. The checklist said "compare the number", not "hand off a number". That became check #7.

The checklist

Each item names where it came from. You don't need my workflows to use it.

Before you switch it on

  1. No placeholders left. Make the Config node throw on anything matching REPLACE|YOUR_|example.com. Source: placeholder Slack URL redirected to the docs page and the run went green (re-run 1: 3/3 caught).
  2. Import it into a clean n8n and run it once using only your own README. Source: 3 of my 10 workflows failed on a fresh install after the documented setup. Importing the same file twice also gave me two copies fighting over one webhook path.

At the input

  1. Guard the input before the model, in code. Type checks, a "meaningful characters" count, a field whitelist, and a 422 with the reason. Source: 95 of 110 unusable inputs went green in the malformed-input test (re-run 2: 12/12 caught).
  2. Cap input length at what your model's context can actually take. Source: Oversized inputs (~300 KB of text, a 50,000-character token) overflowed the context in 11 runs. The error alert fired, but the caller got a 200.

At the output

  1. Parse strictly and throw on a bad answer. Check exact key names. Source: 8 of 31 raw FAQ answers came back with a " escalate" key (leading space), so escalate was silently missing.
  2. Recompute any number the model extracts against the source, in code. Source: tax added on top of tax-inclusive totals (re-run 3: 6/6 caught).
  3. Normalise numbers and dates into one format before handing them off. Source: 02/11/2026 read as February 11 in the invoice test, and today's "1.234,55 €" string that passed validation.
  4. Ground answers in your own facts and let "I don't know" escalate. Source: my support-FAQ bot made up 22 answers. Grounding brought it to 0.

At the decision

  1. Keep decisions that cost something in code, not in the prompt. Source: in my prompt injection test, the "safer" prompt let 13 of 20 injections through. Moving the decisions into code got that to 0 of 60 runs.
  2. Make failures loud in two places: an Error Workflow for you, and a real error response for whatever called the webhook. Source: in both the import test and the malformed-input test, crashed workflows answered the caller with HTTP 200 and an empty body. The setup guard in re-run 1 still did that. I haven't solved this half yet.
  3. Make action thresholds check more than one signal. If the model gives a score and a tier, require both to agree before you ping anyone. Source: 7 fake hot leads on the 1.8B model test, and today's 15 "hot lead" Slack posts (12 to my mock, 3 lost to the placeholder URL) where the model's own tier said warm.
  4. Hand-review a sample of the outputs your checks marked as passed. Source: in the blog-to-social test, my validator passed posts that a hand review found wrong: 10 of 21 "ready" posts misstated the blog post.

What I took from re-running it

  • A green execution tells you n8n ran. It doesn't tell you the workflow worked. All three failures I re-ran showed up as green executions.
  • The cheapest checks caught the most. Re-runs 1 and 2 are a regex and a character count. They caught 15 of the 21 bad runs before the model was even involved.
  • Re-run your old failures. The model's mistakes move around. The German invoice gave a wrong total 2 of 3 times last week, and this week it got the amount right in a format my pipeline can't use. I only found that because I ran it again.

The base lead qualifier is in the free repo: github.com/arkintof/n8n-ai-agent-workflows (workflows/01-lead-qualifier.json). The guarded and validated versions used in this test are not in the repo, and neither is the invoice extractor. The key code for checks 1, 3 and 6 is above. Each is one Code node.

What's on your pre-launch list that isn't on mine?

Written with AI assistance; workflow built and tested by me.

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.