This is the agent that pays for all the others. "Not interested" is a lazy summary of a real conversation. Re-read each lost deal, record the actual reason and a reopen condition, and then watch for the condition:
- "Needed SSO" → the day the changelog says SSO shipped, they get a personal note quoting their call.
- "Went with a competitor, contract renews in March" → show up in January.
- "Champion left the company" → congratulate them at the new company and reintroduce yourself.
Most companies have a pile of unfinished revenue sitting in their own CRM.
Requires: Cogny ($9/mo) with a CRM connected: HubSpot, Pipedrive or Upsales (Salesforce and
others: see the CRM layer in /marketing-engineer). The steps below are written for HubSpot.
"Pipedrive and Upsales" says what changes for those CRMs. Fibbler is optional and adds
ad-engagement signals.
Step 0: Cogny check
If no mcp__cogny__* tools exist (no list_integrations, no cogny_status), print this and stop:
Closed-Lost Revival needs your marketing data through Cogny's MCP gateway.
1. Sign up at https://cogny.com and connect a CRM: HubSpot, Pipedrive or Upsales (Stripe and fibbler optional).
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
-
Call
list_integrations. This skill needs a CRM: HubSpot, Pipedrive or Upsales. -
Make one cheap read on the CRM: HubSpot
get_user_details, Pipedrivelist_users, Upsaleslist_users(limit: 1). If fibbler is connected,get_account_overview. A 401, Forbidden or "lacks scope" means 🔴 "reconnect in Cogny"; don't retry it. A broken fibbler only drops the ad-engagement signals; a broken CRM stops the run. -
If a CRM tool is missing, call
refresh_mcp_discoveryonce. New tools only appear after the client reconnects, so tell the user to reconnect the Cogny MCP and stop this run. -
No CRM connected: print this and stop. Don't fall back to
runcloser(not supported).Closed-lost revival needs a CRM. Connect HubSpot, Pipedrive or Upsales in Cogny (Integrations → Connect), then run /closed-lost-revival build again.Optional partial ledger from Stripe. If Stripe is connected, offer (don't run unasked) a "lost customers" ledger instead:
stripe_api_readGetSubscriptionswithstatus: "canceled", paged withstarting_afterand aggregated withjq(pages are large), readingcanceled_at,cancellation_details(reason,feedback,comment) and the plan price. Skip trial-only cancellations (ended_at == trial_end). Write it tobrain/evidence/closed-lost/ledgerheaded "Partial: Stripe cancellations only, no CRM", withcommentas the only[verbatim]evidence. These are churned customers, not lost deals, so there is no conversation to re-read and no draft notes.
Usage
/closed-lost-revival build— first pass over lost deals (last 18 months), creates the ledger/closed-lost-revival watch— weekly: check every open reopen condition/closed-lost-revival shipped "SSO"— a feature just shipped, so find everyone who asked for it/closed-lost-revival match BK014— lost prospects that campaign BK014's offer answers
Steps — build
1. Pull lost deals
get_user_details to confirm read access. get_properties(deals, ["dealstage","closed_lost_reason","pipeline"])
to learn the pipelines and which stages are closed-lost, plus the options on closed_lost_reason
(it's often free text, sometimes a dropdown).
search_crm_objects on deals, filters hs_is_closed = true AND hs_is_closed_won = false
AND closedate ≥ 18 months ago. Properties: dealname, amount, deal_currency_code, closedate, closed_lost_reason, dealstage, pipeline, hubspot_owner_id, hs_analytics_source, hs_analytics_source_data_1, hs_analytics_source_data_2, notes_last_contacted. Page with after until done.
Exclude non-losses: reasons or names saying duplicate, test, merged, created in error, or "moved to another pipeline". Report excluded counts by reason. If more than half the rows are noise, file one ticket ("[ME:closed-lost-revival] Clean CRM: N of M lost deals are noise") and continue with the rest.
Amounts: use deal_currency_code. Never add amounts in different currencies together.
2. Re-read each deal
Cap at 50 deals per run, newest and largest first. For each deal:
- Associated contacts and company (
get_crm_objectswith associations): name,jobtitle, companydomain,industry,numberofemployees. - The conversation:
search_crm_objectsoncalls,meetingsandnotesfiltered byassociations.deal = <dealId>(and by the main contact if the deal has none). Readhs_call_body,hs_meeting_body,hs_internal_meeting_notes,hs_note_body, and any transcript property (see/customer-language). - If fibbler is connected,
get_influenced_deals(deal_filter: "lost",crm: "hubspot") shows which campaigns touched the deal.
closed_lost_reason is the rep's one-liner, written at close. The notes are where the real
reason usually is. When they disagree, trust the notes and record both.
3. Record the real reason and a reopen condition
| Reason | Reopen condition type | Example condition |
|---|---|---|
| Missing feature | feature_shipped |
"SSO ships" |
| Timing / budget cycle | date |
"after year-end close, 2027-01-15" |
| Chose competitor | date (renewal) + signal |
"contract renews 2027-03; watch for competitor complaints" |
| Champion left / no champion | person_moved |
"the champion lands somewhere new" |
| Price | offer_change |
"a lower tier or annual discount exists" |
| Too small / bad fit | none — and add to customer-fit evidence |
— |
| Unknown | none — flag the notes as too thin to judge |
— |
Keep the evidence: the note or quote it came from, with object type, id and date, labelled
[verbatim] or [paraphrase]. Never invent a reason. If the record is too thin, say "unknown — notes too thin".
4. Write the ledger
write_context_node at brain/evidence/closed-lost/ledger:
# Closed-lost ledger — built YYYY-MM-DD
Lost deals (18 mo): N · real losses: R · excluded as noise: X · re-read: C (capped at 50)
| Deal (id) | Company | Lost | Amount (ccy) | CRM reason | Real reason | Evidence (object, id, date) | Reopen condition | Type | Watch until | Status |
Failures: <none, or what couldn't be read>
Also add brain/evidence/closed-lost/patterns: reasons ranked by count and by lost amount (per
currency), plus the gap between the CRM reason and the real reason, which is a coaching
finding in itself. If "too small" or "bad fit" leads the list, file a ticket proposing a
customer-fit update. Don't make the edit yourself.
Optional: propose (as a ticket for a HubSpot admin) two custom deal properties, reopen_condition
and reopen_date, so the team sees the conditions in the CRM. This skill never writes to the CRM.
Pipedrive and Upsales
| Step | Pipedrive (Pipedrive) |
Upsales (Upsales) |
|---|---|---|
| Lost deals | list_deals(status: "lost", updated_since: <18 months ago>), cursor-paginated; get_deal for lost_reason, lost_time, value, currency, org_id, person_id |
list_orders(filters=[{"field":"probability","comparator":"eq","value":0}], sort: "-date"); the loss-reason custom field comes from get_metadata |
| Company / people | list_organizations, list_persons(org_id) |
client.id → list_accounts, list_contacts(filters: client.id) |
| The conversation | list_activities(deal_id): subject, note |
list_activities and list_appointments filtered by client.id: notes, description |
| Open deal now? | list_deals(status: "open", org_id) |
list_opportunities(filters: client.id) |
| Last touch | newest list_activities(org_id, done: true) |
newest list_activities / list_appointments for client.id |
| Re-engagement | fibbler get_warm_accounts with crm: "pipedrive" |
list_visits for client.id (Upsales web tracking) |
| Opt-out | person marketing_status when present; otherwise HOLD |
contact unsubscribe/active fields from get_metadata; otherwise HOLD |
Fibbler's get_influenced_deals takes crm: "hubspot" | "salesforce" | "copper" | "attio", with no
Pipedrive or Upsales, so skip it there. Only get_warm_accounts accepts crm: "pipedrive". For Upsales, skip fibbler.
Steps — watch (weekly)
For each open row:
date: is it within 45 days? → duefeature_shipped: readbrain/evidence/changelog(or WebFetch the changelog URL in START-HERE)person_moved:brave_searchfor the person's name with their new title or company. Require a second identifier (old company, city, photo-free profile text) to match, because names collide.offer_change: compare againstbrain/company/offers- Re-engagement (any row): re-read the company and contacts and look for
hs_analytics_last_visit_timestampin the last 14 days, a newrecent_conversion_date(form fill), orhs_email_last_open_date/hs_email_last_click_date. If fibbler is connected,get_warm_accountstoo. Any of these bumps the priority.
Before drafting, re-check the CRM (HubSpot fields below; Pipedrive/Upsales equivalents in the table above) and skip the row if:
- the company has an open deal now (
hs_is_closed = false), or has become a customer (lifecyclestage = customer, or a won deal under any name) - a contact was touched in the last 14 days (
notes_last_contacted,hs_last_sales_activity_timestamp) - the contact has opted out:
hs_email_optout = true, a "do not contact"hs_lead_status, or unsubscribed from sales email
Respecting opt-outs and contact preferences isn't optional.
Steps — shipped ""
search_context and the ledger for rows whose reason or evidence mentions the feature
(and its synonyms). Each match is due now.
Drafting the note
For every due row, write a short personal note (≤100 words) that:
- References their original words: "Back in March you said the deal-breaker was SSO for your IT team."
Only quote
[verbatim]evidence as their words; paraphrase anything else. - States what changed, specifically.
- Makes one low-friction ask.
Use brain/brand/voice. No claims outside offers. For person_moved, congratulate them
first and lead with no pitch. Address it from the original deal owner (search_owners).
Bundle all drafts into one create_ticket ("[ME:closed-lost-revival] Approve: N closed-lost
revival notes"), each with the deal link, the condition met, the evidence and the draft. Mark the rows
drafted in the ledger. After the user sends, the next run sees the new engagement in HubSpot
and marks the row sent.
Steps — match
Read brain/campaigns/<ID>. Select ledger rows whose real reason the campaign's offer answers
(e.g. "maybe after year-end" for a year-end campaign). Draft as above and append the list to the
campaign folder under ## Closed-lost matches. If there's no CRM or no ledger, write one line
there instead: "skipped: ".
Recording findings
Detect the plan from the tools that exist: use create_ticket first when it exists (Cloud), and
create_finding only when it doesn't (Solo).
The fields differ; see section 2 of ../marketing-engineer/references/mcp-routing.md.
Critical rules
- Never invent a reason. "Unknown" is a valid answer.
- Quote only verbatim evidence as the prospect's words, with object id and date.
- Drafts only. Respect opt-outs. Skip open deals and customers.
- Read-only on the CRM. Proposed CRM fields go to a human as a ticket.
- Exclude CRM noise and report how much there was.