Most "AI cold email" advice is the old spray-and-pray approach with a language model bolted on: scrape a list, have a model write a "personal" opener for every row, and blast it out. That gets you spam complaints, a burned domain, and, depending on where your recipients are, legal exposure. An earlier version of this article quoted response rates and meeting counts we can't support, so we've replaced it with a workflow and no invented results.
Here's the version worth building. You decide who's a genuine fit and why. AI helps with the slow parts: summarizing public information about a company, drafting an email that uses one real, verifiable reason to get in touch, and sorting replies. Code enforces the rules, so nothing goes out with an invented fact, a missing opt-out, or a fourth follow-up. Your reply rate depends on your offer, your list, and your market, so measure your own instead of trusting anyone's headline number, including ours.

What "personalized" should mean
Personalization isn't the recipient's first name and a compliment about their LinkedIn post. A useful cold email answers three questions in a few sentences:
- Why them? A specific, true observation, such as a job posting, a product launch, or a problem their public docs or reviews describe.
- Why you? One concrete way you help with that exact situation.
- What next? A small, easy ask, like a reply, a short call, or permission to send something.
AI is good at the mechanics here: reading a company's careers page and summarizing what they're hiring for, or turning your notes into a short, plain draft. It's bad at knowing whether the observation is true and current, and it will invent one if you let it. The whole system below is designed around that weakness.
| Task | AI's role |
|---|---|
| Summarizing a company's public pages you supply | Strong |
| Drafting a short email from a verified research note | Strong, with review |
| Classifying replies (interested, not now, unsubscribe, out of office) | Good, with a human check on anything ambiguous |
| Deciding who's a fit for your offer | No. That's your judgment and your criteria. |
| Finding "facts" about a person without a source | Never. Unsourced facts are how invented details get sent. |
| Sending without review | Not until you've reviewed enough batches to trust the drafts, and even then only for follow-ups |

The legal and platform baseline
This isn't legal advice, and the rules depend on where your recipients are. Check them before you send anything. A few baselines apply widely:
United States. The FTC's CAN-SPAM Act compliance guide applies to commercial email, and it makes no exception for business-to-business email. In short: no misleading headers or subject lines, identify the message as an ad where required, include a valid physical postal address, give a clear way to opt out, and honor opt-outs promptly. The guide sets the deadline at 10 business days.
United Kingdom. The ICO's guidance on electronic mail marketing says you can send marketing email to corporate bodies, such as companies and LLPs, but sole traders and some partnerships are treated as individuals, who need to have consented. The ICO also recommends keeping a "do not email" list of businesses that object, and notes data protection implications for named employee addresses. The page says it's under review after the Data (Use and Access) Act, so check the current version.
Other regions. The EU, Canada, and Australia all have their own rules, and several are stricter than CAN-SPAM. If a meaningful share of your list is there, get advice for those markets specifically.

Data sources. LinkedIn's User Agreement prohibits using software, scripts, robots, crawlers, or browser plugins to scrape or copy the service, including profiles. A "research agent" that scrapes profiles is a terms-of-service violation, not a clever shortcut. Use sources whose terms allow your use: the company's own website, press releases, public filings, job postings, your CRM, and data vendors that license their data for outreach.
Deliverability is part of the system
Cold email that lands in spam isn't outreach. Google's email sender guidelines set the baseline for anyone sending to Gmail accounts:
- All senders need SPF or DKIM, valid forward and reverse DNS, TLS, RFC 5322-formatted messages, and a spam rate in Postmaster Tools below 0.3%.
- Senders of more than 5,000 messages a day to Gmail accounts also need SPF and DKIM, a DMARC record (a
p=nonepolicy is allowed), From-domain alignment, and one-click unsubscribe for marketing messages, plus a visible unsubscribe link in the body. - Google recommends keeping the spam rate below 0.10% and never reaching 0.30%.
Personalized one-to-one outreach rarely approaches 5,000 a day, but meeting the stricter tier anyway costs little and protects you. One-click unsubscribe is defined in RFC 8058: a List-Unsubscribe header with an HTTPS URL, plus a List-Unsubscribe-Post: List-Unsubscribe=One-Click header. Most sending platforms add both if you turn the feature on.
Practical defaults that keep you out of trouble:
- Send from a dedicated subdomain or secondary domain, so a bad week doesn't damage your main domain's reputation.
- Start with small daily volumes and increase gradually while you watch the spam rate in Postmaster Tools.
- Verify addresses before sending. Bounces hurt reputation quickly.
- Send plain text or very light HTML, with one link at most. Tracking pixels and link redirects add weight without much benefit for one-to-one email.
Step 1: Define fit before you build a list
Write your ideal-customer criteria as rules you could check by hand:

