Inbox lifecycle manager skill

The weekly keep/cancel/promote/buy loop for a cold email sending fleet.

by growthenginenowoslawski·MIT license·★ 736 Stars on the repo·GitHub ↗

Use now

Files of Inbox lifecycle manager

growthenginenowoslawski/main1 file shown
SKILL.md
Show the full text341 lines

Inbox Lifecycle Manager

Diagnosis tells you a domain is sick. This skill decides whether it dies.

Every cold email fleet leaks money in two directions at once: you keep paying for burned domains that will never reply again, and you run under your send goal because nobody promoted the warmed reserves sitting idle. This is the weekly loop that fixes both, with an approval gate in the middle so nothing gets cancelled on a hunch.

Read-only by default. The plan step writes nothing, anywhere. Only the apply step, run after an explicit human "yes", changes a tag, a campaign, or a provider subscription.

When to use

  • The weekly hygiene routine (Friday morning is the natural slot — see /cold-email-weekly-rhythm)
  • "Which inboxes should I cancel?" / "What's burned?" / "Clean up my domains"
  • "I'm under my daily send goal" / "How many domains do I need to buy?"
  • "My inbox bill is too high" — the spend section prices every domain against what it earns
  • After /email-deliverability-audit or /deliverability-incident-response hands you a SENDER_BURNED verdict and you need to act on it

Not this skill: why a domain is bad (/email-deliverability-audit), fixing DNS (/zapmail-domain-setup-public), an acute incident (/deliverability-incident-response), warmup settings and signatures (/smartlead-inbox-manager).


The status model

Four statuses, and they apply to a whole domain, never to one inbox inside it.

Status Meaning Warmup In campaigns
Warmup Newly provisioned, under 14 days of warmup ON no
Insurance Warmed, healthy, idle — the reserve pool ON no
Active Sending in live campaigns OFF or minimal yes
Cancel Condemned; stop sending, cancel the subscription OFF no

⛔ Never split a domain's inboxes across statuses. Mailbox providers judge reputation at the domain level. Half a domain "Active" and half "Cancel" means you are still sending from a domain you have declared burned, and the healthy half inherits the reputation of the dead half. Every decision below is computed per domain and applied to every inbox on it.

⛔ Warmup domains are untouchable. Not counted as capacity, not promotable, not demotable, not cancellable. If your tool has no Warmup tag, the domain's age is your only protection — see the age guard below, and keep a list of domains provisioned in the last 30 days.

In Smartlead these statuses are tags. See /smartlead-inbox-manager for the tagging mechanics.


Sample-size floors — the rule that is broken most often

A domain is only judged once there is enough data to judge it. Below the floor the answer is "we don't know yet", and that is a legitimate output, not a failure.

Judgment Floor Why
Reply rate 200 sends in the window At a 1% line, 200 sends is ~2 expected replies. Below 200, a 0% reply rate cannot distinguish a burned domain from an unlucky week.
Bounce rate 50 sends Below that, one dead address reads as 2% and two read as 4% — straight through a 3% threshold.
Bounce composition 30 classified bounces for that domain A domain with no sampled bounces has a 0% sender-originated share by construction, which looks identical to a rotten list.
Domain age 30 days A domain that has not finished ramping has not earned a verdict.
Placement test 100-300 senders per test Smaller samples swing double digits run to run.

⛔ Unresolvable age counts as young. If you cannot date a domain, it is TOO_YOUNG and it is not cancelled. Fail closed: you can always cancel it next week.


The decision ladder

Run top to bottom per domain, over a 7-day window (fall back to 14 days if 7 days is under the 200-send floor). The first gate that matches wins.

G0. Status is Warmup, or any inbox on the domain is in Warmup?
      -> EXCLUDED. Not judged. Stop.

G1. Domain age < 30 days, or age unknown?
      -> TOO_YOUNG. Never a cancel candidate at any reply rate. Stop.

G2. sent < 200 in the window?
      -> INSUFFICIENT_DATA. Do not judge the reply rate. Re-check next week. Stop.

