← All projects
OutboundAgents

Outbound rebuild

When I took over our outbound, the system ran fine and wasn't producing. Most of the replies were auto-responders, and reading back through what we'd sent, nearly every email made the same claim. So I rebuilt the research, the writing and the sequence.

Summer–Fall 2026·Python, Claude, Instantly, HeyReach
01Where I started

Ask the research a writing question

The old research step decided whether a company was a fit, so by the time we wrote anything we had a yes and almost nothing to say. I kept the crawler and changed the question to the three things that would make them a fit, each with a quote and a link, plus one human detail somebody would bring up in conversation.

interview.py (the question)
--- fit_angles ---
**The top three things that would make them a fit for Sensible
that we should mention.** Open ended: whatever you would actually
raise with this company. One entry each, each with its own quote
and URL. Do not pad. Rank them best first.

--- human_detail ---
The single most specific, noticeable thing on the site that a
person would genuinely mention to another person.
02Writing

A plain version first, then a rewrite

A template writes a plain email that's already fine to send. A stronger model rewrites it for voice using emails I liked as examples, and then the two get compared. If the rewrite added a name or a number that wasn't in the plain version, the plain one goes out.

Template first, then a rewrite for voice
To: Dana, Northwind ImagingSubject: radiographs into claims
A sample with a made-up prospect.
03The check

The check is plain code

The comparison isn't another model. It pulls the names and numbers out of both versions and flags anything new, using a dictionary so a normal word at the start of a sentence doesn't get reported as a made-up company.

gen_sequences.py
def drift(new, bones, card):
    """Anything factual the humanising pass added."""
    old_n, old_v = facts_of(" ".join(bones[k] for k in KEYS))
    new_n, new_v = facts_of(" ".join(new.get(k) or "" for k in KEYS))
    added_names = sorted(n for n in new_n - old_n - safe
                         if not _known(n.lower()))
    added_nums = sorted(new_v - old_v)
    if added_names:
        return "rewrite invented names: " + ", ".join(added_names[:4])
    if added_nums:
        return "rewrite invented figures: " + ", ".join(added_nums[:4])
    return None
04Reviewing

I read every email before it goes

Each wave goes onto one page with the research next to every draft, and I approve, edit or skip each one. My notes feed the next round, which is where most of the improvement came from. Pasting in whole emails I liked worked a lot better than writing rules about them.

Outbound · wave review
The wave review page
The real review page. The prospect is made up and the quotes are paraphrased.
05Sequencing

One email, then short replies on the same thread

All the personalization goes in the first email and the follow-ups are short replies on the same thread. While setting that up I found that every follow-up we'd ever sent had been starting a brand new thread, because of a subject line setting nobody had noticed.

Outbound · sequence
The back half of a sequence
Where it is now

Each wave goes out after I read it, with the research sitting next to every draft.