winback-engine

Find dormant subscribers, segment by historical value, draft personalized winback emails per tier, and identify suppression candidates

SKILL.md

GitHub

Winback Engine

Turn dormant subscribers back into customers. This skill scans your connected ESP for inactive subscribers, segments them by historical value, drafts a personalized winback series per tier, and flags who you should suppress before they tank your deliverability.

Requires: Cogny MCP + a connected ESP. Sign up

The output is a complete, ready-to-run winback campaign — not a report.

Usage

/winback-engine — standard mode (60-day dormancy threshold) /winback-engine aggressive — 30-day threshold (acts earlier, bigger list) /winback-engine conservative — 90-day threshold (only chronic dormants)

Step 0: Is Cogny connected?

If no mcp__cogny__* tools exist (no list_integrations, no cogny_status), print this and stop:

Winback Engine needs your marketing data through Cogny's MCP gateway.

1. Sign up at https://cogny.com and connect your ESP (Klaviyo, Mailchimp, Rule, Get a Newsletter, Brevo, MailerLite, Drip, Salesforce Marketing Cloud or Braze); Shopify, WooCommerce or Stripe adds purchase value.
2. Add Cogny to .mcp.json in your project:
   {"mcpServers": {"cogny": {"type": "http", "url": "https://app.cogny.com/mcp"}}}
3. Restart Claude Code; it opens a browser for the OAuth login on first use.

Nothing to run without Cogny.

Prerequisites Check

Detect connected ESP (same pattern as subject-line-lab; on Cloud, mcp__cogny__list_integrations). If Cogny is connected but no ESP is, say "connect your ESP in Cogny" and stop. Also note whether a store (Shopify, WooCommerce) or, for SaaS, Stripe is connected — it supplies purchase data for ESPs that have none.

Health check: health_status misses revoked tokens, so make one cheap read per integration before relying on it (e.g. Mailchimp tool_get_account_info, Get a Newsletter tool_list_senders, Klaviyo get_account, Shopify tool_list_customers with a small page, Stripe stripe_api_read GetSubscriptions with limit: 1). A 401, Forbidden or scope error means 🔴 "reconnect <integration> in Cogny": name it, don't retry, and continue with what remains.

Missing tool: if a tool named here isn't exposed by the workspace, call mcp__cogny__refresh_mcp_discovery once, use the fallback named next to the tool for this run, mark that source ⚠ "needs reconnect", and tell the user to reconnect the Cogny MCP. Never loop on refresh. Get a Newsletter fallbacks: tool_get_report_have_not_opened → that report's list contacts minus the openers from tool_get_report_opens; tool_get_report_link_clicks → tool_get_report_opens (per-contact click counts) or tool_get_report_clicks (per-link totals); tool_get_report_opens_aggregated → the totals in tool_get_report. Mailchimp tool_get_campaign_content → tool_get_campaign for the headers, and ask the user for the body.

ESP connected but 0 sends in the window: skip it with a one-line note ("Rule: connected, 0 sends in the last <threshold> days — no engagement to tier; see step 2 for never-mailed contacts").

Works with