G3. sent >= 50 AND bounce_rate > 3%?
      -> BOUNCE_FORK. This is not a cancel decision yet — a high bounce rate is usually the
         LIST, not the domain. Hand to /email-deliverability-audit to classify the bounce
         codes before you condemn anything. A domain is only cancelled out of this fork when
         the bounces are majority sender-originated AND the DNS auth is already clean.
         (>20% of classified bounces in the blocklist codes 5.7.606-5.7.614, or DSN text
          naming a blocklist, means burned regardless of the bounce rate.)

G4. reply_rate >= 1.0%?
      -> KEEP. Healthy enough to stay Active. Stop.

G5. reply_rate < 1.0% (or 0 replies on >= 150 sends), age >= 30d, bounce not the story?
      -> CANCEL CANDIDATE. The domain has had a fair sample and did not reply. It is burned.
         BUT: only cancel if you have an Insurance domain to swap in, or you are deliberately
         shrinking. Otherwise -> KEEP_BELOW_THRESHOLD, flagged, and buy replacements first.
The numbers, and where they come from
Threshold Value Note
Cancel line (reply) < 1.0% over 7d at ≥200 sends The line below which a domain stops earning its subscription.
Zero-reply cancel 0 replies at ≥150 sends A 0% rate is decisive earlier than a low-but-nonzero one.
Healthy ≥ 1.0% Keep.
Genuinely good ≥ 1.5% Full marks. Do not cancel anything near this.
Bounce fork / autopause > 3% at ≥50 sends Same number your sending tool should use for bounce_autopause_threshold.
Blocklist share > 20% of classified bounces Burned regardless of bounce rate.
Minimum age 30 days Hard floor, no exceptions.
Placement ≥85% inbox = healthy, 70-84% degraded, <70% fails From a real seed placement test, not a guess.
Warmup reputation < 98% = pull the inbox If the tool reports it live.

One number, everywhere in this repo: 1%. A domain that cannot reply at 1% after a fair sample is not earning its subscription. The same 1% that flags a domain in /email-deliverability-audit is the line that cancels it here — the difference between "flag" and "cancel" is not a second threshold, it is the floors: 200 sends and 30 days of age. Below those, 1% means nothing and nothing gets cancelled.

Warmup-off is "held", not "healthy"

An inbox with warmup switched off reports no reputation, so you cannot evaluate it at all. Treat it as HELD: excluded from capacity, not attached to campaigns, reported for a human. Do not silently turn warmup back on to make the number appear — that rewrites the very signal you are measuring. A shortfall caused by holds is a finding to report, never a reason to release the holds.


Capacity and the buy plan

Cancelling without replacing is how you wake up at half your send goal.

goal            = your daily send target (emails/day)
inbox capacity  = 30/day per Google inbox, 30/day per Outlook inbox, ~15/day per SMTP inbox
domain capacity = 30 x (actual inbox count on that domain)     <- never a flat per-domain number
active capacity = sum of domain capacity over Active domains
insurance target = 50% of goal, held warm in reserve

⛔ Count capacity per inbox, never per domain. "150/day per domain" is the single most common capacity error — it assumes 5 inboxes on every domain and silently over-reports a 2-inbox domain by 90/day. Multiply the real inbox count.

Then:

  1. Promote from Insurance until active capacity >= goal — oldest domain first. Age is trust; spend the oldest reserve, keep the newest warming.
  2. Demote Active → Insurance when active capacity is ≥ 1.25x goal. Paying to send more than you planned is still paying.
  3. Buy when goal + 50% insurance exceeds what you have after promotion:
capacity_short   = (goal * 1.5) - (active + insurance capacity, post-promotion)
inboxes_to_buy   = ceil(capacity_short / 30)
domains_to_buy   = ceil(inboxes_to_buy / inboxes_per_domain)     # typically 3

Express the ask to a human in emails/day of capacity, not in domain count — "we are 900/day short" is a decision; "we need 10 domains" is an implementation detail they cannot sanity-check.

Buy new domains with /zapmail-domain-setup-public, then warm them via /smartlead-inbox-manager. New domains are not capacity for at least 14 days of warmup plus the 30-day judging age — plan the buy a month before you need the sends.


Spend math — is this domain worth its subscription?

Item Typical
Inbox subscription $2.00-$2.60 / inbox / month
Domain registration ~$10-12 / year (cap what you will pay; cheap TLDs are fine for sending)
A 3-inbox domain ~$6-8 / month, ~90 sends/day, ~2,700 sends/month

