---
name: do-not-contact
description: Suppression gate consulted before any outbound touch: merges disqualified leads, manual exclusions, bounces, and imported exclusion lists into one do-not-contact list with a live check. Use before promoting discovered leads, drafting outreach, sending email or LinkedIn requests, or resurfacing old leads. A hit drops the candidate; a missing or stale list halts the run. Not for batch send approval itself: use a send-gate skill. Not for post-acceptance deal work: use deal-to-invoice.
license: MIT
metadata:
  author: TechTide AI (Alex Cinovoj)
  provenance: original
  category: Sales & RevOps
---

# Do Not Contact

One suppression list, consulted by every outreach surface, so no agent or manual batch ever touches a person or company the owner has excluded. Partial coverage is the failure mode this closes: a list that guards only one pipeline stage guards nothing.

## Files

All under `$WORKDIR/pipeline/`:

- Unified list (build artifact, never hand-edit): `exclusions/do-not-contact.jsonl`
- Build metadata (build time, source counts): `exclusions/do-not-contact.meta.json`

Sources merged on every rebuild:

| Source | Contributes |
|---|---|
| `exclusions/disqualified-leads.jsonl` | Company-scope and person-scope disqualifications |
| `manual-email-suppression.csv` | Bounced and manually suppressed emails |
| `manual-lead-exclusions.csv` | Friends, peers, bad leads, wrong profiles |
| `imports/buyer-exclusions-*.csv` | Vendor, recruiter, and competitor exclusions |
| `outreach-log.jsonl` | Records with status `do_not_contact`, `bounced`, `excluded_bad_lead` |

## The Gate Contract

Maintain a rebuild script and a check script in the pipeline repo with this exact contract:

- **Rebuild**: full idempotent rebuild of the unified list from all sources, writing the meta file with a timestamp.
- **Check**: takes one candidate (email, LinkedIn URL, name, company) or a batch JSONL file. Prints a JSON verdict with `checked_at`. Exit codes: `0` clear, `1` suppressed hit, `2` hard stop (list missing, unreadable, or older than the configured max age).

Matching rules the check must implement: email exact-match after lowercasing. LinkedIn URL match after stripping scheme, `www.`, query strings, and trailing slashes. Company exact-match on normalized name against company-scope entries. Name-plus-company as the fallback person match when no email or URL exists. Any single identifier hit suppresses the candidate. Near-miss company aliases are not caught: review new discovery batches for aliases by hand.

## Operating Rules

1. Every outbound loop runs the live check twice: immediately before drafting and again immediately before send approval. A cached result is not a live check.
2. Log the `checked_at` timestamp into the loop's run record.
3. A hit means drop the candidate and record the reason. Never soft-suppress, never queue for later, never "reach out anyway just this once".
4. Exit code `2` halts the entire run: rebuild, re-check, log it as a gate incident.
5. New exclusions go into a source file (people into `manual-lead-exclusions.csv`, companies into `exclusions/disqualified-leads.jsonl`), then rebuild. Never append to the unified file directly; the next rebuild would erase it.
6. Rebuild at the start of any day that includes a send batch, so yesterday's bounces and opt-outs take effect today.

## Verification

Run the check against a known-suppressed identifier from a source file. Expect exit code `1` and a verdict naming the source. Then run it against an obviously clear synthetic identifier. Expect exit code `0` with a fresh `checked_at`. If either result differs, the gate is broken: halt all outbound until the rebuild and check behave to contract.

## Good vs Bad

Judgment call: a great-fit lead turns up in a new discovery batch, and the check returns a hit from an old bounce.

**Bad:** "The bounce was a year ago and they changed companies, I'll include them." The gate just became advisory, which means it no longer exists.

**Good:** Drop the candidate, record the hit reason, and surface it to the owner: "suppressed by 2024 bounce, flag if you want the source entry reviewed." Only a human edit to a source file plus a rebuild can clear someone.

## Footguns

- **Hand-editing the unified list.** The next rebuild silently reverts your edit. Fix: edit sources, rebuild, verify the entry appears.
- **Checking at discovery but not at send.** Days pass between the two; yesterday's opt-out slips through. Fix: the check runs twice, draft-time and send-time, no exceptions.
- **Treating exit code 2 as a skip.** A missing list reads like "no hits" to sloppy code. Fix: hard stop semantics enforced by the calling loop, logged as an incident.
- **Company aliases.** "Acme Inc" on the list does not catch "Acme Corporation". Fix: manual alias review on every new discovery batch, add aliases to the source file.

## Red Flags

Stop if you catch yourself thinking or saying:

- "The check passed this morning, that still counts"
- "It's just a follow-up, not a new touch"
- "They're clearly a good lead, the entry must be stale"
- "I'll add them back to the queue and check later"
- "The list is missing, so nobody's suppressed"

Every touch gets a live check, and only the owner clears an entry. There is no candidate valuable enough to bypass this gate; if one seems to be, that is the strongest sign the gate is doing its job.

## Completion Checklist

- [ ] List rebuilt today if a send batch runs today
- [ ] Live check run before drafting AND before send approval
- [ ] `checked_at` logged in the run record
- [ ] All hits dropped with reasons recorded, zero soft-suppressions
- [ ] Any exit code 2 halted the run and was logged as an incident

Any box unchecked: not done. Fix or say so.