offer: "Self-hosted transcription setup for video production teams"
must_have:
- "publishes video content at least weekly" # observable on their site or channel
- "team of 5-200 people"
- "no in-house ML engineer listed"
signals: # reasons to contact now
- "job posting for a video editor or producer"
- "recent launch of a podcast or video series"
- "public mention of captioning or accessibility work"
exclude:
- "existing customers and open deals" # route to account owner instead
- "anyone on the suppression list"
- "sole traders in the UK without consent"
The signals list is what makes personalization possible. A prospect with no current signal doesn't get a cold email this month. They go to a watch list. That rule alone does more for quality than any prompt.
Step 2: Collect evidence, not profiles
For each prospect that passes the fit rules, store a short research note made of quoted snippets, each with its source URL and the date you checked it. You can collect these by hand or with a script that downloads pages you're allowed to fetch, such as the company's own careers page or press page:
# research.py (pip install httpx)
import datetime as dt, httpx, re
def fetch_text(url: str) -> str:
html = httpx.get(url, timeout=20, follow_redirects=True,
headers={"User-Agent": "outreach-research (contact: you@example.com)"}).text
text = re.sub(r"<script[\s\S]*?</script>|<style[\s\S]*?</style>", " ", html)
return re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", text))
def note(company: str, urls: list[str]) -> dict:
return {"company": company, "checked": dt.date.today().isoformat(),
"sources": [{"url": u, "text": fetch_text(u)[:6000]} for u in urls]}
Respect each site's terms and robots.txt, keep request rates low, and don't collect personal data you don't need. A company's careers page tells you far more about whether it fits than an individual's social profile does.
Step 3: Draft with a required evidence quote
The model gets the research note, your offer, and strict rules, and it must return structured output that includes the exact sentence it relied on. This example uses a local model through Ollama's structured outputs, which keeps prospect data on your own machine. Any API with schema-constrained output works the same way.
# draft.py (pip install ollama pydantic; ollama pull qwen3:8b)
from pydantic import BaseModel, Field
from ollama import chat
class Draft(BaseModel):
subject: str = Field(max_length=60)
body: str
evidence_quote: str # exact text copied from one source
evidence_url: str
skip: bool # true if no source supports a relevant reason
OFFER = open("offer.md", encoding="utf-8").read() # who you help and how, in your words
def draft(note: dict, first_name: str, model: str = "qwen3:8b") -> Draft:
sources = "\n\n".join(f"URL: {s['url']}\n{s['text']}" for s in note["sources"])
prompt = f"""Write a cold email to {first_name} at {note['company']}.
Rules:
- Under 120 words. Plain text. No flattery, no exclamation marks, no emoji.
- Open with ONE specific reason for writing, based only on the sources below.
- Copy the exact sentence you relied on into evidence_quote, and its URL into evidence_url.
- If no source gives a relevant reason, set skip to true and leave the body empty.
- Do not mention results, clients, or numbers that are not in OFFER.
- End with one low-effort question.
OFFER:
{OFFER}
SOURCES (checked {note['checked']}):
{sources}"""
resp = chat(model=model, messages=[{"role": "user", "content": prompt}],
format=Draft.model_json_schema(), options={"temperature": 0.4})
return Draft.model_validate_json(resp.message.content)
The skip field gives the model a legitimate way to say "there's nothing here," which is much better than a vague compliment. Expect a fair share of prospects to come back skipped, and treat that as the system working.
Step 4: Validate every draft in code
Never trust the model's claim that its quote is real. Check it, along with the other rules, before a human sees the draft:

