Operations skill
Cold email lead list cleaner
Share a lead CSV and the assistant returns a kept file, a dropped file with a reason for every removed row and a report, so only clean rows with usable merge fields reach your sequencer.
What it does
What the skill checks
- Takes the company domain from the email address, not from the website column
- Dedupes by email and optionally keeps one contact per company
- Drops role accounts and handles free-mail per your target customer
- Finds large companies by how many rows share one email domain
- Fixes ALL CAPS names and strips Inc, LLC and similar suffixes
- Suppresses rows with empty or junk merge fields, past contacts and unsubscribes
- Tags catch-all addresses instead of dropping them
Use it when
- Before importing a purchased or enriched list into a sequencer
- Sent emails show "Hi there", ALL CAPS names or company suffixes
- Merging several lead sources into one campaign
Example
What you paste and what you get back
5,000 rows. Copy uses first_name and company. Columns: email, first_name, company, domain, verification_status.Kept: 4,120.
Dropped: duplicate 210, role_account 180, large_company 290 (3 domains with 40+ rows each), empty_merge_field 160, suppressed 40.
Tagged: 610 catch_all rows.
Note: 6% of rows had a domain column different from the email domain; email domain used.
Files: kept.csv, dropped.csv with drop_reason, report.txt.Install
Install it in your assistant
The same file works everywhere. Claude and Claude Code load it as a skill; the other assistants follow it as instructions.
Claude Code
- Run the command below. It saves the skill to your personal skills folder, available in every project. For one project only, use .claude/skills in the project instead.
- Start a new Claude Code session. Claude uses the skill when your request matches its description, or when you name it.
mkdir -p ~/.claude/skills/lead-list-cleaner
curl -fsSL https://outreach2day.com/skills/lead-list-cleaner/SKILL.md -o ~/.claude/skills/lead-list-cleaner/SKILL.mdClaude (web and desktop)
- Download the ZIP file.
- In Claude, open Settings, find Skills under Capabilities and upload the ZIP. Skills need code execution to be turned on for your account.
- Ask for the task in any chat. Claude loads the skill when the request matches.
ChatGPT
- Download SKILL.md.
- Paste its text into a Project's instructions or a custom GPT's instructions. For a single chat, attach the file and write: follow the instructions in this file.
Gemini
- Download SKILL.md.
- Create a Gem and paste the file's text into its instructions, or attach the file to a chat and ask Gemini to follow it.
Grok
- Download SKILL.md.
- Paste its text into a Project's instructions, or attach the file to a chat and ask Grok to follow it.
Cursor and other coding agents
- Save the file as a project rule with the command below. Cursor reads the description to decide when to apply it.
- Agents that read AGENTS.md (Codex and others): paste the text into AGENTS.md or reference the file from it.
mkdir -p .cursor/rules
curl -fsSL https://outreach2day.com/skills/lead-list-cleaner/SKILL.md -o .cursor/rules/lead-list-cleaner.mdcSource
The full SKILL.md
Read it before you install it. Change the rules to match your own setup.
---
name: lead-list-cleaner
description: Cleans a cold email lead list before import - takes the company domain from the email address, dedupes by email and domain, drops or tags role accounts and free-mail, catches large companies by how many rows share one email domain, matches blocklists by brand stem, tags catch-all addresses, fixes ALL CAPS names and company suffixes, suppresses rows whose merge fields are empty or junk, and removes past contacts and unsubscribes. Returns a kept file, a dropped file with a drop_reason per row and a report with counts. Use when the user shares a CSV of leads, asks to clean, dedupe or prepare a list for a sequencer, or sees "Hi there" or broken names in sent emails.
---
# Lead list cleaner
You prepare a lead list so every row that reaches the sequencer is a real target with clean merge fields. You keep every removed row with a reason, so the user can review and reverse decisions.
## Step 1. Collect
Ask for:
1. The CSV (or its header and 20 sample rows) and the row count.
2. The ideal customer: company size, whether free-mail addresses (gmail.com and similar) are acceptable.
3. The merge fields the copy uses (for example `first_name`, `company`).
4. Suppression sources: past contacts, unsubscribes, bounces, current customers, competitors.
5. Whether the list was verified, and the verification status column.
## Step 2. Clean, in this order
1. **Normalize emails**: trim, lowercase, drop rows without a valid address.
2. **Domain from the email**: take the part after `@`. Do not trust a `domain` or `website` column: enrichment often puts a brand or parent-company site there while the address is on another domain.
3. **Dedupe**: one row per email. Then decide per domain: keep all contacts, or keep the best one per company if the user wants one contact per account.
4. **Role accounts**: drop `info@`, `sales@`, `support@`, `admin@`, `office@`, `hello@`, `contact@`, `noreply@` unless the user targets them on purpose.
5. **Free-mail**: drop or keep per the user's ideal customer.
6. **Large companies**: count rows per email domain inside the list. A domain with many rows is usually a large company even if the size column says otherwise. Flag domains above a threshold the user sets (for example 10 rows).
7. **Blocklist by brand stem**: match customers and competitors by the first label of the domain, so `brand.com`, `brand.co.uk` and `brand.de` all match.
8. **Catch-all addresses**: keep them, but tag them (`mailbox_confidence=catch_all`). Consider sending them from a separate, well-warmed group of mailboxes.
9. **Fix casing**: `JOHN` to `John`, `ACME PLUMBING INC` to `Acme Plumbing`. Strip legal suffixes (Inc, LLC, Ltd, GmbH, Corp) from the company name used in copy.
10. **Merge fields**: suppress rows where any field used in the copy is empty or junk (a job title in the company column, digits only, "N/A", "-"). A fallback like "there" is weaker than not sending.
11. **Suppression**: remove past contacts, unsubscribes, bounces and customers, matched by email and, for companies, by domain.
## Output format
1. `kept.csv`: cleaned rows, with added columns `email_domain`, `domain_rows`, `mailbox_confidence`.
2. `dropped.csv`: every removed row with `drop_reason` (one of: invalid_email, duplicate, role_account, free_mail, large_company, blocklist, empty_merge_field, junk_merge_field, suppressed).
3. A report: rows in, rows kept, rows dropped by reason, top 10 domains by row count, 10 sample kept rows rendered with the copy's first line.
## Optional script (Python, standard library only)
```python
import csv, re, collections
ROLE = {"info","sales","support","admin","office","hello","contact","noreply"}
SUFFIX = re.compile(r"[,\s]+(inc|llc|ltd|gmbh|corp|co)\.?$", re.I)
rows = list(csv.DictReader(open("leads.csv", newline="", encoding="utf-8")))
dom = lambda r: r["email"].strip().lower().split("@")[-1]
per_domain = collections.Counter(dom(r) for r in rows)
seen, kept, dropped = set(), [], []
for r in rows:
e = r["email"].strip().lower()
reason = None
if not re.match(r"^[^@\s]+@[^@\s]+\.[a-z]{2,}$", e): reason = "invalid_email"
elif e in seen: reason = "duplicate"
elif e.split("@")[0] in ROLE: reason = "role_account"
elif per_domain[dom(r)] > 10: reason = "large_company"
elif not r.get("first_name", "").strip() or not r.get("company", "").strip(): reason = "empty_merge_field"
seen.add(e)
if reason: dropped.append({**r, "drop_reason": reason}); continue
r["first_name"] = r["first_name"].strip().title()
r["company"] = SUFFIX.sub("", r["company"].strip()).title() if r["company"].isupper() else SUFFIX.sub("", r["company"].strip())
r["email_domain"], r["domain_rows"] = dom(r), per_domain[dom(r)]
kept.append(r)
```
Extend it with the blocklist, free-mail and suppression steps. Title-casing can damage names like "McDonald" or "IBM"; review the sample.
## Rules
- Never delete silently: every removed row goes to `dropped.csv` with a reason.
- Fix data before import; many sequencers do not let you edit a lead after upload.
- After import, render 5 real leads in the sequencer's preview or a test send before launch.
## Example
Input: 5,000 rows, copy uses `first_name` and `company`.
Output (abridged): kept 4,120. Dropped: duplicate 210, role_account 180, large_company 290 (3 domains with 40+ rows each), empty_merge_field 160, suppressed 40. 610 catch-all rows tagged.
FAQ
Questions
Should I remove catch-all emails from a cold email list?
Not by default. Tag them and consider sending them from a separate group of well-warmed mailboxes, so a higher bounce share does not affect the rest.
Why take the domain from the email instead of the website column?
Enrichment tools often fill the website column with a brand or parent-company site. The email domain is where the person actually receives mail.
What should I do with leads that have no first name?
Suppress them if your copy uses the first name. A fallback like "Hi there" reads as a mass email.
Can I check my copy for spam signals too?
Yes. The free cold email copy checker on this site flags links, spam-prone words and length.
Mailboxes, warm-up and sending in one place
$2.50 a mailbox a month. DNS records are set for you, and every mailbox shows its warm-up numbers from day one.