At 1% reply that is ~27 replies/month from a $7 domain. At 0.2% it is ~5. The cancel line is not an aesthetic judgment — it is the point where the subscription stops paying for itself.

Already dead, do not pay to cancel carefully: a domain whose warmup is off and whose SMTP connection fails is not sending anything. Cancel the subscription and move on; it needs no swap-in.


The weekly loop

Step 1 — Pull fresh data (read-only)
npx tsx scripts/plan-lifecycle.ts --goal=2000 --out=./lifecycle-$(date +%F)

⛔ Always pull live. Any cached or mirrored "last 7 days" column in your own database goes stale silently and you will never notice — the plan just quietly starts condemning healthy domains. The API window is the only truth.

The script pulls per-domain sent/replied/bounced for 7d and 14d, joins every inbox (age, tags, warmup state, reputation, SMTP health), applies the ladder above, and writes:

  • plan.csv — one row per domain, with verdict and every number that forced it
  • actions.csv — only the rows where something should change
  • a printed summary

It calls no write endpoint. You can run it on someone else's account without risk.

Step 2 — Report

Summarize for the human:

  • Totals: cancels / demotes / promotes / buys
  • Capacity vs goal, per client or per program, flagging anything under 90% coverage
  • KEEP_BELOW_THRESHOLD rows — below the line but with no reserve to swap in (this is the "buy first" list)
  • Watch list: zero-reply domains with 100-199 sends, which will cross the floor next week
  • Anything with no goal set or no insurance pool at all
Step 3 — HUMAN APPROVAL GATE

Ask plainly: "Does this plan look right? Should I apply it?" Wait for an explicit yes. Never apply on a maybe.

Ask the second question too, separately: "Tags only, or also cancel the subscriptions at the provider?" These are different blast radii. Tag changes are reversible in seconds; a provider cancellation may not be reversible at all once the billing period lapses.

Ask the third question: "Any domain provisioned recently that should be held back from promotion?" If your tool has no Warmup status, this question is the only thing standing between a two-week-old domain and a live campaign.

Step 4 — Snapshot, then apply

⛔ Snapshot before you apply. Write the current tag/status of every domain the plan touches to a dated file first. Nothing in the apply path records prior state, and without a snapshot there is no clean undo.

npx tsx scripts/plan-lifecycle.ts --goal=2000 --out=./lifecycle-$(date +%F) --snapshot
# review, get the yes, then:
npx tsx scripts/apply-lifecycle.ts --actions=./lifecycle-$(date +%F)/actions.csv --apply

Without --apply the apply script is a dry run: it prints exactly what it would change and exits. Run the dry run every single time and read the counts before you trust them.

Provider cancellations are not in this script on purpose. Do them deliberately, in batches you can see, after the tag changes have settled. See the cancellation rules below.

Step 5 — Verify (mandatory before reporting success)

A 200 OK is not proof. Read the state back.

  1. Re-fetch ~10 mutated domains and confirm the tag actually changed.
  2. Re-run the capacity query and report coverage vs goal. Anything under 90% is an open item.
  3. If you cancelled at a provider, read the subscription state back from the provider. Never report a cancel batch as successful off the cancel response alone.
Step 6 — Rollback, if you regret it

Reversibility has a clock. Tag changes are instant to undo from the snapshot. A scheduled provider removal is usually reversible only while it is still scheduled — once the period lapses the domain and its reputation are gone. Decide fast, and check the provider's revert path exists before you fire the cancel, not after.


Cancellation rules (the part that costs real money)

  1. ⛔ Never cancel a domain under 30 days old, at any reply rate.
  2. ⛔ Never cancel on a partial sample. Under 200 sends the verdict is "unknown".
  3. ⛔ Never bulk-cancel by pattern match. Cancel an explicit, enumerated list of domain IDs you have read back. A partial-match cancel on a provider API has taken out an order of magnitude more domains than intended; the blast radius of a wrong filter here is your whole fleet.
  4. ⛔ Never blind-retry a provider cancel. If the response is ambiguous, read the subscription state back before retrying. Retrying a cancel that succeeded has cancelled neighbouring subscriptions on real provider APIs.
  5. ⛔ Never un-cancel a domain that is replying well just to hit a spend target, and never cancel one just because a domain is idle — idle is what Insurance is for.
  6. Cancel in whole domains. Partial-domain cancellation leaves you sending from a domain you have condemned.
  7. Batch small, verify each batch. Ten domains, read back, then the next ten. A cancel run is not the place to discover your filter was wrong.
  8. Keep the domain registration if the domain is worth re-warming later; cancel only the inbox subscription. Registration is ~$1/month, an inbox is ~$2.50 each.