import re
BANNED = [r"(?i)\bI noticed you\b.*\bamazing\b", r"(?i)guarantee", r"(?i)\b\d+x\b", r"!"]
def validate(d: Draft, note: dict) -> list[str]:
problems = []
if d.skip:
return ["skipped: no supported reason"]
source = next((s for s in note["sources"] if s["url"] == d.evidence_url), None)
if not source:
problems.append("evidence_url is not one of the supplied sources")
elif d.evidence_quote.strip()[:80] not in source["text"]:
problems.append("evidence_quote not found in source text")
if len(d.body.split()) > 120:
problems.append("body over 120 words")
if d.body.count("?") != 1:
problems.append("needs exactly one question")
for pattern in BANNED:
if re.search(pattern, d.body):
problems.append(f"banned pattern: {pattern}")
return problems
The quote check catches the most damaging failure: an email that opens with a "fact" about the prospect that the model made up. The banned patterns are a starting list. Add the phrases you find yourself deleting during review.
Step 5: Review in one pass, then send through your platform
Put validated drafts in a review queue, such as a spreadsheet or your CRM, with the evidence quote and URL next to each email. Reviewing takes seconds per draft when the evidence sits beside it:
- Is the reason true, current, and relevant to what you offer?
- Would you send this exact email if you'd written it by hand?
- Is this person someone your offer could actually help?
Approved emails go to your sending platform, which should add the unsubscribe headers, the postal address, and the suppression check. Keep the suppression list in one place and check it in code before anything is queued. That includes past unsubscribes, bounced addresses, existing customers, and anyone who asked not to be contacted.
Step 6: Follow up with limits
Follow-ups are where automation turns into spam. Set hard limits in code:
| Rule | Default |
|---|---|
| Maximum follow-ups | 2 |
| Minimum gap | 4 business days, then 7 |
| Stop conditions | Any reply, an unsubscribe, a bounce, or an out-of-office lasting more than two weeks |
| Content | Each follow-up adds something new and useful, like a relevant resource or a short answer to a likely objection. "Just bumping this" doesn't count. |
Avoid acting on open tracking. Many email clients prefetch images or block them, so "opened" is an unreliable signal, and following up on opens feels like surveillance to recipients who notice.
Step 7: Sort replies, but answer them yourself
Classification is a good job for a model. Have it label each reply as interested, not_now, unsubscribe, out_of_office, wrong_person, or question, then route it:
- Unsubscribe, or anything that reads like one ("please stop," "remove me"): add to suppression immediately, in code, with no human step required. When in doubt, treat it as an unsubscribe.
- Interested or question: a human replies, ideally within a business day. A model can draft the answer, but you send it.
- Not now: record the timing they gave and set a reminder. Don't add them to an automated sequence.
- Wrong person: thank them. Contact the person they mention only if they actually suggested it.
What to measure
Track your own numbers per batch, not industry averages:
- Skip rate: the share of prospects with no supported reason. A high rate means your list or fit criteria need work.
- Review edit rate: how many drafts you change substantially. If it stays high, improve the offer document and prompt.
- Positive reply rate: replies you'd count as real interest, divided by emails sent.
- Unsubscribe and complaint signals: unsubscribes per batch, plus the spam rate in Google Postmaster Tools.
- Meetings and outcomes: what the conversations led to, so you're optimizing for business rather than replies.
If complaints rise, stop sending and diagnose before the next batch. Volume never fixes a targeting problem.
Common mistakes
Scraping profiles for "personalization." It violates platform terms, and the details it finds are usually shallow anyway. Company-level signals from allowed sources are more relevant and more defensible.
Letting the model invent the opener. "Loved your recent talk on..." about a talk that never happened ends the conversation. Require a quote and check it.
Treating cold email as a volume game. More sends from the same weak list mean more complaints and a damaged domain. Tighten fit criteria first.
Skipping the opt-out because it's "one-to-one." If it's commercial email, the rules on opt-outs and identification apply. Include them every time.
Automating replies to interested prospects. The moment someone engages is the moment a person should take over.
FAQ
Is AI-personalized cold email legal? Using AI doesn't change the rules. What matters is whom you email, what the message says, and whether you identify yourself and honor opt-outs. Requirements differ by country, so check the rules for your recipients' locations, and get legal advice for anything you're unsure about.
Should I use a local model or a hosted API? Either can draft short emails well. A local model keeps prospect research on your own hardware, which simplifies data-handling questions. If you need more capacity than your machine has, a private model on a rented GPU keeps the same privacy properties. How I Use Local AI Models covers local setup.
How many emails can I send per day? There's no safe universal number. Start small on a new domain, increase gradually, and let Postmaster Tools and your complaint rate decide. Google's stricter requirements begin above 5,000 messages a day to Gmail accounts, but your reputation can suffer well before that.
Can an AI agent run this end to end? It can run the research, drafting, validation, and reply sorting. Keep humans on fit decisions, draft approval, and real conversations. How to Build a Lead Generation AI Agent From Scratch covers the agent structure, and AI Workflow Automation: Chain Multiple Agents shows how to connect the steps.
How do I keep prospect data safe? Store only what you need, restrict who can access the research notes and suppression list, and delete research you no longer use. AI Agent Security covers credentials and permissions for automation like this.



