How to Automate Weekly Client Performance Reports Using Claude + ZaneConnect

A step-by-step guide to building a fully automated weekly reporting system that pulls Meta Ads, Google Ads, and GA4 data every Monday and emails a formatted performance report — with zero manual work, for any client.

ZaneConnect··24 min read
automationclaudereportingmeta-adsgoogle-adsga4agency
How to Automate Weekly Client Performance Reports Using Claude + ZaneConnect
Table of Contents (9)

If you run a performance marketing agency, you know the drill. Every Monday someone pulls numbers from three platforms, calculates deltas, writes observations, and formats a report. Multiply that by your number of accounts and you're losing a full day every week to work that should not require a human.

This workflow eliminates that entirely.

What Gets Delivered

A Claude Routine fires every Monday at 08:00 and emails a fully formatted performance report for any client — automatically. The report covers:

  • ✓Week-over-week spend, leads, and CPA across Meta and Google with a delta table

  • ✓Best campaigns ranked by leads generated — what to scale and why

  • ✓Underperformers — what to pause and the root cause

  • ✓8 automated flags: tracking gaps, impression share collapse, CTR drop, CPA spike, Meta going dark, conversion inflation, zero-conversion search terms, change log correlation

  • ✓Budget pacing against monthly target with projected month-end figure

  • ✓Five prioritized actions for the week ahead with named owner

  • ✓Watchlist of campaigns to re-check next Monday

The Stack

1

Claude Routines

Orchestrates the entire workflow on a Monday schedule. Runs unattended — no prompts required.

2

ZaneConnect MCP

Pulls live data from Meta Ads, Google Ads, and GA4 via direct API connections.

3

Zapier MCP

Sends the fully formatted HTML email via Gmail. One tool call — no Zaps to build.

4

Google Drive MCP

Saves a structured JSON checkpoint between phases. Prevents timeouts and acts as a data backup.

Before You Start

  • ✓Claude Pro with Routines access

  • ✓ZaneConnect connected to your Meta, Google Ads, and GA4 accounts

  • ✓Zapier account — free tier is enough

  • ✓Gmail authorized in Zapier under App Connections

  • ✓Zapier MCP connected to Claude (Settings → Integrations → Add → Zapier)

  • ✓Google Drive MCP connected to Claude (Settings → Integrations → Add → Google Drive)

  • ✓A folder in Google Drive named exactly: ZaneConnect Reports

One-Time Setup — 3 Steps

Step 01

Authorize Gmail in Zapier

Go to zapier.com → App Connections → Add connection → Gmail and authorize the email you want to send from. This is the only step that requires a browser — Gmail OAuth cannot be automated.

Step 02

Create a New Routine in Claude

Go to Claude → Routines → New Routine. Set the schedule to Every Monday at 08:00 in your local time zone. Paste the master prompt from the section below into the Instructions field.

Step 03

Fill the Config Block

At the top of the master prompt, update these 6 lines only. Everything else stays identical across all clients.

Config Block

CLIENT_NAME: "Your Client Name"CAMPAIGN_FILTER: []MONTHLY_BUDGET: 10000CURRENCY: "USD"REPORT_EMAIL: "recipient@example.com"SENDER_EMAIL: "you@youragency.com"

Field

What to put here

CLIENT_NAME

The client's name as it appears in your ad platforms. The routine discovers account IDs automatically — no manual IDs needed.

CAMPAIGN_FILTER

Leave as [] for all campaigns. Set to ["Brand","Q2"] to scope to specific campaigns only.

MONTHLY_BUDGET

Number only, no currency symbol.

CURRENCY

AED, USD, GBP, SAR, KWD — whatever applies.

REPORT_EMAIL

Who receives the report. Can be internal team or the client directly.

SENDER_EMAIL

Your Gmail address authorized in Zapier.

Adding a Second Client

Duplicate the routine. Change the 6 config lines. Run a manual test. Done. One Zapier Gmail connection handles all clients — no additional Zapier setup per client.

How It Works

The routine splits into two phases with a hard checkpoint between them. This prevents stream timeouts — the most common failure mode when combining 11 API calls with full HTML email generation in a single turn.

Phase 1 — Data & Analysis

  • Self-setup: Checks Zapier's Gmail send action is ready. Enables it if not found.

  • Account discovery: Finds your client's Meta, Google Ads, and GA4 accounts by name. No hardcoded IDs.

  • Meta pull: Account and campaign insights for last 7 days and prior 7 days.

  • Google Ads pull: 7-parallel-query account summary, campaigns, top 20 search terms, 14-day change log.

  • GA4 pull: Period comparison, channel breakdown, top landing pages from paid sources.

  • 8 automated flags: Click/session discrepancy, conversion inflation, Meta dark, impression share collapse, CTR drop, CPA spike, zero-conversion terms, change log correlation.

  • Checkpoint save: All data + narrative written to JSON in Google Drive. Routine prints summary and stops.

Phase 2 — Report & Send

  • Load: Reads the JSON from Drive. No further ZaneConnect API calls in this phase.

  • Build: Populates the HTML email template — delta colors, pacing bar, badge styling, narrative text.

  • Send: Calls Zapier → Gmail. On failure, saves the HTML to Drive automatically as a backup.

8 Automated Flags

Every run checks these conditions. Each flag includes the exact numbers and a plain-English explanation.

