winback-engine — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited winback-engine (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.
/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)
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.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").
| 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.
| 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 |
From the connected ESP, pull every active subscriber with:
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:
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 |
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-seriesRevenue estimate for Tier A/B: count × historical AOV × expected reactivation rate (use 3-5% for Tier A, 1-2% for Tier B).
For each tier (A, B, C), generate 3 emails with cadence:
Tone by tier:
Tier A (high-value):
Tier B (medium):
Tier C (low engagement):
For every email produce: subject, preheader, body, CTA. Same format as /welcome-series.
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.
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).
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"
}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.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.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.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.