ResearchSkill
Sung Jo
Sung JoSEO/AEO lead at Clay

Clean Email List

Clean email addresses into keep / risky / remove segments with per-row evidence, free deterministic passes first (dedupe, syntax, role and disposable screens, domain MX check), then a paid mailbox validator only on rows that survive

About this Skill

When to use it

A list's value is set by its worst segment. Bounce rate is measured over the whole send, a few percent of dead rows can gate the other 95% out of inboxes.

SKILL.md preview

---
name: clean-email-list
description: |
  Clean a CSV or table of email addresses into keep / risky / remove segments with
  per-row evidence — free deterministic passes first (dedupe, syntax, role and
  disposable screens, domain MX check), then a paid mailbox validator only on rows
  that survive. Use whenever someone says: clean this email list, scrub my list
  before a send, remove invalid or duplicate emails from this CSV, how many of
  these addresses are still good, or prep this list for cold outreach. Every
  removed row ships with its reason — nothing is silently deleted, and rows that
  can't be verified are flagged, never guessed. Do NOT use it to check one or two
  addresses (verify-email-deliverability), to find or replace missing emails
  (find-work-email), or to merge duplicate CRM contact *records* (dedupe-contacts).
  It states total cost before spending any credits.
category: verify-and-clean
type: task
tags: [csv, clay-action, persona:revops, persona:marketing-ops, persona:sales-reps]
keyword: clean-email-list
---

# Clean an email list

The insight: **a list's value is set by its worst segment.** Bounce rate is measured
over the whole send — a few percent of dead rows can gate the other 95% out of inboxes.
So cleaning is triage, not filtering: every input row lands in exactly one of
keep / risky / remove / could-not-verify, with quoted evidence, and rows are never
silently deleted — a row you can't account for is a row you can't defend. The other
half is ordering: every free deterministic pass runs before the first paid call, so the
validator only ever sees rows that could still be sendable.

How someone would prompt this

Clean this 500-row conference list for a cold send.

Other ways to ask

Inputs & outputs

What someone provides before the Skill runs, and the reviewable artifacts it returns.

Inputs

  • 01The lista CSV or table, and which column holds the email
  • 02The purposecold volume send, CRM refresh, or re-engagement
  • 03Budgetcredits for paid validation after the free passes

Outputs

  • 01Audit CSV
  • 02keep.csv
  • 03Summary: rows in → unique → per-segment counts + % → credits actually spent

Representative output

214 keep, 71 risky (29 personal + 42 catch-all), 203 remove, 12 could-not-verify. 500 = 214 + 71 + 203 + 12 ✓.

How the Skill runs

Select any node to inspect how inputs move through the workflow and become reviewable outputs

Clean Email List

Inputs

The listThe purposeBudget