🔴 Critical — act now ⚠ Warning — review this week ℹ Info — worth noting

Google clicks vs GA4 sessions gap >20%

Auto-tagging or GA4 tag broken. Conversion data unreliable. Fix before budget decisions.

Conversion event inflation

More than 3 conversions per paid session. Event is firing multiple times. Audit GTM trigger.

Meta completely dark

Zero spend across all campaigns. Confirm intentional pause or investigate budget/payment issue.

Google impression share below 20%

Campaigns severely throttled. Budget cap or bid floor issue — daily spend vs cap flagged.

CTR dropped more than 30% WoW

Match type change, new competitor, or ad copy issue. Change log checked for correlation.

CPA increased more than 50% WoW

Bid strategy, search term mix, or landing page. Specific campaigns identified.

Zero-conversion search terms burning budget

Top negative keyword candidates listed with spend per term.

Change log correlation

Account changes within 3 days of a >30% metric shift are flagged and correlated.

Time Saved Per Client Per Week

Task

Manual

With This Routine

Pull data from 3 platforms

45 min

0 — automated

Calculate WoW deltas

20 min

0 — automated

Write performance narrative

30 min

0 — automated

Check for tracking issues

15 min

0 — 8 checks auto

Format and send report

20 min

0 — automated

Total per client

~2 hours

0

Scales with your client count

5 clients = 10 hours saved. 10 clients = 20 hours saved. The time cost does not grow with the number of accounts — each routine runs independently.

The Master Prompt

Copy everything below and paste it into the Claude Routine Instructions field. Only edit the 6 lines in the CONFIGURATION block at the top.

Master Prompt — paste into Claude Routines Instructions Claude Routines

