Skip to content
Aetenum

The customer said no. The automation kept going for another year.

Every rule passed. The copy was approved. Nothing in the system was broken. That is exactly why it happened.

9 min read Scott Watson

If your follow-up automation is still messaging someone who has already told you no, the reason is almost certainly that every check you have runs on timestamps rather than on words. Timestamp rules cannot detect a customer who has given up, because giving up looks identical to silence, and silence is the exact condition your sequence was built to act on. The fix is a third kind of check: something that reads the whole thread before each send and answers one question. Given everything that has happened in this conversation, does sending this particular message, to this particular person, right now, make sense?

I built one of these this year for a home services company. Here is what prompted it and what it took.

What happened

A customer wrote, on a Friday: “We decided to go another route. Thanks.”

Someone at the company answered him twenty six minutes later, kindly, and closed it out properly.

On the Sunday, the automated sequence emailed him his city’s water quality report. Behind that email, twenty six further touches were still queued at him, scheduled out to September of the following year.

The company found out because I went looking. He did not complain. People rarely do. They just remember.

Nothing was broken

This is the part worth sitting with, because it is the reason the problem is common. Every component did exactly what it was built to do.

The conversation responder skipped his message, correctly. A human had replied inside the half hour that a human gets first, so the assistant stood down. But those guards run before the classifier, for good reasons of cost and latency. Which means nothing in the system ever read the words “we decided to go another route.”

The scheduling preflight approved every step behind it. It works on timestamps, and it had one: the inbound message was recorded as the most recent customer contact. The rule said, in effect, this lead is warm because they wrote to us recently. That rule would have returned “send” forever, because a customer who has given up never writes again, so the timestamp it depends on never advances. The signal it was reading gets permanently stuck at the exact moment the relationship ended.

The copy was fine. Every claim in the email was on the approved list. A copy checker would have passed it, correctly, because there was nothing wrong with the email. There was something wrong with sending it.

Two checks, and neither could ask the only question that mattered. One reasons about time and has never read a word a customer wrote. The other reads words, but only ever looks at a message the assistant itself just drafted in reply to something.

The third check

So the system needed a check whose entire job was the question a person would ask if they read the thread before pressing send.

Mechanically it is simple. Every automated send goes through one function. That function pulls the message thread and the lead record, hands both to a model along with the message about to go out, and asks for a verdict. If the verdict is no, nothing sends.

The interesting parts are all in the details around it.

The hard part is what it must not block

A gate that stops good messages is worse than no gate at all, because it gets switched off inside a fortnight and then you have neither the gate nor the trust. I put that reasoning into the judge’s instructions in as many words.

It stops a message when the thread shows the customer has said no, has already received whatever the message is offering, has an unresolved complaint sitting unanswered, or when sending would talk over a colleague or contradict something already said.

It lets a message go when the customer simply has not replied. Silence is the normal case. The entire sequence exists for silence, and a gate that treats quiet as a reason to stop has misunderstood what it is guarding.

Before switching it on I ran it against the real backlog. Of the first fourteen pending steps it would have sent ten and blocked four. I checked all four by hand. Every one was right, and two were already sitting on a known-issues list that nobody had got to.

That ratio matters more than the block rate on its own. What a gate blocks is only meaningful next to what it lets through, which is why the system records the approvals as well as the refusals.

Blocking is not canceling

When it stops a message, three things happen.

The message does not send. Nothing reaches the customer.

The lead is paused, not canceled. Every other queued step for that person is held, and nothing is deleted. This distinction is load bearing. The judge is allowed to stop messages. It is not allowed to throw away fourteen months of somebody’s carefully built follow-up on the strength of one model call.

A person gets a text, immediately, whatever the hour. Not an alert saying something was blocked, which is useless at two in the morning. It says who it was about, what they said and when, what was stopped, and what to actually do about it, as numbered steps. It also says how many other queued touches are now paused, because that number is usually the thing that makes somebody act.

Clearing the alarm releases the lead and the sequence carries on from where it stood. Deliberately the same gesture in the same place, rather than a second button on another screen. A block that can only be released somewhere nobody looks is how a lead sits frozen for a month.

