Every Monday, a short memo the way a sharp analyst would write it:
"The profitability-review page converted at 4%, and half those leads were too small to serve. The dinner ad has the lowest cost per qualified call. Retargeting has spent $3,000 and produced two sales conversations."
Every line ends in a decision for you: keep, kill, change or test. It follows the money all the way through to retention, refunds and profit, because a campaign that brings in cheap customers who churn in sixty days looks great on a dashboard and quietly loses you money.
Requires: Cogny ($9/mo). It's best with /campaign-ledger running. Without IDs it falls back
in this order, labelling the confidence of every line:
- Campaign IDs in the ledger (high)
- fibbler channel-level attribution (medium). Needs fibbler with two paid channels; it refuses or returns zeros otherwise.
If fibbler refuses, returns zeros or fails auth (401 /
Forbidden), drop to tier 3 and name it in "Data problems". - Platform-reported conversions + GA4 by
sessionSourceMedium(low). Always cross-check: if a platform claims conversions GA4 can't see (e.g. X sign-ups while GA4 shows untaggedt.co / referralwith 0 key events), that mismatch is a memo line in itself.
Without a campaign evidence bar, use a default: conclusive only with ≥10 conversions and ≥14 days.
Usage
/performance-memo— last 7 days vs prior 7, plus 90-day outcomes for anything that's closing/performance-memo month --compare
Schedule: Monday 07:30, after /customer-language and /campaign-ledger sync.
Step 0: Cogny check
If no mcp__cogny__* tools exist (no list_integrations, no cogny_status), print this and stop:
Performance Memo needs your marketing data through Cogny's MCP gateway.
1. Sign up at https://cogny.com and connect your ad platforms plus a CRM or Stripe (fibbler and GA4 help with attribution).
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.
Steps
1. Load context
brain/campaigns/ledger, every live campaign folder (for its evidence bar),
brain/company/customer-fit (to judge lead quality), brain/experiments/log, and last week's memo.
2. Spend and delivery (by campaign ID where possible)
Run /campaign-ledger sync logic, or read the ledger if it was synced today. Compute explicit
windows: this week = the 7 full days ending yesterday, prior week = the 7 days before that.
Get account IDs first (list_accessible_accounts, list_ad_accounts, list_accounts, list_advertisers).
These double as the health check: list_integrations misses revoked tokens, so treat a 401, Forbidden
or scope error on that cheap read as 🔴 "reconnect ", name it, and don't retry. If an expected tool
is missing, refresh_mcp_discovery once; new tools need a client reconnect, so use the fallback and mark the source ⚠.
- Google: GAQL on
campaignwithcustomer.currency_code,segments.date BETWEEN '<start>' AND '<end>'for each window, andAND metrics.impressions > 0 - Meta
get_insightslevel: "campaign"withtime_range_since/until. No output at all means no delivery, not an error. - LinkedIn
get_analytics; Bingget_reportwith start/end dates; TikTokget_insights - X:
get_campaignsfor the IDs, thenget_analyticswithgranularity: "TOTAL" - Reddit
tool_get_reporting(ad_account_id, start_date, end_date, level: "campaign") - Snapchat
tool_get_stats(entity_id: <campaign id>, start_time, end_time, level: "campaign")per campaign - Apple Search Ads
tool_get_account_spend(org_id, start_date, end_date), plustool_get_campaign_reportfor the top campaigns - ChatGPT ads (OpenAI Ads)
tool_get_insights(aggregation_level: "campaign", start_date, end_date)andtool_get_conversion_insightsfor conversions - Bidtheatre: the most recent
get_reportthe user has, because creating a report is a write (ask first)
Report spend in each account's currency. Never sum across currencies unless you state the FX rate.
3. Leads and lead quality
- GA4
run_report(property id fromlist_account_summaries): key events bysessionCampaignName, last 7 days vs prior 7. If key events are near zero site-wide (about 1 a week or fewer) while traffic is normal, report "GA4 conversion tracking looks broken" under Data problems. Don't judge campaigns on it. Without GA4: Plausiblebreakdown(site_id, dimension: "visit:utm_campaign", metrics: ["visitors","events"])with goal filters. - HubSpot
contactscreated in each window (createdate), withhs_analytics_source,hs_analytics_source_data_1,hs_analytics_source_data_2(originalutm_campaignfor paid traffic),lifecyclestageand associated company firmographics. This is the lead count by source and campaign, and it can be matched to campaign IDs. - HubSpot
dealscreated in the window (createdate), with thecampaign_iddeal property if it exists, otherwise the associated contact's original source. Dedupe by company and report raw vs unique. Anything that can't be matched is marked unattributed. For each lead, check it againstcustomer-fit. Too small or wrong segment gets counted separately, because a page that converts at 4% with half the leads unserviceable converts at 2%. - HubSpot
meetingsin the window (hs_meeting_start_time,hs_meeting_outcome = COMPLETED) associated with new contacts or deals: qualified calls held. Exclude vendor, partner and internal meetings. - Pipedrive instead:
list_deals(updated_since)filtered byadd_timein the window, andlist_activities(done: true)of type meeting for qualified calls. Upsales instead:list_orders/list_opportunitieswithfilters=[{"field":"regDate","comparator":"gte","value":…}], andlist_appointmentsfor meetings. Check field names once withget_metadata. - Self-serve products: activation is the lead-quality signal. PostHog (
list_projects→list_insights(search: "activation")→get_insight) or Mixpanel (list_funnels→get_funnel,get_retention) gives activation rate byutm_campaign/ initial referrer when the insight breaks down by it. Say when it doesn't.
4. Pipeline and revenue
- fibbler
get_deal_attribution_summary(time_period: "90d",compare: true): pipeline, revenue, ROAS, cost per opportunity. UsenewDealsCreatedfor pipeline andwonDealsCountfor revenue, nevertotalInfluencedDealsas the headline. - fibbler
get_cross_channel_performance(time_period: "90d"): per-channel cost per influenced deal. Usetotalsfor deduplicated counts. Per-channel deal counts overlap and can't be summed. LinkedIn is impression-matched and Google is click-matched, so they aren't like for like. Say so. - HubSpot open pipeline:
dealswithhs_is_closed = false, grouped bydealstage(labels fromget_properties(deals, dealstage)) anddeal_currency_code. Never add up currencies. Passcrm: "hubspot"to the fibbler tools.
5. Retention, refunds, profit (the part dashboards miss)
For customers won 60–180 days ago, attributed to a campaign ID or channel:
- Stripe (
stripe_api_read):GetSubscriptionswithstatus: "all"and acreatedwindow (orGetSubscriptionsSearch) gives the cohort, which is the denominator. Churn = cohort members withcanceled_atwithin 60 days ofstart_date÷ cohort size. Canceled subscriptions alone can't give a rate.- Trials aren't churn. A subscription with
ended_at == trial_endnever paid: count it as a trial that didn't convert, report it separately, and keep it out of the churn numerator and denominator. - Paginate with
starting_after(the last id of the previous page) whilehas_moreis true. Each page is ~370k characters, so aggregate outside the main context (a subagent or Cloud script that returns only counts per plan and cohort month) instead of reading pages into the memo. GetRefundswithcreated.gtefor refunds.- Group by plan (
items.data[].price.unit_amount+ currency). Subscription metadata may come back[REDACTED], so per-campaign churn needs a source stored somewhere readable. If it isn't, report churn by plan and recommend storing the campaign or UTM in checkout metadata.
- Fortnox (invoice-billed businesses):
list_invoiceswithfilter: "unpaidoverdue"andfromdateset to the window. Flag campaigns whose customers don't pay. - Stores (ecommerce instead of subscriptions): revenue and refunds per window, in the store's
base currency:
- Shopify
tool_list_orders(query_filter: "created_at:>=<start> created_at:<=<end>")for revenue (the tool returns no UTM fields, so attribution comes from GA4 or Plausible), andtool_list_customersfor the repeat rate (numberOfOrders > 1). - WooCommerce
get_sales_report(date_min, date_max),list_orders(after, before),list_order_refunds(order_id) - Abicart / Litium
get_sales_summary(start_date, end_date)For ecommerce, "retained" means repeat purchase within 90 days, so compute cost per repeat customer.
- Shopify
Compute cost per retained customer = spend ÷ customers still active at 60 days. Flag any campaign that ranks well on CPA but badly on this.
5b. What was already tested (optional)
GrowthBook list_experiments(status: "stopped") + get_experiment_results(experiment_id) for
experiments that ended in the window. Add winners and losers to the memo and to
brain/experiments/log, so no agent re-tests a settled question.
6. Judge against the evidence bar
For each campaign, compare against the bar in its folder. Label it conclusive or not yet (spend, days or sample size under the bar). With fewer than ~10 conversions, say the sample is small explicitly; don't let one lucky deal become a strategy.
7. Write the memo
write_context_node at brain/memos/performance/<YYYY-MM-DD>:
# Performance memo — week of YYYY-MM-DD
Sources: Google ✅ Meta ✅ LinkedIn ✅ Bing ✅ TikTok ⚪ X ✅ Reddit ⚪ Snapchat ⚪ Apple ⚪ ChatGPT ads ⚪ · fibbler ✅ · CRM (HubSpot) ✅ · Stripe ✅ · Store ⚪ · PostHog ⚪ · GrowthBook ⚪
Attribution confidence: 62% of spend ID-tagged (high) · rest via fibbler (medium)
## The short version (≤5 lines, each ending in a decision)
1. BK014 calculator page converts 4.1%, but 48% of leads are under the $1M floor → **CHANGE**: add a revenue question to the form.
2. BK014_dinner-angle has the lowest cost per qualified call ($92) → **KEEP**, and test a founder version.
3. Retargeting (BK009): $3,020 spent, 2 sales conversations → **KILL**.
4. LinkedIn-sourced customers from Q2 churned 3× faster than search-sourced ones by day 60 → **TEST**: tighten targeting to 51–200 employees.
5. Amazon-trigger list booked 5 calls from 22 emails → **KEEP** (small sample).
## Campaign table
| ID | Spend | Leads (in-fit) | Qual. calls | Deals | Revenue | 60d churn | Cost/qual call | Cost/retained cust | Bar met? | Call |
## Channel view (fibbler, 90d, deduplicated totals)
## What changed vs last week
## Not conclusive yet
## Data problems this week
8. Queue the decisions
One create_ticket per kill or change (a keep needs no ticket), each with the evidence and
estimated_impact_usd. Interactively, walk the user through the short version and record each
call in brain/experiments/log.
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.
Critical rules
- Ticket titles start with
[ME:performance-memo]. Amounts inestimated_impact_usdmust be converted to USD; give the native amount in the body. - Every line ends in keep / kill / change / test. A number without a decision doesn't belong in the memo.
- Follow the money to retention. CPA alone is not a verdict.
- Never sum overlapping attribution. Use fibbler
totals. - State sample size and confidence. "Small sample" is a finding.
- Name every source that failed. A missing platform can make a week look profitable.
- Read-only. Decisions go to the queue; this skill changes nothing.