What AI-Drafted Marketing Email Copy Actually Gets Wrong
Published
I draft a meaningful share of client email copy with Claude as a first pass. The complaint I hear most about AI email copy is that it sounds generic. That's not the failure mode I actually hit. The copy usually sounds specific. The problem is the specifics aren't always true. A number that sounds right but isn't in the brief. A product detail that fits the tone but doesn't match the catalog. These pass a quick read because they sound confident, not because they're correct.
Invented Specifics Are The Real Risk
A generic line is easy to catch. It reads flat, and a human editor fixes it in one pass without needing to check anything.
An invented specific is harder, because it reads like evidence. "Over 40% of customers reorder within 60 days" sounds like a real stat. If it isn't in the brief, Claude built it to sound plausible, not because it's true.
I've had this happen with discount percentages, shipping timeframes, and product counts. Each one looked exactly like the real numbers around it. Nothing about the sentence flagged itself as wrong.
One almost went out. A win-back email claimed a 30-day return window when the actual policy was 14 days.
The brief never mentioned 30 days anywhere. Claude just picked a number that sounded like a typical ecommerce return policy, and it read completely normal until someone checked it against the actual policy page.
That's the part that makes this failure mode genuinely risky rather than just annoying. A wrong return window in a live email damages customer trust in a way that goes well beyond a copy-quality complaint.
Generic Benefit Language With No Account Context
The second failure mode is closer to the complaint people usually make, but it's more specific than "sounds generic." It's copy that could run for almost any brand in the category without changing a word.
A welcome email that says "join thousands of happy customers" works for a supplement brand, a clothing brand, or a software tool equally well. That's the actual tell.
If a line survives being pasted into a competitor's email with no edits, it isn't doing the account-specific job a flow email needs to do.
This one is less dangerous than an invented number, since it doesn't risk sending something false. It still costs revenue quietly, because generic copy converts at the generic rate.
Merge Tags Assume Data That Isn't Always There
The third failure mode is less about prose and more about setup. Claude will draft a personalization line, like referencing a customer's last purchased category, without checking whether that property is actually populated for every subscriber.
Klaviyo's flow and campaign segmentation depends on that property existing consistently across the list it's sending to.
A merge tag referencing a field that's empty for a meaningful chunk of subscribers doesn't error, it just renders blank or awkward, and nobody notices until open rates on that specific segment look off.
I've seen Claude draft a post-purchase line referencing "your last category" without flagging that the property only populates after a second order. For first-time buyers, exactly the people most likely to open that email, the line either goes blank or defaults to something generic.
This is the one failure mode a straight read-through usually catches, since a blank-looking sentence in a preview is visually obvious once you send a test to an account without that property set. The other two aren't nearly as visible.
The Check I Actually Run
Reading carefully doesn't scale once a cycle has five or six emails drafted in one sitting, so I built a small script that cross-checks numeric claims in a draft against a source-of-truth file before anything gets approved.
It pulls every number-shaped token out of the draft text first.
import re
NUMBER = re.compile(r"\$?\d+(?:,\d{3})*(?:\.\d+)?%?")
def extract_claims(draft_text):
return set(NUMBER.findall(draft_text))
Then it loads the approved figures from a facts file I keep per client, the real numbers that are actually allowed to appear.
import json
def load_approved_numbers(facts_path):
facts = json.load(open(facts_path))
return {str(value) for value in facts.values()}
The check itself is just a set difference. Anything in the draft that isn't in the approved set gets flagged for a manual look before the email goes anywhere.
def check_draft(draft_path, facts_path):
draft_text = open(draft_path).read()
claims = extract_claims(draft_text)
approved = load_approved_numbers(facts_path)
return claims - approved
It doesn't catch generic benefit language or missing merge-tag data. It catches one specific thing well, which is the invented-number failure mode, the one a quick read is most likely to miss.
The facts file itself is nothing complicated. Just a small JSON object per client with the numbers that are actually allowed to show up: return window, shipping cutoff, discount codes currently live, loyalty tier thresholds.
Updating it takes a minute whenever one of those changes, and it's the only place those numbers need to be correct for the check to work.
A flagged number can mean two different things. Either Claude invented it, or it's a real figure that just hasn't been added to the facts file yet.
The script's job is to surface the question, not answer it. A flagged claim still gets a manual check before anyone decides which one it is.
A Human Read-Through Is Still The Last Step
None of this replaces reading the email before it sends. The script catches numbers that don't match a source file, nothing about tone, nothing about whether a merge tag will actually render for every subscriber.
What it does is take the one failure mode that's hardest to catch by eye and makes it mechanical instead of relying on attention. The read-through still happens. It's just not the only thing standing between a draft and a send anymore.
Want a second set of eyes on your email flows?
I audit Klaviyo accounts for exactly this kind of quiet drift between what copy claims and what's actually true.