ESP (Prefix) Subscribers + engagement Purchase / LTV Suppression write (asks first) Coverage
Klaviyo (Klaviyo) list_profiles, get_profile list_events (Placed Order metric) unsubscribe_profiles Full
Mailchimp (Mailchimp) tool_list_members store join tool_update_member Full, engagement-tiered
Rule (Rule) tool_list_subscribers, tool_list_suppressions store join tool_update_subscriber Full, engagement-tiered
Get a Newsletter (Get_a_Newsletter) tool_list_contacts (no engagement fields) + tool_get_report_opens (paged, per report) to rebuild engagement store join tool_update_contact / tool_delete_contact Partial: last-engaged is rebuilt from recent reports only, so it can't see engagement older than the reports you page through
Brevo (Brevo) list_contacts (modified_since), get_contact (per-contact statistics with open/click events) store join update_contact (email_blacklisted: true) Full, but last-engaged needs one get_contact per contact — sample if the list is large
MailerLite (MailerLite) list_subscribers (filter_status: "active"): lifetime opens_count, clicks_count, open_rate, subscribed_at store join upsert_subscriber (status: "unsubscribed") No last-open date: tier on lifetime rates + tenure, and say so
Drip (Drip) list_subscribers (status: "active", tags) subscriber lifetime_value (when a store integration feeds Drip) unsubscribe_from_all No per-subscriber last-open date; use lifetime_value + tags
Salesforce Marketing Cloud (Salesforce_Marketing_Cloud) list_data_extensions → get_data_extension_fields → get_data_extension_rows (row_filter) on an engagement or customer data extension same, if the data extension holds orders none in the MCP; hand over the list Only if such a data extension exists; rows are PII, fetch only what's needed
Braze (Braze) braze_list_segments → braze_get_segment_details / braze_get_segment_analytics (sizes only) — none in the MCP Partial: sizes existing lapsed segments; can't list users, so tiers and the suppression list are recommendations to build in Braze
Symplify, Drip broadcasts — — — Symplify has no bulk contact read (only get_contact by id), so winback isn't possible there

Store join (any ESP): mcp__cogny__mcp__Shopify__tool_list_customers returns numberOfOrders and amount_spent per customer email; WooCommerce list_orders (after, status: "completed") gives order history. For SaaS, Stripe stripe_api_read GetSubscriptions (status: "all") gives active vs cancelled customers and GetInvoices (status: "paid") the billed total per customer (LTV); treat cancelled subscribers as past purchasers. Join on lowercased email to turn engagement-only tiers into value tiers. If Stripe's email/customer_email comes back [REDACTED] (a restricted key), skip the Stripe join and say so ("Stripe emails redacted by the restricted key: tiers stay engagement-only"). Page Stripe with limit 20–25 and starting_after (a 100-row page is ~370KB). Name the source ("LTV from Shopify"), use the store currency, and never mix ESP-attributed revenue with store revenue.

Steps

1. Define dormancy thresholds

Mode Dormant Deep dormant Suppress candidate
aggressive 30+ days no open 90+ days no open 180+ days no open/click, no purchase
standard 60+ days 180+ days 365+ days no engagement
conservative 90+ days 270+ days 540+ days

2. Pull subscriber data

From the connected ESP, pull every active subscriber with:

  • Subscribe date
  • Last open date + last click date (take max as last_engaged)
  • Total historical opens / clicks
  • Signup source / list
  • Tags / segments
  • Email domain (gmail, yahoo, hotmail, corporate)

Purchase / LTV data — Klaviyo, Drip (lifetime_value), or a connected store. The cogny-mcp-proxy exposes purchase data in Klaviyo (via list_events filtered to the "Placed Order" metric, summing the value field per profile) and Drip. With Shopify or WooCommerce connected, use the store join above for any ESP. Otherwise Mailchimp, Rule, Get a Newsletter, Brevo and MailerLite are engagement-only — they do not expose:

  • Total spend / LTV
  • Order count
  • AOV
  • Last order date

Get a Newsletter engagement rebuild: tool_list_contacts returns no open or click fields. Rebuild them per contact: list the sends in the window with tool_list_sent (each row has subject, send time and report_id), skip test sends (under 10 recipients or "test" in the subject), then page through tool_get_report_opens for each report_id (each row: email, opens, clicks). A contact's last_engaged is the send time of the latest report they appear in; lifetime opens/clicks are the sums. tool_get_report_opens returns only openers, so "mailed but never opened" has to be derived: use tool_get_report_have_not_opened if the workspace exposes it; otherwise take the contacts on the lists that send targeted (tool_get_sent, matched against each contact's lists) whose created is before the send time, minus that report's openers. Cover at least the deep-dormant threshold (e.g. 180 days in standard mode) and state the date range, because engagement older than the oldest report read is invisible.

Last send older than the dormancy threshold: if the ESP's most recent send is older than the dormant threshold, every contact is dormant by default. That says nothing about the people; it means the program stopped sending. Say so up front, tier only contacts who were actually mailed, and treat the cadence gap as the main finding.