One text per contact per day, however many things it stops for them.

People are not gated

A person pressing send is never checked. Somebody looking at the thread knows things the judge does not: what was said on a call, what the spouse asked at the door, what the neighbour told them.

Internal alerts and digests are not customer messages, so they are not gated either. Half of what they report is the gate itself, and a gate that can block its own alarms is a system that goes quiet exactly when it is doing the most work.

Call tasks are not gated. Nothing reaches the customer, and a person reads the task before acting on it.

Making it impossible to bypass by accident

This is the part I would most encourage you to copy, because it is the difference between a control that works and one that quietly stops working in four months.

Before the gate, there were eighteen places in the codebase that could send a customer message, spread across twelve modules. Reading any one of them told you nothing about whether the message had been checked. Adding a nineteenth that skipped every check would have been a one line change that no reviewer would notice in a diff.

So the send function will now not compile unless the caller names who cleared the message. There are exactly three ways to produce that clearance: the gate itself, an internal-alarm marker, or an explicit human-send marker. A test walks the whole codebase and fails the build if any module sends without a clearance, or if any module that is not the gate tries to mint a gate clearance.

The point is not that the test is clever. The point is that bypassing the gate is now a deliberate act that shows up in review, rather than an oversight.

It fails closed

If the model cannot be reached, the message does not send.

A null verdict is not an approval. The last check in the chain must not quietly become the first one to guess. The step is not canceled and nothing is lost, so it comes round again on the next run once the API is answering.

This is the one place I would push back on most people’s instincts. The temptation is to let messages through when the checker is down, because blocking feels like an outage. It is not an outage. It is a delayed message, and a delayed message costs you close to nothing next to the one you cannot unsend.

What it costs

Roughly 3,800 input and 520 output tokens per message on a frontier model, which is about three cents a send. The company sends around sixty automated messages a month, so the gate costs a couple of dollars a month.

I used the expensive model on purpose. This is the last thing standing between a scheduled message and a customer who has already said no, and a shallow read of a long thread is precisely how “we decided to go another route” gets skimmed past a second time. Verdicts are cached briefly against an unchanged thread so a retry inside one run does not pay twice.

If you are weighing three cents against the cost of one customer telling people you kept emailing them after they said no, the arithmetic is not close.

If you are going to build one

Three things I would do again.

Build the rehearsal mode first. Mine reads live data and the model but writes nothing, sends nothing, and runs before the database migration has even been applied. It prints the exact text alert you would have received for anything it stops. That is what makes it possible to look at your real backlog and form a judgment before anything is switched on, which in turn is what gets it switched on at all.

Have a shadow mode, and store shadow verdicts separately. Everything gets checked and recorded, nothing gets blocked. Keeping those rows under their own verdict means a week of shadow running can never be miscounted later as a week of clean sends.

Make “off” noisy. Turning the gate off logs a warning on every single send. An application dispatching unchecked messages to customers should never be a quiet state that somebody can leave running after an incident and forget about.

The general version

There were 446 pending email and SMS steps queued at that company when I looked, most of them scheduled weeks earlier against conversations that had moved on since. That is not unusual. It is what every drip campaign looks like underneath.

Automation does what you configured it to do, at the moment it was configured, forever. The configuration was written when the relationship was in a different state, and nothing in a normal system ever revisits that assumption. Rules that reason about time cannot help, because the most important thing a customer ever does is stop talking to you, and that is invisible to a timestamp.

You do not need a model to send messages. That part has worked for twenty years. You need one to decide whether a message should still go, and that is a question you can only answer by reading what actually happened.

Written by Scott Watson, who runs Aetenum. One senior developer on a monthly subscription, building one thing at a time until it is done. See how that works.

If this describes something you are living with

Book thirty minutes. Describe what broke and what it is costing you, and I will tell you whether a seat is the right answer for it.

Or reach me directly: +1 385-208-1781 scott@aetenum.com

If you are comparing quotes on price, I am not your cheapest option and I will not pretend otherwise.