What it does
Closes the bounce loop in three directions:
1. Detect — mines Gmail for mailer-daemon notifications (last 7d default, 30d on first run for backfill)
2. Classify — RFC 5321: 5XX = HARD permanent, 4XX = SOFT temporary; soft → hard auto-escalates at 3+ strikes per email; HARD never downgrades
3. Reconcile — matches recipient to SoT lead_id, updates 5 new ledger columns; queues HS proposal + SmartLead-removal action; sets EMAIL VALID: FALSE on hard
4. Reroute — hard-bounced leads still meet postcard criteria → flow into next postcard-ledger round on mailing-address channel
Files
- Skill:
~/.claude/scheduled-tasks/smartlead-bounce-handler/SKILL.md(prompt the scheduler invokes) - Spec:
~/.claude/scheduled-tasks/smartlead-bounce-handler/SPEC.md(canonical design — edit here) - Helper:
~/Library/Application Support/SkyRun/bounce_handler.py(Python: status / ingest / report-bounces)
Cron
0 11 * — every day at 11am MDT. After morning brief, after stalled-deal-watchdog (8am), parallel with daily DQ.
Plus on-demand from SkyRun QB.
SoT ledger columns (added to Lead Details tab)
| Column | Type | Purpose | |
|---|---|---|---|
EMAIL BOUNCE DATE | YYYY-MM-DD | most-recent bounce | |
EMAIL BOUNCE TYPE | HARD / SOFT | sticky once HARD | |
BOUNCE COUNT | int | running count (3+ SOFT → auto-HARD) | |
EMAIL VALID | TRUE / FALSE | active gate; FALSE blocks future email outreach | |
BOUNCE EVIDENCE | string | <date>:<code>:<msg-id> \ | <date>:<code>:<msg-id> history |
EMAIL column on Lead Details is NEVER cleared on bounce — preserves historical record. EMAIL VALID: FALSE is the gate.
Outputs every fire
- Updated SoT (Lead Details ledger columns) via openpyxl
- HS proposals →
pending_hs_updates.jsonl(one per ingested event; Joseph 👍 to write) - SmartLead-removal actions →
pending_smartlead_actions.jsonl(Joseph processes via SL dashboard until API integration lands) - Gmail label
skyrun-bounce-processedon each handled thread (idempotency) - Heartbeat at
~/Library/Application Support/SkyRun/health/ - Audit summary at
~/Desktop/SkyRun/audit/<DATE>/ - ntfy push if
hard_count > 0:📤 Bounce handler — N hard, M soft, K SmartLead removals queued
Hard rules
- Microsoft Office toolchain — openpyxl only
- HS writes are proposals (never auto-write)
- SmartLead removals are proposals (manual until API integration; never call SL API directly)
EMAILcolumn never cleared (record preservation);EMAIL VALIDis the gate- HARD is sticky — no SOFT can downgrade
- 3+ SOFT strikes auto-escalate to HARD (helper enforces)
Manual operation
bash
Status
python3 "$HOME/Library/Application Support/SkyRun/bounce_handler.py" status
Ingest manually (events.json)
python3 "$HOME/Library/Application Support/SkyRun/bounce_handler.py" ingest --events /path/to/events.json
Dump full ledger as JSON
python3 "$HOME/Library/Application Support/SkyRun/bounce_handler.py" report-bounces
Loop closure — what happens to a hard-bounced lead
1. SoT: EMAIL VALID: FALSE
2. HS: proposal to write lead_source_notes |BOUNCE:HARD:<date> + email_bounced=true custom property
3. SmartLead: removal queued for Joseph to process in dashboard
4. Postcard pipeline: lead REMAINS postcard-eligible (postcards use mailing address); flows naturally into next postcard-ledger round
5. BV runs: should NOT re-enrich email field for this lead (logged as kg_pending advisory for downstream BV update)
6. Live-EA: drafting standard reads HS; EMAIL VALID: FALSE blocks outbound drafts to that email
Interactions with other skills
- postcard-ledger — hard-bounced + valid mailing address = automatic postcard candidate. Two skills compose cleanly.
- daily-data-quality-check — picks up new ledger columns; flags drift between SoT
EMAIL VALIDand HSemail_bounced - live-ea — drafting standard honors
EMAIL VALID; won't draft to dead emails - postcard-updater — independent (candidate-pool growth, not bounce concerns)
Tuning notes
After 4 weeks:
- Hard bounce rate per campaign — target <2% (industry healthy <5%)
- Soft → hard escalation count — high count may indicate sender-domain reputation issue, not address invalidity
- Median time-to-process — should be <24h after bounce arrival
- SmartLead-removal queue depth — should drain to ≤5 within 48h; persistent growth = Joseph not processing → ntfy escalate
Future enhancements
1. SmartLead API integration — direct DELETE call instead of manual queue
2. SmartLead webhook — POST to PWA /api/smartlead-bounce for real-time event capture (cuts latency from 24h to minutes)
3. Multi-mailbox support — also mine the 5 Gmail variants on skyrungrandcounty.com
4. Domain-reputation watch — auto-pause campaign if hard-bounce rate >3%
Origin
Built 2026-04-27 as Gap B closure in the prospecting-loop walkthrough. Audit (same day) flagged 9 Day-1 bounces from Apr 22 SmartLead launch still un-invalidated. First run (tomorrow 11am) backfills the 30-day window to clear that backlog.