Failure modes

  • Stale mirrored metrics. Any *_7d column you maintain yourself drifts silently. Pull live.
  • Flat per-domain capacity. See the capacity section — multiply real inbox counts.
  • min() over a domain's statuses. If a domain has mixed inbox statuses, aggregate by restrictiveness (Warmup > Cancel > Insurance > Active). Alphabetical min() returns "Active" and lets a mostly-warming domain be judged and cancelled.
  • Judging bounces per client instead of per domain. A client-level bounce split tells you nothing about which domain to cancel. Attribute bounces to the sending domain first.
  • Old campaign history polluting the window. Bounce/reply rosters often return a campaign's entire history with no date parameter. Filter client-side on the sent date, or bounces from a completed campaign six months ago will drive this week's cancel list.
  • Page size over 100. Most Smartlead endpoints silently return zero rows above limit=100, which reads as "no data" instead of an error. The domain-wise analytics endpoint is the exception and accepts larger pages.
  • Judging a domain that is not Active. If an Insurance or Cancel domain shows sends, that is an infrastructure bug to report, not a deliverability verdict.

What to do next

  • Cancels approved and applied: buy replacements now, not when you feel the shortfall — /zapmail-domain-setup-public, then warm for 14 days via /smartlead-inbox-manager.
  • Plan shows no cancels and you are at goal: nothing to do. Re-run next week.
  • Plan is mostly INSUFFICIENT_DATA: your fleet is bigger than your send volume. Consolidate onto fewer domains before you buy more.
  • A domain is bad but you don't know why: /email-deliverability-audit before you cancel it — a fixable DNS problem looks exactly like a burned domain in the reply rate.
  • /email-deliverability-audit — why a domain is failing (auth, placement, bounce codes)
  • /deliverability-incident-response — acute triage when something breaks today
  • /smartlead-inbox-manager — tags, warmup, signatures; the mechanics this skill plans
  • /zapmail-domain-setup-public — buying and provisioning the replacements
  • /cold-email-weekly-rhythm — where this loop sits in the week
  • /deliverability-test-public — reply/bounce comparison by inbox type

Scripts

  • scripts/plan-lifecycle.ts — read-only planner: pulls live metrics, applies the ladder, writes plan.csv + actions.csv
  • scripts/apply-lifecycle.ts — dry-run by default; applies tag changes from actions.csv on --apply

References

  • references/decision-ladder.md — the full gate-by-gate tree, verdict table, and what each verdict hands off to
  • references/cancellation-safety.md — provider cancellation procedure, batching, readback, rollback windows
