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.

Works with

One Markdown file, free to use and change. It reads only what you paste into the chat.

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

You
5,000 rows. Copy uses first_name and company. Columns: email, first_name, company, domain, verification_status.
Assistant, with the skill
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

  1. 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.
  2. Start a new Claude Code session. Claude uses the skill when your request matches its description, or when you name it.
Terminal
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.md

Claude (web and desktop)

  1. Download the ZIP file.
  2. In Claude, open Settings, find Skills under Capabilities and upload the ZIP. Skills need code execution to be turned on for your account.
  3. Ask for the task in any chat. Claude loads the skill when the request matches.

ChatGPT

  1. Download SKILL.md.
  2. 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

  1. Download SKILL.md.
  2. 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

  1. Download SKILL.md.
  2. 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

  1. Save the file as a project rule with the command below. Cursor reads the description to decide when to apply it.
  2. Agents that read AGENTS.md (Codex and others): paste the text into AGENTS.md or reference the file from it.
Terminal, in your project
mkdir -p .cursor/rules
curl -fsSL https://outreach2day.com/skills/lead-list-cleaner/SKILL.md -o .cursor/rules/lead-list-cleaner.mdc

Source

The full SKILL.md

Read it before you install it. Change the rules to match your own setup.

lead-list-cleaner/SKILL.md
---
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.

Download SKILL.md · Download ZIP

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.