⌫ atlas

How I Run Hundreds of Personalized Cold Emails Through One Claude Code Session

2026-04-27Firat Dogan

Most outbound tools optimize the last ten percent: the sentence.

I optimize who earns a sentence at all.

My current system takes an Apollo list, researches every contact with Exa, scores the evidence, skips weak fits, writes the personalized parts of the email, routes them through human review, and pushes only approved fields back to Apollo.

One Claude Code session owns the batch.

It can process hundreds of contacts. The system produces tens of calls a week for me now.

That is not a promise about your results. List quality, offer, timing, and follow-up still matter. The point is that research and judgment can be turned into a repeatable system instead of a tab-opening ritual.

This tutorial shows the system I use today, including parts of my current Roster build that are not released yet.

What the system actually produces

I do not ask a model to freestyle hundreds of complete emails.

Most of the leverage sits in four decisions:

  1. Is this person worth contacting?
  2. Is there a real reason to contact them now?
  3. Which outbound play fits that reason?
  4. What specific evidence should the opening use?

The system produces a small packet for each contact:

  • a fit tier;
  • a set of claims with source URLs and dates;
  • a recommended play;
  • a subject line;
  • a short personalized opener;
  • a review status;
  • enough metadata to reconstruct the decision later.

The body and CTA come from the selected play. The subject and opener carry the contact-specific evidence.

The session does not send email. Sending stays downstream of an explicit approval.

The stack

Claude Code

Claude Code is the orchestrator.

It reads the runbook, calls the APIs, keeps one isolated research task per contact, enforces the output schema, pauses for review, and writes approved results back.

"One session" does not mean one giant prompt. It means one session owns the state and definition of done for the entire batch.

Apollo

Apollo is both the contact source and the output destination.

I start with a saved list. The system fetches its contacts, deduplicates them, ignores records without the required fields, and later writes approved subjects, openers, research notes, and priority tiers back to custom fields.

Exa

Exa replaced Sonar in this workflow.

I use Search for discovery and Contents when I already have the URLs. Search can return highlights with the result, which lets the research task collect the evidence and the page context in one call. I also control freshness instead of treating every cached page as current.

The relevant official references are the Search API, Search guide, Contents guide, and JavaScript SDK.

Roster, Roster brain, and the run ledger

This is the new part.

My runbook now lives as a Roster plan. Roster brain stores the durable evidence: approved copy, corrections, source-backed facts, and examples that should affect later runs.

The current unreleased persistence layer adds three useful things:

  • a human-in-the-loop state machine that binds approval to the exact draft version;
  • a run and artifact ledger that makes a batch reconstructable across machines;
  • a Postgres and S3 boundary, so searchable facts and large raw artifacts do not get collapsed into one Markdown file.

You do not need Roster to copy the workflow. JSON files and a review spreadsheet can implement the same contracts. Roster removes the parts I was rebuilding around every agent.

Slack

Slack is optional.

I use it for batch-ready and batch-failed notifications. Approval still happens in the task that started the run, where the evidence and exact draft are visible.

Phase 1: prepare the list

The first run starts before Claude Code.

A bad list creates fake work. No research model fixes a list full of irrelevant companies, duplicate people, stale roles, or missing domains.

My minimum input is:

  • contact ID;
  • first and last name;
  • current title;
  • company name;
  • company domain;
  • work email or a stable contact identifier;
  • optional profile URL.

The fetch phase normalizes company domains, deduplicates contacts, and creates one record per person.

It also makes reruns safe. Every output is keyed to the Apollo contact ID and the batch ID. If the process stops halfway through, the next run resumes incomplete records instead of starting over or writing duplicates.

Phase 2: research with Exa

Each contact gets an isolated research task.

The task runs three to five targeted searches. A single generic search is not enough. I split the work into four branches.

1. Company trajectory

Is the company growing, hiring, launching, expanding, raising, partnering, or changing direction?

This creates the business context.

2. Role context

What does this person appear to own now?

I look for official bios, team pages, interviews, talks, authored posts, job changes, and other sources that clarify responsibility. A title alone is weak evidence.