Never-mailed contacts: contacts created after the last send (Get a Newsletter: tool_list_contacts with created_gt = last send time), or on lists no send has targeted, have never received an email. They aren't dormant, so keep them out of the tiers and the suppression list: count them separately and route them to /welcome-series.

Per-ESP tool adapter:

ESP Subscriber list Purchase history Suppression write
Klaviyo list_profiles (+ get_profile for detail) list_events with metric="Placed Order" — use value field per profile for LTV unsubscribe_profiles
Mailchimp tool_list_members (filter status=subscribed) Not available tool_update_member (status=unsubscribed)
Rule tool_list_subscribers Not available tool_update_subscriber / tool_list_suppressions
Get a Newsletter tool_list_contacts + tool_get_report_opens (engagement rebuild above) Not available tool_delete_contact or tool_update_contact
Brevo list_contacts + get_contact Not available (store join) update_contact (email_blacklisted)
MailerLite list_subscribers Not available (store join) upsert_subscriber (status)
Drip list_subscribers lifetime_value on the subscriber unsubscribe_from_all
SFMC get_data_extension_rows only if a data extension holds it not exposed
Braze segment sizes only not exposed per user not exposed

3. Segment into tiers

If Klaviyo, Drip with lifetime_value, or any ESP + store join (has purchase data): cross-cut by engagement recency × historical value:

Tier Criteria Tone Offer
A — High value dormant Dormant + LTV ≥ top quartile OR past purchaser Personal, apologetic, premium Strong (20% off, free shipping, exclusive access). This is real money.
B — Medium value dormant Dormant + some past engagement or 1 purchase Warm, value-forward Moderate (10–15% off, bundle, content)
C — Low engagement dormant Dormant + never purchased + low engagement history Direct, honest Last-chance offer or content-only
D — Suppression candidates Deep dormant + no purchase + no recent engagement Final email only "We're removing you unless..."

If engagement-only (Mailchimp / Rule / Get a Newsletter / Brevo / MailerLite with no store connected): tier by engagement depth instead. LTV-based tiering falls back to:

Tier Criteria Tone Offer
A — Deep engaged dormant Dormant now, but historically high open+click rate (top quartile of clicks) Personal, "we miss you" Strong offer or exclusive content
B — Casual dormant Dormant, moderate historical engagement Warm, value-forward Modest offer
C — Low engagement dormant Dormant, few lifetime opens/clicks Direct, honest Content-only or re-opt-in ask
D — Suppression candidates Deep dormant, minimal lifetime engagement Final email only Goodbye

Revenue estimates in the output should be omitted for engagement-only ESPs, or marked estimated_impact_usd: null on findings with a note that the user can pair with their own LTV data to size the opportunity.

Output the tier sizes before drafting:

Winback candidates: <total>
├─ Tier A (high-value dormant):   <count> subscribers (est. revenue: $<X>)
├─ Tier B (medium):               <count>
├─ Tier C (low engagement):       <count>
└─ Tier D (suppression):          <count>  ← do not keep sending to these

Never mailed (not tiered):        <count>  → run /welcome-series

Revenue estimate for Tier A/B: count × historical AOV × expected reactivation rate (use 3-5% for Tier A, 1-2% for Tier B).

4. Draft a 3-email winback series per tier

For each tier (A, B, C), generate 3 emails with cadence:

  • Email 1 — day 0, soft reopener ("we noticed")
  • Email 2 — day 4, value + offer
  • Email 3 — day 8, last-chance / decision

Tone by tier:

Tier A (high-value):

  • E1: Personal note, reference last purchase by name if available. No offer yet.
  • E2: Show what they're missing — new products in their category, or customer stories
  • E3: Strongest offer, explicit decision framing ("if we're not a fit anymore, we'll respect that")

Tier B (medium):

  • E1: "We miss you" + social proof
  • E2: Content-led (best of what they missed) + modest offer
  • E3: Offer + deadline

