Running the gauntlet: how I released a fiddly n8n workflow without crossing my fingers
How a client-persona test loop took an early-stage startup's n8n waitlist automation from 18 failing checks to zero before release.
Originally published on XThere's a particular kind of nervous you get just before handing a client an automation that sends real emails to real people. The happy path works. You've clicked through it a dozen times. But you know, deep down, that the first week in the wild will throw up something you never thought to try.
I recently built a waitlist workflow in n8n for an early-stage startup. My client's founder would be the one putting it through its paces before launch. Candidates fill in a Typeform, join a queue, get a heads-up, get invited, reply, sign up, or don't. On paper it's deterministic. In practice it's a tangle of timing, state and edge cases.
So rather than just "test it a bit", I ran a Claude Code session using what I call a gauntlet loop. Here's what that looked like, and what it caught.
The idea: play the client, on the real engine
The plan in the session was four steps, and it's worth showing them as they appeared, because they are the method:
- Build a client-persona UAT runner on real n8n
- Gauntlet iteration 1: run UAT, triage findings
- Fix defects and re-run until clean
- Commit, push, publish UAT report
Two details matter here. First, the runner plays the client: it behaves the way the person accepting the work would, not the way the developer hopes they will. Second, it runs against the real n8n engine, with Airtable and SendGrid simulated. So the workflow logic is the genuine article, but nothing actually gets sent to anyone and no real data gets touched.
That combination is the bit I like. Unit tests tell you your functions do what you think. A persona running the real workflow tells you whether the thing does what the client thinks.
What the persona put it through
The runner simulated 12 days at an early-stage startup across 34 scenarios. Some were obvious, many weren't.
- Typeform: ten variants, including "All-Access" and "LES", messy email addresses, HTML in a name, and duplicate submissions.
- Candidate replies: every reply type, plus the irritating real-world stuff: email scanners pre-clicking links, double-clicks, three replies in the same second, late replies, and people changing their mind.
- Staff actions: Send Now, recounts, manual rank, a mistyped minimum, and a queue with no signup link.
- Email copy: checked against the client's approved templates.
- Whole-run integrity: checks across the entire run, not just individual steps.
If you've ever run a waitlist, you'll recognise that list. It's less "does the button work" and more "what happens when three things happen at once".
Five rounds, from 18 failing to none
The first full run came back with 111 checks passing and 18 failing. That's not a disaster; that's the point. Each round I'd triage what failed, fix the real defects, and run everything again.
After five rounds of test, fix, retest, it finished at 140 passing and 0 failing. I've deliberately not plotted the rounds in between; what matters is where it started and where it ended.
Here's a glimpse of what the middle of the session actually looked like. You can see the plan ticking along, and the loop in action: an experiment, a finding, a change, another iteration.
The eight defects it caught
Eight genuine defects were fixed along the way. None of them would have shown up if I'd just clicked through the happy path.
- A heads-up and an invite could reach the same person in the same minute.
- Someone who said "not ready", then clicked Sign up, was wrongly told their hold had lapsed.
- That same person was then stuck in a 72-hour cool-down, despite being told they were "first in line".
- A new member refilling the form went back onto the waitlist for the membership they'd just bought.
- The membership page link wasn't clickable.
- A "a Unlimited" typo in the tour alert.
- A fast double-click sent two confirmations and inflated the signup count in the report.
- The heads-up email now uses the client's wording, and the daily report moved to 7:30am.
Look at numbers two and three together. One small mind-change by a candidate led to a wrong message and a stuck state. That's exactly the kind of chain a persona-driven run finds and a quick demo doesn't.
The big one: Airtable's lockout
The most important finding wasn't on that list. It was about how Airtable behaves under a burst.
Airtable locks the base for 30 seconds once more than five requests arrive in a second, and n8n's retries give up after 20. With the simulated Airtable behaving that way, three people pressing Sign up together caused one signup silently lost and two raw error pages. Silent loss is the worst kind of bug: nobody complains, the person just never hears back.
The fix: saves now wait out the lockout and resend, and anyone caught in a burst sees a "try again in a minute" page which retries itself. All three sign up correctly.
This is where the loop really earned its keep, because the first fix wasn't the last. After 63/63 unit tests passed, I re-ran the lockout burst experiment against real n8n, with two "busy" candidates pressing Try again after 40 seconds. The fix held, and alert noise dropped from four alerts to just the expected hourly retry. I reworded that remaining alert to make clear it's a harmless temporary rate limit, then switched the UAT to a realistic lockout mode.
UAT iteration 3, with realistic Airtable lockout, then revealed something new: busy candidates retrying in sync re-trip the lockout. So the busy page now auto-retries after a random 35–55 second delay to stagger retries, and the harness was updated to mimic that. Then iteration 4. A deliberate collision test passed: Jo went straight through, Leo hit the busy page but recovered via the auto-retry, and there was no false alert.
One more useful note from the report: on self-hosted n8n, setting N8N_CONCURRENCY_PRODUCTION_LIMIT=1 removes these rate-limit collisions entirely.
Shipping it, and being honest about what's left
Before committing, there was a final regression sweep: the unit tests plus the mock simulation tool. And one small thing I'm glad got caught: a figure in the report was wrong. The unit-test baseline was 50, not 46, so that was corrected before publishing. If you're going to hand someone a report, the numbers in it need to be right.
The workflow is now ready for the client's hands-on testing. There are four product decisions still open, none of them blocking, each with a suggested way forward:
- "Not ready yet" check-back: list those people after 30 days in the daily report.
- Opt-out link in the heads-up email: launch with "reply to be removed".
- Daily report layout: switch to the client's template.
- Mistyped minimum: typing 500 instead of 50 keeps sending ten invites an hour until the list runs out, so pause the queue and email the client instead.
That last one is my favourite kind of finding. It isn't a bug, exactly; the workflow is doing what it was told. But it's a decision the client should make with eyes open, not discover on a Tuesday morning.
Why I'll keep doing it this way
The gauntlet loop isn't complicated. Build something that behaves like your client, point it at the real engine, keep the outside world simulated, and go round until it's clean. Then ship, and publish what you found.
What it changes is the conversation. Instead of "I think it's ready", I can hand my client's founder a report that says: here's what we threw at it, here's what broke, here's what we fixed, and here are the four calls that are yours to make. Nothing was actually sent to anyone along the way. That's a much better place to start hands-on testing from than crossed fingers.