AI systems · Automation guardrails

We almost let our own automation cold-email its own founder

The fix was supposed to be a one-line merge. Looking at the actual rows before running it found a FOIA desk, two charities, and the founder's own inbox sitting in the send queue.

WritingBy Landon LittleSeptember 25, 20265 min read

The plan was to merge the backlog forward

We were in the middle of repairing a broken cold-email queue on our own outreach pipeline. Part of the fix meant a backlog of contacts, stranded by the earlier bug, needed to rejoin the working pipeline. The plan, as written, was simple: merge the stranded backlog forward.

A merge like that is easy to trust. The rows had been collected before, the fields lined up, and the fix elsewhere was already done. Nothing about the plan looked risky on paper: it was a data operation, not new code, and data operations tend to get less scrutiny than the code around them.

The tempting move was to run it and move on. We did not do that. Before merging anything, we opened the actual rows and read them, the same way we would read the diff on a code change before shipping it.

What was actually sitting in the backlog

The list was not what a clean prospect backlog should look like. It contained a federal .gov address belonging to a FOIA desk, two charity addresses, an immigration legal-aid nonprofit, a support inbox for a hosting platform, and a placeholder address left behind by a website-builder tool.

None of these rows were ever real prospects. They were the accumulated residue of however the original list had been built over time: a scrape, a form submission, a support ticket, an address typed into the wrong field once and never corrected. A list assembled this way does not announce which rows are bad. It just sits there looking like the rest of the data, until someone reads it closely enough to notice.

A merge treats every row the same. It has no concept of “this one is a government agency” or “this one is us.” Only a human looking at the actual data notices that some of those rows do not belong in an outreach queue at all, which is exactly why the step of looking cannot be automated away along with everything else.

A permanent filter, then the real migration

Instead of merging the backlog as it was, we built a junk filter first: rules to catch government and military domains, our own company's own domains, known platform-support domains, and common nonprofit signals. Only after the filter existed did we run the actual migration.

The 2026-07-29 backlog migration, after the junk filter was in place.
Backlog rowsJunk (filtered out)Already knownClean, migrated
163733123

Seven of 163 rows were junk by the filter's own rules. Thirty-three were contacts already known elsewhere in the pipeline. The remaining 123 were genuinely clean and migrated forward. The filter did not just catch the founder's own address. It caught six other rows an automation should never have been allowed to touch on its own.

The cheaper version of the same habit: look before you act

Reading 163 rows by hand does not scale, and it was never meant to be the permanent answer. The filter is the durable version of the same instinct: instead of a human reading every row every time, a fixed set of rules reads them automatically and refuses to pass through anything that matches a known-bad pattern, before a single message goes out.

The same habit shows up in a plainer form as a dry run: a mode where an automation reports exactly what it would do, to whom, and how many times, without actually doing it. A dry run over this backlog would have printed the founder's own address as a send target in plain text, which is a much cheaper way to catch the problem than finding out after the fact. Any automation that touches real addresses, real accounts, or real money is worth running once in a mode that only reports, before it is ever allowed to act.

Guardrails matter as much as retries

Most of the engineering attention that goes into an automation goes into making it run reliably: retries, error handling, logging, alerts when something fails. All of that matters. None of it would have caught this. The problem here was not that the automation broke. It was that it was about to work exactly as designed, on data nobody had actually looked at.

An automation inherits every defect already present in its inputs. “The list”, whatever list it is, almost always contains something you would be embarrassed to act on automatically, because it was assembled over time by processes that were never built to guarantee it was clean. A gate that checks who an automation is about to touch, before it touches them, is not optional polish. It is as load-bearing as the retry logic everyone remembers to build.

We keep this filter in place permanently now, on every list that feeds this pipeline, not just the one that happened to catch a founder's own inbox. The cost is a few seconds of automated checking before anything sends. The alternative, on a bad day, is an automation doing exactly what it was told to do, to exactly the wrong address.

This is the same standard we hold any automation to, ours or one we build for someone else: before it runs unattended at volume, someone has actually looked at what it would touch, and a permanent check stands in that person's place going forward. A list is never as clean as it looks from the outside, and the automation running against it will not notice the difference on its own.

Questions this post answers

Why does an automated contact list need a human to look at it before it runs?
Because an automation inherits every defect already sitting in its input, and a list built up over time will always contain rows you would be embarrassed to act on automatically. In our case that meant a government address, charity contacts, and even the founder's own email, all queued for the same automated send.
What kind of "junk" ends up in a cold-outreach list?
Addresses that were never real prospects to begin with: government and institutional domains, nonprofit and charity contacts, platform support inboxes, placeholder addresses left over from tools like website builders, and occasionally an internal address that got swept in by accident.
What does a junk filter for an outreach automation actually check?
Ours checks for gov and mil domains, our own company domains, known platform-support domains, and common nonprofit signals, and excludes any row that matches before it ever reaches the send queue. It runs before volume, not after a complaint.
Does inspecting the data before automating it slow things down?
It costs the time it takes to look. In our case that was one migration pass across 163 rows before anything moved. A dry run that prints what an automation would do, without doing it, costs the same small amount of time and catches the same problems before they become an actual send.

Want this working for your business?

We build the automation your team keeps meaning to build, then hand it over running. Book a call and we will map the first working slice.

Book a 20-minute call

Pick a time that works. Twenty minutes on video, no pitch. You leave knowing whether this is worth doing.