1---
2name: inbox-lifecycle-manager
3description: The weekly keep/cancel/promote/buy loop for a cold email sending fleet. Decides which domains are burned and should be cancelled, which warmed reserves to promote into campaigns, which capacity to demote, and how many new domains to buy — using measured reply, bounce, age and send-volume floors instead of gut feel. Use when asked "which inboxes should I cancel", "what should I replace this week", "my fleet costs too much", "I'm under my send goal", or as the Friday hygiene routine.
4---
5 
6# Inbox Lifecycle Manager
7 
8**Diagnosis tells you a domain is sick. This skill decides whether it dies.**
9 
10Every cold email fleet leaks money in two directions at once: you keep paying for burned domains
11that will never reply again, and you run under your send goal because nobody promoted the warmed
12reserves sitting idle. This is the weekly loop that fixes both, with an approval gate in the
13middle so nothing gets cancelled on a hunch.
14 
15**Read-only by default.** The plan step writes nothing, anywhere. Only the apply step, run after
16an explicit human "yes", changes a tag, a campaign, or a provider subscription.
17 
18## When to use
19 
20- The weekly hygiene routine (Friday morning is the natural slot — see `/cold-email-weekly-rhythm`)
21- "Which inboxes should I cancel?" / "What's burned?" / "Clean up my domains"
22- "I'm under my daily send goal" / "How many domains do I need to buy?"
23- "My inbox bill is too high" — the spend section prices every domain against what it earns
24- After `/email-deliverability-audit` or `/deliverability-incident-response` hands you a
25 `SENDER_BURNED` verdict and you need to act on it
26 
27**Not this skill:** *why* a domain is bad (`/email-deliverability-audit`), fixing DNS
28(`/zapmail-domain-setup-public`), an acute incident (`/deliverability-incident-response`),
29warmup settings and signatures (`/smartlead-inbox-manager`).
30 
31---
32 
33## The status model
34 
35Four statuses, and they apply to a **whole domain**, never to one inbox inside it.
36 
37| Status | Meaning | Warmup | In campaigns |
38|---|---|---|---|
39| **Warmup** | Newly provisioned, under 14 days of warmup | ON | no |
40| **Insurance** | Warmed, healthy, idle — the reserve pool | ON | no |
41| **Active** | Sending in live campaigns | OFF or minimal | yes |
42| **Cancel** | Condemned; stop sending, cancel the subscription | OFF | no |
43 
44⛔ **Never split a domain's inboxes across statuses.** Mailbox providers judge reputation at the
45domain level. Half a domain "Active" and half "Cancel" means you are still sending from a domain
46you have declared burned, and the healthy half inherits the reputation of the dead half. Every
47decision below is computed per domain and applied to every inbox on it.
48 
49⛔ **Warmup domains are untouchable.** Not counted as capacity, not promotable, not demotable,
50not cancellable. If your tool has no `Warmup` tag, the domain's age is your only protection —
51see the age guard below, and keep a list of domains provisioned in the last 30 days.
52 
53In Smartlead these statuses are tags. See `/smartlead-inbox-manager` for the tagging mechanics.
54 
55---
56 
57## Sample-size floors — the rule that is broken most often
58 
59A domain is only judged once there is enough data to judge it. Below the floor the answer is
60**"we don't know yet"**, and that is a legitimate output, not a failure.
61 
62| Judgment | Floor | Why |
63|---|---|---|
64| Reply rate | **200 sends** in the window | At a 1% line, 200 sends is ~2 expected replies. Below 200, a 0% reply rate cannot distinguish a burned domain from an unlucky week. |
65| Bounce rate | **50 sends** | Below that, one dead address reads as 2% and two read as 4% — straight through a 3% threshold. |
66| Bounce *composition* | **30 classified bounces for that domain** | A domain with no sampled bounces has a 0% sender-originated share by construction, which looks identical to a rotten list. |
67| Domain age | **30 days** | A domain that has not finished ramping has not earned a verdict. |
68| Placement test | 100-300 senders per test | Smaller samples swing double digits run to run. |
69 
70⛔ **Unresolvable age counts as young.** If you cannot date a domain, it is `TOO_YOUNG` and it is
71not cancelled. Fail closed: you can always cancel it next week.
72 
73---
74 
75## The decision ladder
76 
77Run top to bottom per domain, over a **7-day window** (fall back to 14 days if 7 days is under the
78200-send floor). The first gate that matches wins.
79 
80```
81G0. Status is Warmup, or any inbox on the domain is in Warmup?
82 -> EXCLUDED. Not judged. Stop.
83 
84G1. Domain age < 30 days, or age unknown?
85 -> TOO_YOUNG. Never a cancel candidate at any reply rate. Stop.
86 
87G2. sent < 200 in the window?
88 -> INSUFFICIENT_DATA. Do not judge the reply rate. Re-check next week. Stop.
89 
90G3. sent >= 50 AND bounce_rate > 3%?
91 -> BOUNCE_FORK. This is not a cancel decision yet — a high bounce rate is usually the
92 LIST, not the domain. Hand to /email-deliverability-audit to classify the bounce
93 codes before you condemn anything. A domain is only cancelled out of this fork when
94 the bounces are majority sender-originated AND the DNS auth is already clean.
95 (>20% of classified bounces in the blocklist codes 5.7.606-5.7.614, or DSN text
96 naming a blocklist, means burned regardless of the bounce rate.)
97 
98G4. reply_rate >= 1.0%?
99 -> KEEP. Healthy enough to stay Active. Stop.
100 
101G5. reply_rate < 1.0% (or 0 replies on >= 150 sends), age >= 30d, bounce not the story?
102 -> CANCEL CANDIDATE. The domain has had a fair sample and did not reply. It is burned.
103 BUT: only cancel if you have an Insurance domain to swap in, or you are deliberately
104 shrinking. Otherwise -> KEEP_BELOW_THRESHOLD, flagged, and buy replacements first.
105```
106 
107### The numbers, and where they come from
108 
109| Threshold | Value | Note |
110|---|---|---|
111| Cancel line (reply) | **< 1.0%** over 7d at ≥200 sends | The line below which a domain stops earning its subscription. |
112| Zero-reply cancel | **0 replies at ≥150 sends** | A 0% rate is decisive earlier than a low-but-nonzero one. |
113| Healthy | **≥ 1.0%** | Keep. |
114| Genuinely good | **≥ 1.5%** | Full marks. Do not cancel anything near this. |
115| Bounce fork / autopause | **> 3%** at ≥50 sends | Same number your sending tool should use for `bounce_autopause_threshold`. |
116| Blocklist share | **> 20%** of classified bounces | Burned regardless of bounce rate. |
117| Minimum age | **30 days** | Hard floor, no exceptions. |
118| Placement | **≥85% inbox = healthy**, 70-84% degraded, **<70% fails** | From a real seed placement test, not a guess. |
119| Warmup reputation | **< 98% = pull the inbox** | If the tool reports it live. |
120 
121> **One number, everywhere in this repo: 1%.** A domain that cannot reply at 1% after a fair
122> sample is not earning its subscription. The same 1% that flags a domain in
123> `/email-deliverability-audit` is the line that cancels it here — the difference between "flag"
124> and "cancel" is not a second threshold, it is the **floors**: 200 sends and 30 days of age.
125> Below those, 1% means nothing and nothing gets cancelled.
126 
127### Warmup-off is "held", not "healthy"
128 
129An inbox with warmup switched off reports no reputation, so you cannot evaluate it at all. Treat
130it as **HELD**: excluded from capacity, not attached to campaigns, reported for a human. Do not
131silently turn warmup back on to make the number appear — that rewrites the very signal you are
132measuring. A shortfall caused by holds is a finding to report, never a reason to release the holds.
133 
134---
135 
136## Capacity and the buy plan
137 
138Cancelling without replacing is how you wake up at half your send goal.
139 
140```
141goal = your daily send target (emails/day)
142inbox capacity = 30/day per Google inbox, 30/day per Outlook inbox, ~15/day per SMTP inbox
143domain capacity = 30 x (actual inbox count on that domain) <- never a flat per-domain number
144active capacity = sum of domain capacity over Active domains
145insurance target = 50% of goal, held warm in reserve
146```
147 
148⛔ **Count capacity per inbox, never per domain.** "150/day per domain" is the single most common
149capacity error — it assumes 5 inboxes on every domain and silently over-reports a 2-inbox domain
150by 90/day. Multiply the real inbox count.
151 
152Then:
153 
1541. **Promote** from Insurance until `active capacity >= goal` — **oldest domain first**. Age is
155 trust; spend the oldest reserve, keep the newest warming.
1562. **Demote** Active → Insurance when active capacity is **≥ 1.25x goal**. Paying to send more
157 than you planned is still paying.
1583. **Buy** when `goal + 50% insurance` exceeds what you have after promotion:
159 
160```
161capacity_short = (goal * 1.5) - (active + insurance capacity, post-promotion)
162inboxes_to_buy = ceil(capacity_short / 30)
163domains_to_buy = ceil(inboxes_to_buy / inboxes_per_domain) # typically 3
164```
165 
166Express the ask to a human in **emails/day of capacity**, not in domain count — "we are 900/day
167short" is a decision; "we need 10 domains" is an implementation detail they cannot sanity-check.
168 
169Buy new domains with `/zapmail-domain-setup-public`, then warm them via `/smartlead-inbox-manager`.
170New domains are not capacity for **at least 14 days of warmup plus the 30-day judging age** — plan
171the buy a month before you need the sends.
172 
173---
174 
175## Spend math — is this domain worth its subscription?
176 
177| Item | Typical |
178|---|---|
179| Inbox subscription | $2.00-$2.60 / inbox / month |
180| Domain registration | ~$10-12 / year (cap what you will pay; cheap TLDs are fine for sending) |
181| A 3-inbox domain | ~$6-8 / month, ~90 sends/day, ~2,700 sends/month |
182 
183At 1% reply that is ~27 replies/month from a $7 domain. At 0.2% it is ~5. The cancel line is not
184an aesthetic judgment — it is the point where the subscription stops paying for itself.
185 
186**Already dead, do not pay to cancel carefully:** a domain whose warmup is off *and* whose SMTP
187connection fails is not sending anything. Cancel the subscription and move on; it needs no swap-in.
188 
189---
190 
191## The weekly loop
192 
193### Step 1 — Pull fresh data (read-only)
194 
195```bash
196npx tsx scripts/plan-lifecycle.ts --goal=2000 --out=./lifecycle-$(date +%F)
197```
198 
199⛔ **Always pull live.** Any cached or mirrored "last 7 days" column in your own database goes
200stale silently and you will never notice — the plan just quietly starts condemning healthy
201domains. The API window is the only truth.
202 
203The script pulls per-domain sent/replied/bounced for 7d and 14d, joins every inbox (age, tags,
204warmup state, reputation, SMTP health), applies the ladder above, and writes:
205 
206- `plan.csv` — one row per domain, with verdict and every number that forced it
207- `actions.csv` — only the rows where something should change
208- a printed summary
209 
210It calls no write endpoint. You can run it on someone else's account without risk.
211 
212### Step 2 — Report
213 
214Summarize for the human:
215 
216- Totals: cancels / demotes / promotes / buys
217- Capacity vs goal, **per client or per program**, flagging anything under 90% coverage
218- `KEEP_BELOW_THRESHOLD` rows — below the line but with no reserve to swap in (this is the "buy
219 first" list)
220- Watch list: zero-reply domains with 100-199 sends, which will cross the floor next week
221- Anything with no goal set or no insurance pool at all
222 
223### Step 3 — HUMAN APPROVAL GATE
224 
225Ask plainly: *"Does this plan look right? Should I apply it?"* Wait for an explicit yes. Never
226apply on a maybe.
227 
228Ask the second question too, separately: **"Tags only, or also cancel the subscriptions at the
229provider?"** These are different blast radii. Tag changes are reversible in seconds; a provider
230cancellation may not be reversible at all once the billing period lapses.
231 
232Ask the third question: **"Any domain provisioned recently that should be held back from
233promotion?"** If your tool has no Warmup status, this question is the only thing standing between
234a two-week-old domain and a live campaign.
235 
236### Step 4 — Snapshot, then apply
237 
238⛔ **Snapshot before you apply.** Write the current tag/status of every domain the plan touches to
239a dated file first. Nothing in the apply path records prior state, and without a snapshot there is
240no clean undo.
241 
242```bash
243npx tsx scripts/plan-lifecycle.ts --goal=2000 --out=./lifecycle-$(date +%F) --snapshot
244# review, get the yes, then:
245npx tsx scripts/apply-lifecycle.ts --actions=./lifecycle-$(date +%F)/actions.csv --apply
246```
247 
248Without `--apply` the apply script is a dry run: it prints exactly what it would change and exits.
249Run the dry run every single time and read the counts before you trust them.
250 
251Provider cancellations are **not** in this script on purpose. Do them deliberately, in batches you
252can see, after the tag changes have settled. See the cancellation rules below.
253 
254### Step 5 — Verify (mandatory before reporting success)
255 
256A `200 OK` is not proof. Read the state back.
257 
2581. Re-fetch ~10 mutated domains and confirm the tag actually changed.
2592. Re-run the capacity query and report coverage vs goal. Anything under 90% is an open item.
2603. If you cancelled at a provider, read the subscription state back from the provider. Never
261 report a cancel batch as successful off the cancel response alone.
262 
263### Step 6 — Rollback, if you regret it
264 
265Reversibility has a clock. Tag changes are instant to undo from the snapshot. A scheduled provider
266removal is usually reversible **only while it is still scheduled** — once the period lapses the
267domain and its reputation are gone. Decide fast, and check the provider's revert path exists
268*before* you fire the cancel, not after.
269 
270---
271 
272## Cancellation rules (the part that costs real money)
273 
2741. ⛔ **Never cancel a domain under 30 days old**, at any reply rate.
2752. ⛔ **Never cancel on a partial sample.** Under 200 sends the verdict is "unknown".
2763. ⛔ **Never bulk-cancel by pattern match.** Cancel an explicit, enumerated list of domain IDs you
277 have read back. A partial-match cancel on a provider API has taken out an order of magnitude
278 more domains than intended; the blast radius of a wrong filter here is your whole fleet.
2794. ⛔ **Never blind-retry a provider cancel.** If the response is ambiguous, read the subscription
280 state back before retrying. Retrying a cancel that succeeded has cancelled neighbouring
281 subscriptions on real provider APIs.
2825. ⛔ **Never un-cancel a domain that is replying well** just to hit a spend target, and never
283 cancel one just because a domain is idle — idle is what Insurance is for.
2846. **Cancel in whole domains.** Partial-domain cancellation leaves you sending from a domain you
285 have condemned.
2867. **Batch small, verify each batch.** Ten domains, read back, then the next ten. A cancel run is
287 not the place to discover your filter was wrong.
2888. **Keep the domain registration** if the domain is worth re-warming later; cancel only the inbox
289 subscription. Registration is ~$1/month, an inbox is ~$2.50 each.
290 
291---
292 
293## Failure modes
294 
295- **Stale mirrored metrics.** Any `*_7d` column you maintain yourself drifts silently. Pull live.
296- **Flat per-domain capacity.** See the capacity section — multiply real inbox counts.
297- **`min()` over a domain's statuses.** If a domain has mixed inbox statuses, aggregate by
298 *restrictiveness* (Warmup > Cancel > Insurance > Active). Alphabetical `min()` returns "Active"
299 and lets a mostly-warming domain be judged and cancelled.
300- **Judging bounces per client instead of per domain.** A client-level bounce split tells you
301 nothing about which domain to cancel. Attribute bounces to the sending domain first.
302- **Old campaign history polluting the window.** Bounce/reply rosters often return a campaign's
303 *entire* history with no date parameter. Filter client-side on the sent date, or bounces from a
304 completed campaign six months ago will drive this week's cancel list.
305- **Page size over 100.** Most Smartlead endpoints silently return zero rows above `limit=100`,
306 which reads as "no data" instead of an error. The domain-wise analytics endpoint is the
307 exception and accepts larger pages.
308- **Judging a domain that is not Active.** If an Insurance or Cancel domain shows sends, that is an
309 infrastructure bug to report, not a deliverability verdict.
310 
311---
312 
313## What to do next
314 
315- **Cancels approved and applied:** buy replacements now, not when you feel the shortfall —
316 `/zapmail-domain-setup-public`, then warm for 14 days via `/smartlead-inbox-manager`.
317- **Plan shows no cancels and you are at goal:** nothing to do. Re-run next week.
318- **Plan is mostly `INSUFFICIENT_DATA`:** your fleet is bigger than your send volume. Consolidate
319 onto fewer domains before you buy more.
320- **A domain is bad but you don't know why:** `/email-deliverability-audit` before you cancel it —
321 a fixable DNS problem looks exactly like a burned domain in the reply rate.
322 
323## Related skills
324 
325- `/email-deliverability-audit` — *why* a domain is failing (auth, placement, bounce codes)
326- `/deliverability-incident-response` — acute triage when something breaks today
327- `/smartlead-inbox-manager` — tags, warmup, signatures; the mechanics this skill plans
328- `/zapmail-domain-setup-public` — buying and provisioning the replacements
329- `/cold-email-weekly-rhythm` — where this loop sits in the week
330- `/deliverability-test-public` — reply/bounce comparison by inbox type
331 
332## Scripts
333 
334- `scripts/plan-lifecycle.ts` — read-only planner: pulls live metrics, applies the ladder, writes `plan.csv` + `actions.csv`
335- `scripts/apply-lifecycle.ts` — dry-run by default; applies tag changes from `actions.csv` on `--apply`
336 
337## References
338 
339- `references/decision-ladder.md` — the full gate-by-gate tree, verdict table, and what each verdict hands off to
340- `references/cancellation-safety.md` — provider cancellation procedure, batching, readback, rollback windows
341 

Discussion

Alternatives