Tier C (low engagement):

  • E1: Direct — "is this still useful?"
  • E2: One-click re-subscribe or one-click unsubscribe (reduce friction)
  • E3: Final send before suppression

For every email produce: subject, preheader, body, CTA. Same format as /welcome-series.

5. Suppression list (Tier D)

Output a block the user can act on:

Suppression recommendation: <count> subscribers

These haven't opened, clicked, or purchased in <threshold> days. Keeping
them on the list:
  • Hurts deliverability (low engagement = more spam placement for everyone)
  • Inflates list size → higher ESP costs
  • Distorts reporting baselines

Recommended action:
  1. Send ONE final "goodbye" email offering resubscribe
  2. After 7 days, suppress non-responders

Sample suppression email is included below.

Then include the goodbye email draft.

6. Write-mode: offer to execute

After presenting the plan, ask:

I can push this into your ESP as:
  [A] Drafts only (you review + launch)
  [B] Full flow installed but paused (you activate when ready)
  [C] Just give me the copy, I'll handle it

Which?

If the user picks A or B and the ESP MCP has write tools available, follow the Write Action Protocol (same as the /cogny skill — present the action plan, wait for approval, then execute one step at a time).

Draft-capable writes for the newer ESPs (each asks first): Brevo create_email_campaign (omit scheduled_at to keep it a draft), MailerLite create_campaign (then schedule_campaign only on approval), Drip apply_tag to enter an existing winback workflow. SFMC and Braze have no write tools in the MCP: deliver copy + segment definitions only (option C).

7. Record findings

For each significant tier (A, B with >100 subscribers), and for a never-mailed group or a stopped send cadence, record a finding. On Cloud call create_ticket first (fields mapped per section 2 of the routing guide); on Solo call create_finding:

{
  "title": "Tier A winback: <N> high-value dormant subscribers, est. $<X> recoverable",
  "body": "Identified <N> past purchasers with LTV in top quartile who haven't engaged in <days> days. Avg historical AOV: $<Y>. At 4% reactivation, estimated winback revenue = $<X>. 3-email series drafted and ready to install.",
  "action_type": "winback_campaign",
  "expected_outcome": "Reactivate 3-5% of Tier A dormant, generate $<X> over 30 days",
  "estimated_impact_usd": <X>,
  "priority": "high"
}

And one for suppression:

{
  "title": "<N> subscribers recommended for suppression",
  "body": "Deep dormant (<threshold>+ days) with no purchases. Keeping them hurts deliverability and inflates ESP cost. Recommend goodbye email + suppression after 7 days.",
  "action_type": "list_hygiene",
  "expected_outcome": "Improve overall open rate by 1-3 pp, lower ESP tier cost",
  "estimated_impact_usd": <monthly_esp_savings>,
  "priority": "medium"
}

Notes

  • Never suppress before a winback attempt. Give Tier D subscribers one last chance to re-engage.
  • Apple MPP means "last open" is noisy for iPhone users. Weight clicks and purchases higher than opens when assigning tiers.
  • Purchase/LTV data comes from Klaviyo events, Drip lifetime_value, a connected Shopify/WooCommerce store, or Stripe for SaaS. Without one of those, Mailchimp, Rule, Get a Newsletter, Brevo and MailerLite fall back to the engagement-only tiering scheme above.
  • Several ESPs connected: tier each list separately and show them side by side. A person on two lists counts once per ESP; don't add tier sizes or revenue estimates across ESPs.
  • Get a Newsletter has no flow/automation object. "Install as a flow" isn't available — winback is delivered as scheduled individual sends via tool_send_draft with time_to_send. For Klaviyo, flows install via the Klaviyo UI (the MCP exposes list_flows/get_flow for reads only, not create). For Mailchimp/Rule, automations/journeys are read-only too — so "install" means drafting campaigns and scheduling them, not creating a native automation.

Recording findings

Use create_ticket when the workspace has it (Cloud) and create_finding otherwise (Solo). The fields differ; see section 2 of ../marketing-engineer/references/mcp-routing.md.