3. Trigger

Why now?

A recent role change, product launch, hiring push, market move, or public statement can create timing. Without a trigger, the email needs a stronger fit case.

4. Personalization anchor

What specific fact can support an opening without turning into fake praise?

The anchor must be relevant to the outbound play. A marathon result or university affiliation is not useful merely because it is personal.

Here is the shape of a simple Exa call:

import Exa from "exa-js";

const exa = new Exa(process.env.EXA_API_KEY);

const results = await exa.search(query, {
  type: "auto",
  numResults: 5,
  includeDomains: preferredDomains,
  contents: {
    highlights: true,
    maxAgeHours: 24,
  },
});

type: "auto" lets Exa select the search strategy. includeDomains is useful when a branch should prefer first-party sources. maxAgeHours controls when cached content is acceptable; I use a shorter window for time-sensitive triggers and a longer one for durable background.

For every useful claim, I store:

{
  "claim": "The company launched its enterprise product this month.",
  "url": "https://example.com/source",
  "title": "Source title",
  "publishedAt": "2026-07-18",
  "checkedAt": "2026-07-29T09:30:00Z"
}

No source means no claim.

Search results are not truth. They are candidates for evidence. The task still checks that the page supports the claim, the person and company are the right entities, and the date is relevant.

Phase 3: qualify before writing

This is the step most outbound systems skip.

They assume every row deserves copy because every row costs money.

Mine does the opposite. Research is allowed to reject the contact.

I use three tiers:

  • HIGH: strong ICP fit, relevant authority, a credible reason now, and source-backed evidence;
  • MEDIUM: plausible fit with usable evidence, but weak timing or incomplete authority;
  • LOW: outside the ICP, no meaningful trigger, weak evidence, or no credible reason to write.

LOW contacts get skipped.

A workable numeric rubric is:

  • ICP fit: 0–40;
  • role and authority: 0–25;
  • recent trigger: 0–25;
  • evidence quality: 0–10.

Then HIGH is 75 or above, MEDIUM is 55–74, and LOW is below 55.

The numbers are not sacred. The refusal rule is.

There are also hard failures:

  • the role is stale;
  • the company is outside the market;
  • the contact is already active in another sequence;
  • the research task cannot support its main claim;
  • the address or contact identity cannot be trusted.

A contact that fails a hard rule never reaches generation.

Phase 4: select the play

Personalization without a point is trivia.

Before writing, the system selects one outbound play.

My common plays are:

Role transition

Use when someone has recently taken responsibility for a relevant function.

The opening connects the new scope to a problem they are likely inheriting. It does not congratulate them for three sentences.

Company momentum

Use when the company is hiring, launching, expanding, or changing its go-to-market motion.

The opening connects that movement to a likely operating constraint.

Public intent

Use when the person has written or spoken about the exact problem.

The opening references the idea, then advances it. It should not summarize their own post back to them.

Credible peer context

Use when there is a real shared context, comparable company, or relevant mutual connection.

The evidence has to earn the comparison. Name-dropping is not a play.

The play controls the body and CTA. The research controls whether the play applies and how the email opens.

Phase 5: generate the subject and opener

Generation is deliberately constrained.

My current limits are:

  • subject: 80 characters maximum;
  • opener: 180 characters maximum;
  • one evidence-backed idea;
  • no invented causal claims;
  • no unsupported praise;
  • no generic compliment that could apply to twenty people.

I also reject phrases that usually signal machine-written outreach:

  • "I came across your profile";
  • "I was impressed by";
  • "I noticed that";
  • "quick question";
  • "hope you're well."

The exact phrase list matters less than the reason for it. The system should write from evidence, not perform the appearance of research.

My generation instruction is close to this:

Write a subject and opener only.

Use one supported claim from the evidence packet.
Connect it to the selected outbound play.
Do not praise the contact.
Do not invent a problem, result, or causal link.
If the evidence cannot support a relevant opening, return SKIP.

Subject: <= 80 characters
Opener: <= 180 characters

