The costly mistake is emailing the wrong person, not sending too little.
The operation sends from 125 mailboxes on behalf of senior partners. Its outbound workflow was spread across a lead database, an enrichment tool, a sequencer, a CRM, every partner's calendar, and a call recorder. Each tool did its own job well. None of them could answer the question that mattered: should this person be emailed at all?
The failures that hurt were never about volume. They were things like a cold pitch landing on the colleague of someone mid-negotiation, or a prospect who said “try me in March” and never heard back. Sometimes an unsubscribe reached the sequencer but never reached the CRM, so the next campaign emailed them again. And a booked call might exist only in one partner's calendar, where nobody chased the no-show.
What makes this a hard workflow to hand to an agent is that the rules interact. Whether to send depends on deal state, correspondence history, a parked follow-up date, and whether the address is deliverable. That information lives in different systems and arrives out of order.
A working build on the real stack, run end to end in a live client demo, not a production deployment. The logic is real. The hosting is still a local scheduler and a temporary webhook tunnel. It was demonstrated on a live offer: geospatial site selection sold to energy developers.
Write the rules down before letting an agent follow them
The usual way to build an outbound agent is to wire the tools together and add guardrails later. We did it the other way around. We wrote down every rule for who may be contacted and why, then picked and connected tools so that those rules could not be skipped.
Before any code, six rule files: who may be contacted, how replies are routed, what each system owns, and what lands on a deal. The specs are the source of truth — the code implements them and the tests assert them. When the two disagree, the code is wrong.
Every vendor was judged on its failure modes, not its feature list. One sequencer was dropped because it has no unsubscribe webhook, so an opt-out could never reach the CRM. The one we chose tends to stop sending silently, so the build writes a last-sent date on every email and that failure shows up within hours.
Two suppression passes, reply routing, and a single sending client that refuses any lead without a gate token. Every block carries exactly one reason code, so an operator can answer why a given person was or wasn't emailed.
Calendars, recordings, and briefs sync back to the CRM on a 15-minute cycle. A booked call gets an automatic research brief. A recorded call gets a summary, action items as tasks, and a proposal clock. None of it is typed by a person.
Eleven stages, from a lead list to a briefed call
Each stage has exactly one owner. Most are deterministic code or a single tool. The two gold rows are suppression gates, and they sit right before the two steps that spend something: money on enrichment, and reputation on sending. A model appears in only one place, and it reads a reply only after the rules have run.
Six design decisions that make the agent trustworthy
In each case, the obvious build works in a demo and fails in month three. The correct build takes only slightly more work.
Code decides. A model only labels.
Suppression, dedupe, state, and routing have to give the same answer hundreds of times a day, so they are code. The model reads a human reply and returns a verdict. Separate code then performs every write. The model cannot create a deal, stop a sequence, or add anyone to the block list. It can decide on a label. It never takes an action.
Suppress before you spend, and do it twice
The lead source gives names without reliable emails, so finding an email is a cost worth protecting. Pass A runs at the company level, before enrichment, for free. Pass B runs on the address before anything is drafted. Pass A's open-deal rule blocks everyone at that company. Cold-pitching a colleague of someone you are mid-negotiation with is the most damaging thing this system prevents.
A rule you must remember to call eventually isn't called
So the sending client refuses any lead that arrives without a gate token. It is enforced, not advised. The check had already been skipped once, in a test harness, on leads that happened to be safe. It will not be skipped on leads that aren't.
Never guess into a permanent suppression
“Not interested” writes to a global block list that only a person can undo, so any ambiguity resolves to “not now.” The asymmetry is deliberate: a wrong “not now” costs one follow-up, and a wrong “not interested” loses a prospect for good. But “remove me” is not ambiguous, and it is honored at once.
Read calendars, not booking tools
A recorder only sees calls that happened. The calendar also knows the calls that are booked, declined, or never attended, and those are the ones someone has to chase. One calendar API holds every rep's mailbox against a single CRM seat. Swap the scheduling tool tomorrow and nothing breaks.
Only observable facts move a deal on their own
“Call done” is the only stage the system sets by itself, because a recording existing is a fact. “Proposal sent” is a claim only a person can make. Outreach status also only moves forward. Webhooks arrive out of order, and without that guard a late send event would downgrade someone who has already replied.
“Why didn't we email them?” always has one answer
The rules are checked in order and the first match wins. Every block carries exactly one reason code, because an operator needs the reason, not a list of all of them. Each code maps to a sentence a partner can act on.
“We are mid-negotiation with a colleague at their company.”
Pass A · re-checked in pass B“A partner already has an open email thread with them.”
Pass B“They were in the Q2 campaign.”
Pass A · pass B“They asked us to circle back in March, and in March they come back.”
Pass B“We emailed them within the last 90 days.”
Pass B“The address wouldn't have reached a person.”
Pass BThe details that took real debugging
None of these show up in a happy-path demo. All of them show up in the first month of live sending.
The CRM creates person records from two sources that mean different things. One is our pipeline. The other is a partner's email capture. Both block, but with different codes, so the answer to “why not?” is useful. Merging them into one rule would quietly change its meaning the day mail capture is switched on.
A follow-up date has to gate the block, not just annotate it. Without that, a lead saved for March is still blocked in March, and “circle back in Q1” never happens.
Free-email domains never become company records. A deal attached to “gmail.com” would make the open-deal rule block every Gmail user alive.
Pause, edit, resume. Editing mid-flight re-sent the sequence to every lead it had already contacted. That lesson is now written into the operating rules.
The model reads a reply last, not first
Most replies do not need judgment. Bounces, out-of-office notices, and “you want my colleague” all have deterministic answers, so rules handle them in order. A language model sees only a real reply from the person we emailed, and even then it returns a label. It never takes the action itself.
The address goes on the block list and the sequence stops. No CRM record is created.
Resume the day after their out-of-office ends, or retry in seven days.
A referral becomes a new lead at the same company, with the original thread attached.
Only now does a model read it and return interested, not now, or not interested.
What it takes to hand a messy workflow to an agent
Specific proof points across architecture, controls, and operations. These are the parts that decide whether an agent can be trusted with a revenue workflow.
An 11-stage pipeline from lead list to booked, briefed call. Deterministic code owns every effect, and language models are confined to the three places judgment is needed: classifying replies, drafting, and writing pre-call briefs.
Six rule files are the deliverable even if nothing runs. The code implements them, the tests assert them, and every suppression rule passes its spec test.
Clay, Smartlead, Attio, Nylas, and Fathom are joined by one sync layer. Each system owns exactly one kind of fact, no fact is written twice, and when two systems disagree the owner wins.
Two suppression passes with a single reason code per block. The gate is enforced at the sending client, and unsubscribes are wired all the way back to the CRM.
A three-way reply classifier that leans toward the recoverable answer, plus automatic pre-call briefs built from live web research and linked to the deal.
A one-command health check across the whole stack, an append-only event log with idempotency, and last-sent tracking that catches a sequencer silently not sending.
What we built
- Six specification files — the source of truth
- Two-pass suppression with single reason codes
- Reply routing: rules first, model last
- CRM schema, built and corrected idempotently
- Sync layer across Clay, Smartlead, Attio, Nylas, Fathom
- Reply classifier with a verdict-only contract
- Automatic pre-call briefs with live research
- Calendar and recording sync into CRM tasks
- One-command preflight health check
- 15-minute cycle: calendars, recordings, briefs
- Realistic, fully removable demo pipeline
- Bug log and open decisions for the client
An agent you can audit, not just an agent that sends
A lead list goes in and a booked, briefed call comes out. Every step in between is either a rule anyone can read or a label from a model that is not allowed to act on it. When a partner asks why a prospect was skipped, why a reply was filed where it was, or why a deal moved, the answer is already written on the record.
We were just as clear about the limits. Enrichment finds about 83% of work emails, so roughly one lead in six can't be reached. Sending to inboxes we own proves the plumbing works, not where messages land. The build shipped with a bug log and a short list of decisions only the client can make, such as what formally closes a dead deal. That is how an agent reaches production without surprising anyone.