This is part of my build-in-public series on running LLM workflows in n8n in production. Everything here is a failure I actually watched happen, not a hypothetical.
The workflow passed every test I wrote. Green on the happy path, green on the empty input, green on the retry. Then it met real data for the first time, and it failed within the hour — not on a bug in the logic, but on a shape of data my tests had never shown it. The tests were not wrong. They were just too polite.
Real data is rude in three specific ways, and each one breaks a different assumption.
First, fields that are always present in fixtures and sometimes absent in production. My test rows all had a phone number, an email, and a company name, because I wrote them that way. Real rows had blank cells, missing keys, and a contact whose email field contained the words "ask Priya" instead of an address. A node that reads {{$json.email}} does not fail on a missing key — it passes an empty value downstream, and the send step either fails three nodes later or, worse, succeeds with garbage. The fix is a contract at the boundary: the first node after ingestion asserts which fields must exist and what shape they must have, and anything that fails the contract lands in review instead of flowing on. A workflow that cannot say "this row is malformed" will eventually say something worse to a customer.
Second, type drift: the same field arriving as a different type. A quantity that was a number in testing arrived as the string "12" from a form, and as "12 units" from a spreadsheet. A webhook that sent one item as an object sent two items as an array — so the single-item case skipped the loop entirely and the workflow processed nothing, successfully. n8n nodes coerce quietly: a string in a numeric comparison, an object where an array was expected, and the execution stays green while the result is wrong. The rule I use now: normalize at ingestion, never downstream. Convert, trim, and cast in one place, right after the trigger, and make every later node assume the normalized shape. If a value cannot be normalized, it is a review item, not a best effort.
Third, scale and content extremes that fixtures never include. A test sheet with ten rows hides the row with 4,000 characters of pasted HTML in a notes field, the name with an emoji that breaks a downstream CSV, and the duplicate submission that arrives twice within a minute. None of these are edge cases in production; they are Tuesday. My test data now comes from real payloads, redacted: I take a sample of actual inputs, strip the personal data, and keep the shapes — the blanks, the odd types, the long strings, the duplicates. A test that only uses data I invented tests my imagination, not my workflow.
The guard that belongs at the boundary
All three failures share one root cause: the workflow trusted its input. The guard is a small validation step that sits between the trigger and everything else, and it does three things. It checks required fields exist and match expected types. It normalizes what can be normalized — trims whitespace, casts numeric strings, wraps a lone object into a one-item array. And it quarantines what it cannot fix, with the original payload attached, instead of letting it flow downstream in a degraded form.
That quarantine step is the part most people skip, because it feels like giving up. It is the opposite: it is the workflow admitting what it does not know, in a place where a human can still act, instead of failing silently three steps later or succeeding with the wrong result. A malformed row in review is a Tuesday task. A malformed row that reached a customer is an apology.
Test with production shapes, not polite shapes
Before a workflow that touches real data goes live, I run it against a dataset built to embarrass it: a row with every optional field missing, a single item where the code expects a list, a list where it expects a single item, a value ten times longer than expected, a duplicate delivery, and a field that contains text where a number should be. I assert on counts, not just on success: the right number of rows processed, the right number quarantined, zero rows silently dropped. If any of those asserts fail, the workflow does not go live yet — it goes back to the boundary step until the contract holds.
The rule I keep coming back to: a workflow that passes only on data you invented has not been tested. It has been rehearsed. Production data will improvise, and the only question is whether your workflow meets the improvisation with a contract, or with a green execution log and a wrong result.
Free workflows from this series (MIT): https://github.com/zhp910318/n8n-ai-automation-lab
The production templates in my store: https://payhip.com/haopengailab