Steps

  1. 01
    Collect the inputs

    1. The list (CSV/table) and which column holds the email. Never edit other columns. 2. The purpose, cold volume send vs CRM refresh vs re-engagement. It decides the policy segments: for cold sends, role/generic mailboxes (info@, support@…) go to remove and personal/freemail goes to risky; for other purposes both go to risky.

  2. 02
    Free passes (zero credits, deterministic code, not an LLM)

    Run in order, recording per-row reason + evidence at each: 1. Normalize + dedupe. Trim, lowercase for comparison (keep the original string). Exact-match duplicates: keep the first, mark the rest remove: duplicate with a pointer to the kept row. 2. Syntax. One local-part @ one domain with a dot, no spaces, a cheap structural check, not full RFC. Failures → remove: syntax-invalid, quoting the string. Never "fix" a typo and keep the fixed version silently. 3. Role/generic screen. Local-part blocklist (production lists run ~40 tokens): admin, info, contact, support, sales, billing, hr, press, help, hello, office, team, marketing, careers, jobs, noreply, no-reply, postmaster, abuse, webmaster… Segment per the Step 1 policy, these are policy calls, not deliverability verdicts (a support@ box may be genuinely read). 4. Disposable domains. Known disposable providers (mailinator, guerrillamail, yopmail…) → remove: disposable. 5. MX per unique domain (any DNS tool, don't hardcode a DoH URL; some networks block them). Three shapes: NXDOMAIN or no MX → dead; null MX (single record 0 .) → the domain declares it takes no mail, dead *even though a record exists*; real MX → survives. Dead domains → remove: dead-domain for every row on them, quoting the DNS answer. This kills paid calls on dead rows for free.

  3. 03
    Classify survivors (Clay-native, ~free)

    Run extract-email-components (Clay action; catalog tier 1) per surviving row: it returns domain plus isLikelyPersonalEmail / isLikelyCompanyEmail / isLikelyEducationEmail. Personal → risky under a cold-send policy. Read the actual charge from run usage metadata (verified at 0 credits + 0 action executions in one workspace, but read it, don't assume).

  4. 04
    Paid validation on what's left (approval gate)

    Pick a validator from the live catalog (clay workflows actions list), lowest priorityTier that separates catch-all from valid (tier 2 is typically ZeroBounce validate-email or enrichley-verify-email, ~0.1 credits/check). State survivor count × per-check cost and get approval before running. Then call it per row (Clay MCP execute_clay_action); checks return in seconds for every verdict. Map field PAIRS, never one field, vocabularies invert across validators: ZeroBounce puts catch-all at status: valid + sub_status: catch_all; Enrichley puts it at valid: false + result: catch_all. Verdicts: plain valid (sub_status: "", empty string) → keep; catch-all → risky (Enrichley's catch_all_validated upgrade = keep, probe-confirmed); do_not_mail / suppression / disposable sub-statuses → remove, quoting the sub-reason; invalid → remove; unknown, timeouts, empty payloads → could-not-verify, never rounded either way. Fuse validator with classifier: a freemail address can validate as deliverable (observed live: valid / sub_status: alternate + free_email: true on a Gmail address), a mailbox verdict never overrides the policy segment. Run status is not data: it reports SUCCESS for every verdict. For many hundreds of rows or a recurring clean, build it as a Clay table/workflow instead and say so. ## What good looks like - Reconciliation is the first check: rows in = keep + risky + remove + could-not-verify. If the numbers don't add up, a row was dropped silently, the cardinal failure of list cleaning. - Every removed row carries a reason AND quoted evidence (the DNS answer, the validator field pair, the duplicate pointer), an audit trail, not a verdict. - The common mistake: reading one validator field as a boolean. Catch-all hides inside the safest-looking field, in opposite directions per provider. - Could-not-verify is an honest segment, not a failure, a common shape is unknown / mail_server_temporary_error, which is retryable later; the user decides whether to spend more checks. ## Rules - MUST account for every input row in exactly one segment, never silently drop. - MUST run all free passes before any paid call, and state cost + get approval first. - MUST report raw provider fields alongside each verdict. - NEVER guess a verdict for an unverifiable row; NEVER silently correct a malformed address; NEVER report catch-all, role, or disposable rows as plain valid.

Outputs

Audit CSVkeep.csvSummary: rows in → unique → per-segment counts + % → credits actually spent

Connections required

Clay CLI / agent pluginApproved connection for this Skill

Install Clay and run it

Works in Claude Code, Cowork, or Codex, not in standard chat. Sign in once, install the Skill, then run the prompt.

  1. 1Install ClayUse the connector or official plugin in your coding-agent environment.
  2. 2Sign up or log inAuthorize Clay access and return when the connection is ready.
  3. 3Install the SkillRun the one-line install command from the Add this Skill card.
  4. 4Run the SkillPaste the example prompt and review the result.

Version history

Version 1From submitted SKILL.mdListing copy uses the declared SKILL.md block when present, then moderator edits, then measured legacy extraction. Missing fields remain visibly unparsed rather than generated.Current