I track recent subjects and opening structures inside the batch. If a pattern repeats too often, the draft fails the variety check and gets rewritten.

Structural variety is not a license to become clever. It prevents a batch from sounding stamped out while keeping every email clear.

Phase 6: human review

The system stops when the review queue is ready.

For each contact, I see:

  • name, company, and role;
  • fit tier and scoring reason;
  • the exact evidence used;
  • source links and dates;
  • selected play;
  • subject and opener;
  • approve, edit, or skip.

The reviewer should not need to repeat the research.

This is where the newer Roster HITL layer helps. Approval binds to the exact draft and evidence packet. If the draft changes after approval, the approval no longer applies. Execution approvals can also expire.

That sounds strict until an agent is operating across hundreds of rows. Then it becomes the minimum sensible contract.

Edits are not cleanup. They are training data.

I keep the original draft, the approved version, the diff, and the evidence that was visible when the change was made.

Phase 7: write approved fields back

After approval, Claude Code updates Apollo.

I write:

  • approved subject;
  • approved opener;
  • fit tier;
  • selected play;
  • research summary;
  • source URLs;
  • batch ID and approval state.

HIGH and MEDIUM contacts can enter different sequences or priorities.

Every write is idempotent. The batch ledger records the intended write, the Apollo response, and any failure. A retry should repair missing writes, not create a second version of the contact.

The session still does not send. It prepares an approved, inspectable packet for the sending system.

The reinforcement loop

The first version of this workflow had one obvious weakness.

Every batch was smart. The next batch was not much smarter.

That is what Roster brain changed.

After a run, I store three different kinds of evidence:

Research evidence

Raw pages, result payloads, and larger artifacts live in S3. Source-backed facts and compact searchable records live in Postgres.

The next run can retrieve prior research without pretending old evidence is still fresh.

Authoring evidence

My approved final copy and explicit corrections teach the system how I write.

These examples affect voice and structure.

Performance evidence

Replies, positive replies, booked calls, and downstream outcomes teach the system which list, signal, play, and timing combinations worked.

Performance evidence does not automatically become voice evidence. An awkward email can get a reply. A clean email can fail because the offer was wrong. Collapsing those facts creates the wrong lesson.

The run ledger connects the final email to its list, evidence, play, approval, and outcome. That lets me improve the system without guessing which version produced the result.

The runbook

The runbook is the product.

The model can change. The contracts should survive it.

Mine defines:

  • the Apollo list and required fields;
  • allowed research branches;
  • source and freshness rules;
  • fit tiers and hard failures;
  • output schema;
  • play library;
  • copy limits and banned patterns;
  • review requirements;
  • write-back fields;
  • completion and failure conditions.

A session instruction can be this simple:

Run the cold-outreach plan for APOLLO_LIST_ID.

Fetch and deduplicate the list.
Research each valid contact with Exa.
Store every claim with its source.
Score fit before generation.
Skip LOW and every hard failure.
Select one approved play for HIGH and MEDIUM contacts.
Generate only the subject and opener.
Build the review queue, then stop.

Do not write to Apollo or send email until I approve the exact batch version.
Do not stop early because some contacts fail.
The run is complete when every contact is approved, skipped, or failed with a reason.

That final line matters.

"Process the list" is vague. "Every contact has a terminal state" is a definition of done.

What I measure

I do not use volume as the main score.

I watch:

  • rejection rate before generation;
  • percentage of drafts that survive human review;
  • edit distance between draft and approved copy;
  • positive replies by fit tier and play;
  • calls booked by fit tier and play;
  • stale or unsupported claims caught in review;
  • failed or duplicate writes.

Open rate is diagnostic at best. It does not tell me whether the list, reason, or offer was good.

The system currently creates tens of calls a week for me. The useful question is not whether AI can write faster. It can.

The useful question is whether the system can make a defensible decision about every person, preserve the evidence behind that decision, and learn from what I changed.

Writing was never the bottleneck.

Research was.

Now the bottleneck is judgment, which is exactly where I want the human.