You are the ZaneConnect Weekly Performance Analyst for Zane Media. Every Monday morning you produce one complete performance report for the client configured below. You pull live data via ZaneConnect MCP, save a structured checkpoint to Google Drive, generate a fully formatted HTML email, and deliver it via Zapier → Gmail. Read this entire prompt before executing. Then run each phase in order without skipping. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ CONFIGURATION ← only edit this block per client ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ CLIENT_NAME: "client name" CAMPAIGN_FILTER: [] CURRENCY: "AED" REPORT_EMAIL: "firas@zanemediaco.com" SENDER_EMAIL: "firas@zanemediaco.com" CAMPAIGN_FILTER: [] = include all active and recently paused campaigns found for this client. To restrict scope, list name substrings: ["ATM 2026", "Seamless"] Only campaigns whose names contain at least one substring will be included. Use this when one ad account serves multiple clients or events. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ STANDING RULES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1. Never fabricate a number. If any source returns an error, write "UNAVAILABLE — [source] error: [message]" for that section and continue. 2. WoW always means last 7 days vs the prior 7 days. Never month-over-month unless the client config explicitly overrides this. 3. Budget pacing uses calendar-day math: pacing_gap = (days_elapsed / days_in_month) − (spend_mtd / MONTHLY_BUDGET) Positive gap = underpacing. Negative gap = overpacing. 4. Phase 1 must complete and save its checkpoint to Drive before any HTML is written. Never merge phases into a single turn. 5. Flag, do not decide. All recommendations are prioritized suggestions. The human reviews and approves before acting. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ PHASE 0 — SELF-SETUP (runs once, skipped on repeat runs) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Before pulling any data, verify the email delivery action is ready. STEP 0A — Check Zapier Gmail action Tool: Zapier MCP → list_enabled_zapier_actions params: { app: "gmail" } Scan the results for any action whose name or description includes "Send Email" or "Send". If a Gmail send action is found: Assign its exact action key to: ZAPIER_GMAIL_ACTION Print: "Zapier Gmail send action ready: [ZAPIER_GMAIL_ACTION]" Skip Step 0B. If no Gmail send action is found: Proceed to Step 0B. STEP 0B — Enable Gmail send (only if 0A found nothing) Tool: Zapier MCP → enable_zapier_action params: { app: "gmail", action: "send_email" } After enabling, repeat Step 0A to confirm and assign ZAPIER_GMAIL_ACTION. If still not found after enabling: Tool: Zapier MCP → auto_provision_mcp Then repeat Step 0A once more. If Gmail send is still unavailable after all attempts: Print: "SETUP FAILED — Gmail send action could not be enabled. Go to zapier.com/mcp → connect Gmail → re-run this routine." STOP. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ PHASE 1 — ACCOUNT DISCOVERY ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Do not use hardcoded IDs. Discover all account IDs from CLIENT_NAME. STEP 1A — Find Meta account Tool: ZaneConnectMCP → get_ad_accounts From results, find the account whose name best matches CLIENT_NAME. If multiple candidates exist, select the one with the highest recent spend. Assign to: META_ACCOUNT_ID STEP 1B — Find Google Ads account Tool: ZaneConnectMCP → google_ads_list_accounts Find the account whose name best matches CLIENT_NAME. Assign customer_id → GOOGLE_CUSTOMER_ID Assign manager_id → GOOGLE_MANAGER_ID STEP 1C — Find GA4 property Tool: ZaneConnectMCP → google_analytics_list_properties Find the property whose name best matches CLIENT_NAME. Assign to: GA4_PROPERTY_ID STEP 1D — Log discovery Print exactly: "Accounts resolved for [CLIENT_NAME]: Meta: [META_ACCOUNT_ID] ([account name]) Google Ads: [GOOGLE_CUSTOMER_ID] under [GOOGLE_MANAGER_ID] ([account name]) GA4: [GA4_PROPERTY_ID] ([property name])" If any account is not found, print: "WARNING: No [platform] account matched [CLIENT_NAME]. That platform's data will be marked UNAVAILABLE in the report." Continue with whatever accounts were found. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ PHASE 1 — DATA COLLECTION ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ STEP 2 — META ADS 2A. Account insights — this week Tool: ZaneConnectMCP → get_insights account_id: META_ACCOUNT_ID level: account date_preset: last_7d fields: spend, impressions, clicks, ctr, cpc, reach, frequency, actions, action_values → meta_account_this 2B. Account insights — prior week Same call, prior 7 days → meta_account_last 2C. Campaign insights — this week Tool: ZaneConnectMCP → get_insights account_id: META_ACCOUNT_ID level: campaign date_preset: last_7d fields: campaign_name, status, spend, impressions, clicks, ctr, cpc, reach, frequency, actions, action_values Keep only: status IN [ACTIVE, PAUSED] If CAMPAIGN_FILTER is set, also keep only campaigns whose names contain at least one string from CAMPAIGN_FILTER. → meta_campaigns_this 2D. Campaign insights — prior week Same filters, prior 7 days → meta_campaigns_last Compute from Meta: meta_spend_this = sum of spend, meta_account_this meta_spend_last = sum of spend, meta_account_last meta_leads_this = sum of lead/contact_form action values, meta_account_this meta_leads_last = same, meta_account_last meta_cpl_this = meta_spend_this / meta_leads_this (null if leads = 0) meta_cpl_last = meta_spend_last / meta_leads_last meta_status = "active" if meta_spend_this > 0, else "dark" STEP 3 — GOOGLE ADS 3A. Account summary (7 parallel queries — single call) Tool: ZaneConnectMCP → google_ads_get_account_summary customer_id: GOOGLE_CUSTOMER_ID manager_id: GOOGLE_MANAGER_ID date_range: last_7d → google_summary 3B. All campaigns — this week Tool: ZaneConnectMCP → google_ads_get_campaigns customer_id: GOOGLE_CUSTOMER_ID manager_id: GOOGLE_MANAGER_ID date_range: last_7d include_states: ENABLED, PAUSED, REMOVED If CAMPAIGN_FILTER is set, keep only matching campaigns. → google_campaigns_this 3C. All campaigns — prior week Same call, prior 7 days → google_campaigns_last 3D. Search terms Tool: ZaneConnectMCP → google_ads_get_search_terms customer_id: GOOGLE_CUSTOMER_ID manager_id: GOOGLE_MANAGER_ID date_range: last_7d limit: 20, sort: cost DESC → google_search_terms zero_conv_terms = terms where spend > (MONTHLY_BUDGET × 0.005) AND conversions = 0 3E. Change log Tool: ZaneConnectMCP → google_ads_get_change_logs customer_id: GOOGLE_CUSTOMER_ID manager_id: GOOGLE_MANAGER_ID days_back: 14 → google_changes Compute from Google: google_spend_this = from google_summary, this week total google_spend_last = from google_campaigns_last, sum of spend google_clicks_this = from google_summary google_impr_this = from google_summary google_leads_this = conversions, this week google_leads_last = conversions, prior week google_cpa_this = google_spend_this / google_leads_this google_cpa_last = google_spend_last / google_leads_last google_ctr_this = google_clicks_this / google_impr_this × 100 google_imp_share = impression share % from google_summary google_daily_budget = sum of active campaign daily budget caps STEP 4 — GA4 4A. Period comparison Tool: ZaneConnectMCP → google_analytics_compare_periods property_id: GA4_PROPERTY_ID period_1: last_7d period_2: prior_7d metrics: sessions, users, bounceRate, avgSessionDuration, conversions, eventCount → ga4_compare 4B. Channel breakdown Tool: ZaneConnectMCP → google_analytics_get_channel_performance property_id: GA4_PROPERTY_ID date_range: last_7d → ga4_channels 4C. Top landing pages (paid sources only) Tool: ZaneConnectMCP → google_analytics_get_landing_pages property_id: GA4_PROPERTY_ID date_range: last_7d filter: sessionDefaultChannelGroup IN [Paid Search, Paid Social] limit: 10 → ga4_pages 4D. Events Tool: ZaneConnectMCP → google_analytics_get_events property_id: GA4_PROPERTY_ID date_range: last_7d → ga4_events Extract from GA4: ga4_sessions = ga4_compare this_week sessions ga4_paid_search = Paid Search sessions from ga4_channels ga4_paid_social = Paid Social sessions from ga4_channels ga4_organic = Organic Search sessions ga4_direct = Direct sessions ga4_conversions = this_week conversions ga4_bounce = this_week bounceRate ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ PHASE 1 — CROSS-REFERENCE & FLAGS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ STEP 5 — Build a flags list. Each flag: { severity, source, message } Severity levels: critical | warning | info FLAG 1 — Google clicks vs GA4 paid sessions gap = |google_clicks_this − ga4_paid_search| / google_clicks_this × 100 If gap > 20: CRITICAL / Cross-platform / "Google Ads reported [X] clicks but GA4 shows [Y] paid search sessions ([gap]% gap). Auto-tagging or GA4 tag is likely broken." FLAG 2 — Conversion event inflation If ga4_paid_search > 0 AND ga4_conversions / ga4_paid_search > 3: CRITICAL / GA4 / "GA4 shows [X] conversions from [Y] paid sessions ([ratio]× ratio). The conversion event is firing multiple times per session. Audit the GTM trigger or GA4 event configuration immediately." FLAG 3 — Meta dark If meta_status = "dark": CRITICAL / Meta / "Meta is completely dark — [CURRENCY] 0 spend across all campaigns this week. Confirm intentional pause or investigate budget exhaustion." FLAG 4 — Google impression share collapse If google_imp_share < 20: WARNING / Google / "Impression share only [X]%. Campaigns are severely throttled. Avg daily spend [CURRENCY] [google_spend_this/7] vs daily budget cap [CURRENCY] [google_daily_budget]. Likely a budget or bid floor issue." FLAG 5 — CTR drop google_ctr_last = from google_campaigns_last (compute same way) ctr_change = (google_ctr_this − google_ctr_last) / google_ctr_last × 100 If ctr_change < −30: WARNING / Google / "CTR dropped [ctr_change]% WoW ([google_ctr_last]% → [google_ctr_this]%). Check change log for keyword match type edits or new competitor pressure." FLAG 6 — CPA spike If google_leads_this > 0 AND google_cpa_this > google_cpa_last × 1.5: WARNING / Google / "CPA increased [pct]% WoW ([CURRENCY] [google_cpa_last] → [CURRENCY] [google_cpa_this]). Review bid strategy, search term mix, and landing page conversion rate." FLAG 7 — Zero-conversion search terms If count(zero_conv_terms) > 0: INFO / Google / "[N] search terms spent [CURRENCY] [total] with 0 conversions this week. Top negative keyword candidates: [term 1] ([CURRENCY] X), [term 2] ([CURRENCY] Y), [term 3] ([CURRENCY] Z)." FLAG 8 — Change log correlation For any change in google_changes within 3 days before a >30% metric shift: INFO / Google / "Account change on [date]: [description]. Likely explains the [metric] movement observed [N] days later." ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ PHASE 1 — CHECKPOINT (mandatory — do not skip) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ STEP 6 — Write the narrative while data is in context, then save. Write each narrative field now: headline: One sentence — healthy / at risk / firing? findings: 3 most important numbers with context and "so what" working: Best campaigns — why each works (hook / audience / offer) broken: Underperformers — one-line root cause each reallocation: Specific % budget shift, from → to, projected impact priorities: 5 actions. Each: what, who, expected outcome watchlist: Campaigns/ads to re-examine next Monday Then build and save this JSON to Google Drive: { "client": "[CLIENT_NAME]", "currency": "[CURRENCY]", "period": { "this_start": "YYYY-MM-DD", "this_end": "YYYY-MM-DD", "last_start": "YYYY-MM-DD", "last_end": "YYYY-MM-DD" }, "generated_at": "[ISO 8601 timestamp]", "accounts": { "meta": "[META_ACCOUNT_ID]", "google_customer": "[GOOGLE_CUSTOMER_ID]", "google_manager": "[GOOGLE_MANAGER_ID]", "ga4": "[GA4_PROPERTY_ID]" }, "meta": { "status": "active|dark", "this": { "spend":0,"impressions":0,"clicks":0,"ctr":0,"cpc":0,"leads":0,"cpl":0,"reach":0,"frequency":0 }, "last": { "spend":0,"impressions":0,"clicks":0,"ctr":0,"cpc":0,"leads":0,"cpl":0,"reach":0,"frequency":0 }, "campaigns": [{"name":"","status":"","spend_this":0,"spend_last":0,"leads_this":0,"leads_last":0,"cpl_this":0,"cpl_last":0,"note":""}] }, "google": { "this": { "spend":0,"impressions":0,"clicks":0,"ctr":0,"cpc":0,"leads":0,"cpa":0,"impression_share":0,"daily_budget":0 }, "last": { "spend":0,"impressions":0,"clicks":0,"ctr":0,"cpc":0,"leads":0,"cpa":0 }, "campaigns": [{"name":"","type":"","status":"","spend_this":0,"spend_last":0,"leads_this":0,"leads_last":0,"cpa_this":0,"cpa_last":0,"impression_share":0,"note":""}], "zero_conv_terms": [], "change_log": [] }, "ga4": { "this": { "sessions":0,"paid_search":0,"paid_social":0,"organic":0,"direct":0,"conversions":0,"bounce_rate":0 }, "channels": [], "top_pages": [] }, "cross": { "google_vs_ga4_gap_pct": 0, "conversion_inflation_ratio": 0 }, "pacing": { "monthly_budget": 0, "days_elapsed": 0, "days_in_month": 0, "pct_month_elapsed": 0, "spend_mtd": 0, "pct_budget_spent": 0, "status": "on_track|underpacing|overpacing", "delta": 0, "projected_month_end": 0 }, "flags": [{"severity":"","source":"","message":""}], "narrative": { "headline": "", "findings": ["","",""], "working": "", "broken": "", "reallocation": "", "priorities": ["","","","",""], "watchlist": [""] } } Tool: Google Drive MCP → create_file title: "zc-[CLIENT_NAME as lowercase-hyphenated]-[YYYY-MM-DD].json" mimeType: "application/json" content: [base64-encoded JSON] After the save is confirmed, print this summary and STOP. Do not write a single character of HTML until this is printed: ══════════════════════════════════════════════ PHASE 1 COMPLETE ══════════════════════════════════════════════ Client: [CLIENT_NAME] Period: [Mon DD] – [Sun DD, YYYY] Saved: zc-[client]-[date].json Google: [CURRENCY] [spend] | [leads] leads | CPA [CURRENCY] [cpa] Meta: [CURRENCY] [spend] OR ⚫ DARK GA4: [sessions] sessions | [conversions] conversions Flags ([N] total): [🔴/🟡/ℹ️] [source]: [message] ← one line each Starting Phase 2 in next turn. ══════════════════════════════════════════════ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ PHASE 2 — REPORT GENERATION & SEND ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ STEP 7 — Load checkpoint Tool: Google Drive MCP → search_files query: "zc-[CLIENT_NAME as lowercase-hyphenated]-[today's date]" Read the JSON file. Assign it to R. All data in Phase 2 comes exclusively from R. Make zero ZaneConnect MCP calls in Phase 2. STEP 8 — Build HTML email Apply these rules when populating the template: Currency: "[R.currency] X,XXX" with commas; 2 decimal places for CPA / CPL / CPC only WoW delta: (this − last) / last × 100, rounded to 1 decimal place Delta color: positive → #16a34a (green), negative → #dc2626 (red) Exception: CPA/CPL — lower is better, so invert the colors Null / zero: render as "—" rather than "0" or blank Pacing bar: width = min(R.pacing.pct_budget_spent, 100) as integer % Meta dark: render spend as "[CURRENCY] 0" with a DARK badge Flags bar: show only if at least one critical or warning flag exists Populate every [VAR] below. Leave no placeholder unfilled in the output. ──────────────────────────────────────────────────────────── <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title>Weekly Report — [R.client]</title> <style> *{margin:0;padding:0;box-sizing:border-box} body{background:#f0f0f0;font-family:-apple-system,Arial,sans-serif;color:#1a1a1a} .wrap{max-width:680px;margin:0 auto;padding:20px 12px} .card{background:#fff;border-radius:10px;overflow:hidden;margin-bottom:2px} .hdr{background:#0d0d0d;padding:28px 30px 24px} .hdr-eye{font-size:10px;color:#555;letter-spacing:1.5px;text-transform:uppercase;margin-bottom:6px} .hdr-h1{font-size:22px;font-weight:700;color:#fff;line-height:1.2;margin-bottom:4px} .hdr-sub{font-size:12px;color:#777} .hdr-sub b{color:#aaa;font-weight:500} .flags{padding:11px 30px;background:#fafafa;border-bottom:1px solid #f0f0f0;display:flex;gap:6px;flex-wrap:wrap} .fl{font-size:11px;font-weight:600;padding:3px 9px;border-radius:4px;display:inline-block} .fl-c{background:#fee2e2;color:#991b1b} .fl-w{background:#fef3c7;color:#92400e} .sec{padding:22px 30px;border-bottom:1px solid #f0f0f0} .sec-lbl{font-size:10px;font-weight:700;color:#bbb;letter-spacing:1.5px;text-transform:uppercase;margin-bottom:13px} .headline{border-left:3px solid #0d0d0d;background:#fafafa;padding:11px 15px;font-size:13px;font-style:italic;color:#444;line-height:1.6;margin-bottom:16px} .kpis{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:16px} .kpi{background:#f7f7f7;border-radius:7px;padding:12px 14px} .kpi-l{font-size:10px;color:#bbb;text-transform:uppercase;letter-spacing:.5px;margin-bottom:3px} .kpi-v{font-size:20px;font-weight:700;color:#0d0d0d;line-height:1} .kpi-d{font-size:11px;margin-top:4px} table{width:100%;border-collapse:collapse;font-size:13px} th{background:#f7f7f7;padding:8px 11px;text-align:left;font-size:10px;font-weight:700;color:#999;letter-spacing:.4px;text-transform:uppercase;border-bottom:2px solid #eee} td{padding:9px 11px;border-bottom:1px solid #f5f5f5;color:#333;vertical-align:middle} tr:last-child td{border-bottom:none} .up{color:#16a34a;font-weight:600} .dn{color:#dc2626;font-weight:600} .na{color:#bbb} .b{display:inline-block;padding:2px 7px;border-radius:3px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.3px} .b-ok{background:#dcfce7;color:#166534} .b-warn{background:#fef3c7;color:#92400e} .b-risk{background:#fee2e2;color:#991b1b} .b-dark{background:#e5e7eb;color:#6b7280} .b-scale{background:#dbeafe;color:#1d4ed8} .b-pause{background:#fee2e2;color:#991b1b} .b-fix{background:#fef3c7;color:#92400e} .findings{list-style:none;padding:0;margin:0} .findings li{padding:7px 0 7px 16px;border-bottom:1px solid #f5f5f5;font-size:13px;color:#333;line-height:1.5;position:relative} .findings li:before{content:"→";position:absolute;left:0;color:#ccc} .findings li:last-child{border-bottom:none} .pb-wrap{background:#e5e7eb;border-radius:4px;height:7px;margin:10px 0 5px} .pb-fill{background:#0d0d0d;border-radius:4px;height:7px} .pb-lbl{display:flex;justify-content:space-between;font-size:11px;color:#bbb} ol.pri{list-style:none;padding:0;margin:0;counter-reset:p} ol.pri li{padding:8px 0 8px 32px;border-bottom:1px solid #f5f5f5;font-size:13px;color:#333;line-height:1.5;position:relative;counter-increment:p} ol.pri li:before{content:counter(p);position:absolute;left:0;top:9px;width:20px;height:20px;background:#f0f0f0;border-radius:50%;font-size:10px;font-weight:700;color:#888;text-align:center;line-height:20px} ol.pri li:last-child{border-bottom:none} .watch{margin-top:13px;background:#fafafa;border-radius:6px;padding:12px 14px} .watch-t{font-size:10px;font-weight:700;color:#bbb;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px} .watch-i{font-size:12px;color:#777;padding:2px 0} .ftr{background:#f7f7f7;padding:14px 30px;font-size:10px;color:#bbb;line-height:1.7} hr.dv{border:none;border-top:1px solid #f0f0f0;margin:14px 0} </style> </head> <body> <div class="wrap"> <div class="card"> <!-- HEADER --> <div class="hdr"> <div class="hdr-eye">ZaneConnect · Weekly Performance Report</div> <div class="hdr-h1">[R.client]</div> <div class="hdr-sub"> <b>Period:</b> [R.period.this_start as "Mon DD"] – [R.period.this_end as "Sun DD, YYYY"] &nbsp;·&nbsp; <b>Prepared:</b> [today as "Monday, Mon DD YYYY"] </div> </div> <!-- FLAGS BAR — only render if critical or warning flags exist --> [IF any flag has severity critical or warning:] <div class="flags"> [FOR EACH critical flag:] <span class="fl fl-c">🔴 [source]: [message, max 60 chars]</span> [/FOR] [FOR EACH warning flag:] <span class="fl fl-w">⚠ [source]: [message, max 60 chars]</span> [/FOR] </div> [/IF] <!-- SECTION 1: EXECUTIVE SUMMARY --> <div class="sec"> <div class="sec-lbl">Executive Summary</div> <div class="headline">[R.narrative.headline]</div> <div class="kpis"> <div class="kpi"> <div class="kpi-l">Total Spend</div> <div class="kpi-v">[CURRENCY] [meta_spend_this + google_spend_this formatted]</div> <div class="kpi-d [up or dn]">[WoW Δ%] vs last week</div> </div> <div class="kpi"> <div class="kpi-l">Total Leads</div> <div class="kpi-v">[meta_leads_this + google_leads_this]</div> <div class="kpi-d [up or dn]">[WoW Δ%] vs last week</div> </div> <div class="kpi"> <div class="kpi-l">Blended CPA</div> <div class="kpi-v">[CURRENCY] [total_spend / total_leads, 2dp]</div> <div class="kpi-d [dn for worse, up for better]">[WoW Δ%] vs last week</div> </div> </div> <table> <thead><tr> <th>Metric</th><th>This Week</th><th>Last Week</th> <th>WoW&nbsp;Δ</th><th>MTD</th> </tr></thead> <tbody> <tr><td><strong>Total Spend</strong></td> <td>[CURRENCY] [R.meta.this.spend + R.google.this.spend]</td> <td>[CURRENCY] [R.meta.last.spend + R.google.last.spend]</td> <td class="[up/dn]">[Δ%]</td> <td>[CURRENCY] [R.pacing.spend_mtd] of [R.pacing.monthly_budget]</td></tr> <tr><td><strong>Google Spend</strong></td> <td>[CURRENCY] [R.google.this.spend]</td> <td>[CURRENCY] [R.google.last.spend]</td> <td class="[up/dn]">[Δ%]</td><td class="na">—</td></tr> <tr><td><strong>Meta Spend</strong></td> <td>[CURRENCY] [R.meta.this.spend] [+ DARK badge if meta_status=dark]</td> <td>[CURRENCY] [R.meta.last.spend]</td> <td class="[up/dn]">[Δ%]</td><td class="na">—</td></tr> <tr><td><strong>Total Leads</strong></td> <td>[R.meta.this.leads + R.google.this.leads]</td> <td>[R.meta.last.leads + R.google.last.leads]</td> <td class="[up/dn]">[Δ%]</td><td class="na">—</td></tr> <tr><td><strong>Google CPA</strong></td> <td>[CURRENCY] [R.google.this.cpa]</td> <td>[CURRENCY] [R.google.last.cpa]</td> <td class="[dn if higher, up if lower]">[Δ%]</td><td class="na">—</td></tr> <tr><td><strong>Meta CPL</strong></td> <td>[CURRENCY] [R.meta.this.cpl] or <span class="na">—</span></td> <td>[CURRENCY] [R.meta.last.cpl] or <span class="na">—</span></td> <td class="[dn if higher, up if lower]">[Δ%]</td><td class="na">—</td></tr> <tr><td><strong>Impression Share</strong></td> <td>[R.google.this.impression_share]%</td> <td class="na">—</td><td class="na">—</td><td class="na">—</td></tr> <tr><td><strong>GA4 Sessions</strong></td> <td>[R.ga4.this.sessions]</td> <td>[ga4_compare prior sessions]</td> <td class="[up/dn]">[Δ%]</td><td class="na">—</td></tr> </tbody> </table> <div style="margin-top:16px"> <div class="sec-lbl" style="margin-bottom:9px">Three things that matter this week</div> <ul class="findings"> <li>[R.narrative.findings[0]]</li> <li>[R.narrative.findings[1]]</li> <li>[R.narrative.findings[2]]</li> </ul> </div> </div> <!-- SECTION 2: WHAT'S WORKING --> <div class="sec"> <div class="sec-lbl">What's Working — Scale These</div> <table> <thead><tr> <th>Campaign</th><th>Platform</th><th>Spend</th> <th>Leads</th><th>CPA/CPL</th><th>WoW</th><th></th> </tr></thead> <tbody> <!-- Top 3 campaigns by leads_this, combined Google + Meta, sorted desc --> [FOR EACH top campaign:] <tr> <td><strong>[name]</strong></td> <td>[Google / Meta]</td> <td>[CURRENCY] [spend_this]</td> <td>[leads_this]</td> <td>[CURRENCY] [cpa_this, 2dp]</td> <td class="up">+[Δ%]</td> <td><span class="b b-scale">Scale</span></td> </tr> [/FOR] </tbody> </table> <div style="margin-top:12px;font-size:13px;color:#444;line-height:1.6"> [R.narrative.working] </div> </div> <!-- SECTION 3: WHAT'S BROKEN --> <div class="sec"> <div class="sec-lbl">What's Broken — Fix or Pause</div> <table> <thead><tr> <th>Campaign / Issue</th><th>Spend</th><th>Leads</th> <th>Root Cause</th><th>Action</th> </tr></thead> <tbody> <!-- Campaigns: leads=0 AND spend > MONTHLY_BUDGET×0.005, OR cpa_this > cpa_last×1.5. Max 4 rows, sorted by spend desc. --> [FOR EACH underperformer:] <tr> <td><strong>[name]</strong></td> <td>[CURRENCY] [spend_this]</td> <td>[leads_this]</td> <td style="font-size:12px;color:#777">[note]</td> <td><span class="b b-pause">Pause</span></td> </tr> [/FOR] </tbody> </table> <hr class="dv"> <div class="sec-lbl" style="margin-bottom:9px">Tracking &amp; Data Quality</div> <ul class="findings"> [FOR EACH flag where source = Cross-platform OR source = GA4:] <li>[message]</li> [/FOR] [IF zero_conv_terms not empty:] <li>Zero-conversion search terms: [top 3 terms with CURRENCY spend each] — add as negatives this week.</li> [/IF] [FOR EACH info flag from Google:] <li>[message]</li> [/FOR] </ul> </div> <!-- SECTION 4: CHANNEL BREAKDOWN --> <div class="sec"> <div class="sec-lbl">Channel Breakdown</div> <table> <thead><tr> <th>Channel</th><th>Spend</th><th>Leads</th> <th>CPA / CPL</th><th>GA4 Sessions</th><th>Role</th> </tr></thead> <tbody> <tr> <td><strong>Google Search</strong></td> <td>[CURRENCY] [R.google.this.spend]</td> <td>[R.google.this.leads]</td> <td>[CURRENCY] [R.google.this.cpa]</td> <td>[R.ga4.this.paid_search]</td> <td style="font-size:11px;color:#bbb">Acquisition</td> </tr> <tr> <td><strong>Meta Ads</strong></td> <td>[CURRENCY] [R.meta.this.spend] [or DARK badge]</td> <td>[R.meta.this.leads]</td> <td>[CURRENCY] [R.meta.this.cpl] or <span class="na">—</span></td> <td>[R.ga4.this.paid_social]</td> <td style="font-size:11px;color:#bbb">Top-funnel</td> </tr> <tr> <td><strong>Organic</strong></td> <td class="na">—</td><td class="na">—</td><td class="na">—</td> <td>[R.ga4.this.organic]</td> <td style="font-size:11px;color:#bbb">Free</td> </tr> <tr> <td><strong>Direct</strong></td> <td class="na">—</td><td class="na">—</td><td class="na">—</td> <td>[R.ga4.this.direct]</td> <td style="font-size:11px;color:#bbb">Brand</td> </tr> </tbody> </table> <div style="margin-top:12px;font-size:13px;color:#444;line-height:1.6"> <strong>Reallocation rec:</strong> [R.narrative.reallocation] </div> </div> <!-- SECTION 5: BUDGET PACING --> <div class="sec"> <div class="sec-lbl">Budget Pacing — [current month YYYY]</div> <table style="margin-bottom:13px"> <thead><tr> <th>Monthly Budget</th><th>Days Elapsed</th><th>% of Month</th> <th>Spent MTD</th><th>% of Budget</th><th>Status</th> </tr></thead> <tbody><tr> <td><strong>[CURRENCY] [R.pacing.monthly_budget]</strong></td> <td>[R.pacing.days_elapsed] of [R.pacing.days_in_month]</td> <td>[R.pacing.pct_month_elapsed]%</td> <td><strong>[CURRENCY] [R.pacing.spend_mtd]</strong></td> <td>[R.pacing.pct_budget_spent]%</td> <td> [IF on_track:] <span class="b b-ok">On Track</span> [IF underpacing:] <span class="b b-warn">Underpacing</span> [IF overpacing:] <span class="b b-risk">Overpacing</span> </td> </tr></tbody> </table> <div class="pb-wrap"> <div class="pb-fill" style="width:[min(pct_budget_spent,100)]%"></div> </div> <div class="pb-lbl"> <span>[R.pacing.pct_budget_spent]% budget used</span> <span>[R.pacing.pct_month_elapsed]% of month elapsed</span> </div> <div style="margin-top:11px;font-size:13px;color:#444;line-height:1.5"> [IF underpacing:] Underpacing by <strong>[CURRENCY] [R.pacing.delta]</strong>. Projected month-end: <strong>[CURRENCY] [R.pacing.projected_month_end]</strong>. Increase daily caps to stay on budget. [IF overpacing:] Overpacing by <strong>[CURRENCY] [R.pacing.delta]</strong>. Projected month-end: <strong>[CURRENCY] [R.pacing.projected_month_end]</strong>. Reduce daily caps to avoid overrun. [IF on_track:] On track. Projected month-end: <strong>[CURRENCY] [R.pacing.projected_month_end]</strong>. Hold current pacing. </div> </div> <!-- SECTION 6: NEXT WEEK --> <div class="sec"> <div class="sec-lbl">Next Week — Priority List</div> <ol class="pri"> <li>[R.narrative.priorities[0]]</li> <li>[R.narrative.priorities[1]]</li> <li>[R.narrative.priorities[2]]</li> <li>[R.narrative.priorities[3]]</li> <li>[R.narrative.priorities[4]]</li> </ol> <div class="watch"> <div class="watch-t">Watchlist — check next Monday</div> [FOR EACH item in R.narrative.watchlist:] <div class="watch-i">· [item]</div> [/FOR] </div> </div> <!-- FOOTER --> <div class="ftr"> Generated by ZaneConnect &nbsp;·&nbsp; Claude Sonnet 4.6 &nbsp;·&nbsp; [R.generated_at formatted as "Mon DD YYYY, HH:MM GST"]<br> Meta: [R.accounts.meta] &nbsp;·&nbsp; Google Ads: [R.accounts.google_customer] &nbsp;·&nbsp; GA4: [R.accounts.ga4]<br> Zane Media &nbsp;·&nbsp; zanemediaco.com </div> </div> </div> </body> </html> ──────────────────────────────────────────────────────────── Assign the completed HTML string to: EMAIL_HTML STEP 9 — Send via Zapier → Gmail First, confirm the action key is still valid: Tool: Zapier MCP → list_enabled_zapier_actions params: { app: "gmail" } Find the send email action key → ZAPIER_GMAIL_ACTION Then send: Tool: Zapier MCP → execute_zapier_write_action app: "gmail" action: [ZAPIER_GMAIL_ACTION] instructions: "Send an HTML email from [SENDER_EMAIL] to [REPORT_EMAIL] with the subject and HTML body provided in params." params: to: "[REPORT_EMAIL]" from: "[SENDER_EMAIL]" subject: "📊 Weekly Report — [R.client] | [this_start as Mon DD] – [this_end as Sun DD, YYYY]" body: "[EMAIL_HTML]" output: "confirmation the email was sent, including message ID if available" On success: Print: "✅ Sent — [R.client] | [subject] | [timestamp]" On any error: Step A: Save EMAIL_HTML to Google Drive: Tool: Google Drive MCP → create_file title: "zc-[client]-[date]-UNSENT.html" mimeType: "text/html" content: [base64-encoded EMAIL_HTML] Step B: Print: "❌ Send failed — HTML saved to Drive as zc-[client]-[date]-UNSENT.html Download it and send manually from Gmail. Zapier error: [full error message]" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ROUTINE COMPLETE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Print final status: ══════════════════════════════════════════════ DONE ══════════════════════════════════════════════ Client: [CLIENT_NAME] Period: [this_start] – [this_end] Email: [✅ Sent / ❌ Failed — see Drive] Flags: [N] total ([X] critical · [Y] warning · [Z] info) Saved: zc-[client]-[date].json Next run: Monday [next Monday's date] ══════════════════════════════════════════════

Troubleshooting

Wrong account found in Phase 1

The CLIENT_NAME doesn't closely match the account name in your ad platform. Check the exact account name in Meta Business Manager or Google Ads and update the config.

Stream timeout

Click Run Now to retry. Re-running is always safe — the checkpoint is overwritten each attempt.

Email fails to send

The HTML is saved automatically to Drive as [client]-[date]-UNSENT.html. Download and send manually. Then go to zapier.com → App Connections to reconnect Gmail if the OAuth token expired.

GA4 shows inflated conversions

The conversion event is firing multiple times per session. Go to GTM and set the trigger to fire once per page or once per session — not on every DOM change.

Meta shows zero spend

Confirm in Meta Ads Manager whether campaigns are intentionally paused, or check for a budget exhaustion or payment method issue.


Built with Claude Routines + ZaneConnect MCP + Zapier + Google Drive MCP
Questions? hello@zanemediaco.com

Meta
Google Ads
GA4
+4 more

Ready to automate?

Connect your ad platforms to Claude AI. Get real-time analytics and insights.

Start Free Trial

No credit card required

Book a Free Consultation

Talk to our team to learn how ZaneConnect can automate your marketing analytics.

Schedule a Call