# /meta-ads-analyze

> Check `ACME Agency/clients/<folder>/CLIENT.md`.


# Meta Ads Deep Analyzer

## Triggers

- `/meta-ads-analyze ACME Agency` — run interactively for a specific client
- `/meta-ads-analyze` — will use `--client` arg or ask if not provided
- ClickUp task tagged `claude-code` with name/description matching:
  - `analiz.*facebook`, `analiz.*meta`, `facebook.*report`, `meta.*report`
  - `provjeri.*facebook`, `pogledaj.*meta`, `koliki.*cpl`, `meta.*analiz`
  - `oglas.*analiz`, `analiz.*oglas`, `performance.*meta`, `facebook.*performance`

## What this skill does

1. Identifies the client from the argument or ClickUp task context
2. Loads `CLIENT.md` (or synthesizes from Drive + website if missing) for business context, conversion goal, competitors
3. Pulls data for **5 time windows**: last 7d, 14d, 30d + last 3 full calendar months
4. Fetches at **4 levels**: campaign → ad set → ad → account breakdowns (placement, age/gender, device)
5. Retrieves creative asset details (copy, thumbnails) for top 10 ads by spend
6. Optionally downloads thumbnails and runs **Claude vision analysis** on creative visuals
7. Optionally searches the **Meta Ad Library** for competitor ad angles and hooks
8. Applies **Ben Heath framework**: classifies each creative as winner / learner / loser / fatigued
9. Analyzes video metrics (hook rate, hold rate) when video data is available
10. Generates prioritized, honest recommendations (budget, placement, creative, audience)
11. Posts a structured **Slack report** (main message + threaded sections) to the client's channel
12. Saves full raw + computed data to `ACME Agency/clients/<folder>/meta_analysis_YYYY-MM.json`

## How to run

```bash
# Full analysis + Slack report
node ACME Agency/scripts/meta_ads_analyze.mjs --client "ACME Agency"

# Dry run — full console output, no Slack post
node ACME Agency/scripts/meta_ads_analyze.mjs --client "ACME Agency" --dry-run

# Skip visual thumbnail analysis (faster)
node ACME Agency/scripts/meta_ads_analyze.mjs --client "ACME Agency" --no-vision

# Include Meta Ad Library competitor search
node ACME Agency/scripts/meta_ads_analyze.mjs --client "ACME Agency" --competitors
```

## Interactive step-by-step

### Step 1 — Identify client
- Read `ACME Agency/clients/clients.json`, match `--client` arg case-insensitively
- Resolve `ad_account` (single) or `ad_accounts` (array — some clients have multiple)
- Get Slack channel via `getClientChannel()` from `lib/slack.mjs`
- If not found → ask the user to confirm or provide the client name

### Step 2 — Load or build CLIENT.md
Check `ACME Agency/clients/<folder>/CLIENT.md`.

If it **exists**: read it — it has business type, target audience, key products, competitors, conversion goal, and CPL benchmarks.

If it **doesn't exist**:
1. Try Google Drive: search `Klijenti/<ClientName>/` for a file with "upitnik" in the name → download
2. Fetch client website from `landing_page` field in clients.json
3. Synthesize and save as `ACME Agency/clients/<ClientFolder>/CLIENT.md`

CLIENT.md informs:
- What counts as a conversion (lead form / pixel purchase / registration)
- Which audiences are strategic (diaspora, local, specific cities)
- Who the competitors are (for Ad Library search)
- What CPL is acceptable for this client

### Step 3 — Fetch all performance data (parallel batches)

**Batch A** — 30d detail levels:
```
level=campaign, last_30_days
level=adset,    last_30_days
level=ad,       last_30_days
```

**Batch B** — 30d breakdowns:
```
level=account, breakdowns=publisher_platform,platform_position, last_30_days
level=account, breakdowns=age,gender,  last_30_days
level=account, breakdowns=device_platform, last_30_days
```

**Batch C** — short windows + calendar months:
```
level=campaign, last_7_days
level=campaign, last_14_days
level=campaign, month1 (last full month)
level=campaign, month2
level=campaign, month3
```

All calls use fields including video metrics:
`campaign_name, adset_name, ad_name, impressions, reach, frequency, clicks,
inline_link_clicks, spend, ctr, cpm, cpp, actions, cost_per_action_type,
<id>, <id>,
<id>`

### Step 4 — Enrich all rows
`enrichRow()` from `lib/meta_ads.mjs` adds to every row:
- `_spend`, `_leads`, `_cpl`, `_freq`, `_cpm`, `_ctr`, `_cpc`
- `_hookRate` = (2s video views / impressions) × 100
- `_holdRate` = (ThruPlay / impressions) × 100
- `_hasVideo` = true if video metrics are present

### Step 5 — Creative asset analysis (skip with --no-vision)
For the **top 10 ads by spend** in the last 30d:
1. `GET /{ad_id}?fields=creative{body,title,description,thumbnail_url,image_url,video_id,object_story_spec}` for each
2. Extract: hook text (first line of body), headline, CTA, format (video/carousel/single-image)
3. Download thumbnail to temp dir using `downloadThumbnail()` from `lib/meta_ads.mjs`
4. Use Claude's **Read tool** on each image to visually assess:
   - Hook strength — is the first frame/headline immediately attention-grabbing?
   - Visual quality — clarity, professional look, brand consistency
   - Copy angle — pain point / desire / curiosity / social proof / authority / urgency
   - CTA clarity — is it clear what action to take?
   - Fatigue signal — does the creative look fresh or worn out given its frequency?
5. For videos: analyze thumbnail frame + read hook text from `body` field
6. Build `creativeInsights[]` with notes for the report

Note: if a thumbnail URL returns 403 (expired CDN link), log a warning and skip — do not crash.

### Step 6 — Meta Ad Library check (--competitors or competitors in CLIENT.md)
```
GET https://graph.facebook.com/v21.0/ads_archive
  ?search_terms=<competitor_name>
  &ad_type=ALL
  &ad_reached_countries=[country]
  &fields=page_name,ad_creative_bodies,<id>,<id>
  &limit=8
```
Run for: up to 2-3 competitors from CLIENT.md.
Extract: copy angles, hooks, headlines in active use by competitors → include in testing suggestions.
If `ads_read` permission is unavailable, skip gracefully (no crash).

### Step 7 — Ben Heath creative performance tiers
Apply to every ad in the 30d window:

| Tier | Condition | Action |
|------|-----------|--------|
| **WINNER** | CPL < 70% of avg AND frequency < 3 AND spend > €50 | Scale budget |
| **LEARNER** | CPL within 130% of avg AND spend < €50 | Let run |
| **LOSER** | CPL > 150% of avg AND spend > €30 | Pause |
| **FATIGUED** | Frequency ≥ 3 AND CPL trending up | Refresh creative |
| **LEARNING** | 0 leads AND spend < €30 | Give more time |

Video benchmark thresholds (when data available):
- Hook Rate (2s views / impressions): < 15% = weak, 15–30% = ok, > 30% = strong
- Hold Rate (ThruPlay / impressions): < 5% = weak, 5–15% = ok, > 15% = strong

### Step 8 — Generate analysis + recommendations
Produce for each section:

1. **Trend check** — 7d / 14d / 30d pulse: is CPL improving or worsening right now?
2. **MoM comparison** — 3-month table with % deltas
3. **Campaign analysis** — each active campaign with tier label and brief "why"
4. **Ad set analysis** — best/worst performers, audience breakdown
5. **Creative analysis** — ranked by CPL, with tiers and visual notes from Step 5
6. **Placement analysis** — budget efficiency, HIGH CPL / NO LEADS flags
7. **Demographics** — age × gender CPL grid, flag strongest segments
8. **Device** — mobile vs desktop CPL
9. **Competitor signals** — (if --competitors) copy angles being used
10. **Recommendations** — tagged: [BUDGET] [FATIGUE] [PLACEMENT] [AUDIENCE] [DEMO] [DEVICE] [ZERO] [TREND] [CREATIVE]

Keep the tone honest and specific: not optimistic spin, not doom-and-gloom. Call out what's actually working and what isn't, with clear reasoning tied to data.

**Testing roadmap** — always suggest 3–5 specific, actionable creative tests:
- Specify format (video vs single image vs carousel)
- Specify angle (pain point / transformation / curiosity / authority)
- Specify hook idea (concrete example, not vague)
- Explain why to test it (based on data or competitor signal)

### Step 9 — Save raw output
`ACME Agency/clients/<folder>/meta_analysis_YYYY-MM.json`

Contains: raw API rows, computed totals, enriched rows, creative insights, competitor ads, recommendations.

Save BEFORE delegating to the analyst — the analyst reads from this file.

### Step 10 — Delegate analysis to the analyst agent

Invoke the `analyst` subagent via the **Task tool** with `subagent_type: "analyst"`. Pass:

```
client: <client name>
data_source: <absolute path to meta_analysis_YYYY-MM.json from Step 9>
data_type: meta-ads
period: last 30 days (or whatever window was analyzed)
compare_to: previous 30 days (or previous calendar month, whichever is in the data)
focus: general health check — find the story
output_format: sections
```

The analyst returns a structured payload:
- `summary` — Slack-formatted main message (already follows SLACK_REPORT_STANDARD: single asterisks, • bullets, no emojis)
- `details` — optional longer breakdown for the thread
- `<id>` — guidance on whether further agents should be invoked

**Do NOT analyze the data yourself in this step.** The whole point of delegating is to keep the main context clean and let the analyst use its own context window for the data crunching. If the analyst stops because the data file is broken, surface the error and DO NOT invent numbers.

### Step 11 — Deliver via slack-reporter

Invoke the `slack-reporter` subagent via the Task tool with `subagent_type: "slack-reporter"`. Pass:

```
client: <client name>
channel: <resolved via getClientChannel(clientName) — the channel ID>
type: meta-ads-analysis
headline: "*Meta Ads — <ClientName> — <YYYY-MM-DD>*"
sections: <derived from analyst's summary block — slack-reporter will keep its format intact>
links:
  - label: "Kompletan izvještaj"
    url: <link to Drive doc if one was generated>
thread_details: <analyst's details block, if any>
language: <client's language from clients.json — hr/bs/de/en>
```

Do NOT call `postMessage()` or any helper from `lib/slack.mjs` directly anymore. The `slack-reporter` agent owns Slack delivery and enforces SLACK_REPORT_STANDARD.md.

## Verification (run AFTER Step 11 — prove the report shipped, don't assume)

Rubric: [verify.md](verify.md). Report integrity is now **enforced in `meta_ads_analyze.mjs`**:
before posting, `postSlackReport` runs `assertFiniteNumbers()` on the 30d totals (spend, leads,
reach, freq, ctr, cpl) and refuses to post if any is `NaN`/`undefined` or if the period/recs
data is empty — so a malformed fetch can never post "€NaN | undefined leads" to a client channel.
The raw JSON is saved BEFORE this gate, so a failure exits loud without losing the analysis.

Then check the rest before declaring done. If any fail, surface the specific gap to the user:

- [ ] Raw JSON file exists at `ACME Agency/clients/<folder>/meta_analysis_YYYY-MM.json` AND its `campaigns[]` array is non-empty
- [ ] Analyst's `summary` block has at least 3 named sections (KPIs / Findings / Recommendations or equivalent)
- [ ] Summary mentions specific numbers from the data (spend, CPL, leads) — NOT vague phrases like "performance was good"
- [ ] Slack post via slack-reporter returned `ts` non-null AND posted in the last 5 minutes
- [ ] Channel matches `clients.json[client].slack_channel`
- [ ] Slack message body uses `*` bold (NOT `**`), `•` bullets (NOT `-`), zero emoji characters
- [ ] If `--competitors` was passed: report mentions at least 1 competitor signal (or explicitly notes "no competitor data found")
- [ ] If recommendations name specific creatives, those creative IDs exist in the raw data (not hallucinated)
- [ ] Raw JSON has the time period actually requested (not silently shifted to a different window)

If any check fails, name the gap (e.g. "analyst returned summary with no numbers — re-invoke with `focus: extract specific KPIs`"). Never claim success when verification fails.

## Key files

| File | Purpose |
|------|---------|
| `ACME Agency/scripts/meta_ads_analyze.mjs` | Main script + `runMetaAdsAnalyze()` export |
| `ACME Agency/scripts/lib/meta_ads.mjs` | Shared Meta API library: enrichRow, computeTotals, fetchInsights, fetchAdCreatives, downloadThumbnail, searchAdLibrary, <id>, classifyCreative |
| `ACME Agency/scripts/lib/slack.mjs` | Slack posting: postMessage, getClientChannel |
| `ACME Agency/scripts/clickup_executor.mjs` | Routes `meta_ads_optimize` ClickUp tasks to this skill |
| `ACME Agency/clients/clients.json` | Client registry: ad_account, folder, country, competitors |
| `ACME Agency/clients/<folder>/CLIENT.md` | Per-client business context (auto-created if missing) |
| `ACME Agency/clients/<folder>/meta_analysis_YYYY-MM.json` | Saved output |

## Important rules

- **NEVER auto-apply** budget changes, campaign pauses, audience exclusions, or creative pauses — recommendations only
- **Always read CLIENT.md first** — it determines which action types count as conversions
- **Creative vision analysis**: if thumbnail returns 403, skip gracefully (CDN URLs expire) — log a warning, continue
- **Ad Library**: if `ads_read` permission is unavailable on the token, skip silently — do not crash
- **Multiple ad accounts**: run analysis for each account, merge data before analysis
- **0 active campaigns**: report clearly "no active campaigns found in this period" and exit cleanly
- **Conversion goal matters**: for ecommerce clients use purchase/add_to_cart; for lead gen use lead/leadgen_grouped
- Check CLIENT.md for known CPL benchmarks before labeling performance as "good" or "bad"
- **Delegate Slack output to slack-reporter** — never call `postMessage()` directly anymore
- **Delegate analysis to analyst** — never crunch the raw JSON yourself in the main context
- **If the fetch script fails twice with the same error, invoke the `diagnostician` subagent** before retrying. Pass the error verbatim. Common Meta failures (#your-channel OAuthException, page-permission errors, missing ads_read scope) almost always have UI/permission root causes — don't loop on code workarounds.

## Example ClickUp tasks that trigger this skill

- "Analiziraj Facebook oglase za ACME Agency"
- "Meta ads analiza — provjeri zadnji mjesec za ACME Agency"
- "Koliki je CPL na Facebooku za ACME Agency"
- "Provjeri performanse meta kampanja za ACME Agency"
- "Facebook ads performance report for ACME Agency"
- "Pogledaj što se dešava na Facebooku za ACME Agency ovaj mjesec"
- "Meta ads optimization report — ACME Agency"
- "Analiza oglasa i kreativa za ACME Agency — zadnja 3 mjeseca"

## Testing roadmap framework

When suggesting creative tests, always include at minimum:
1. **Video testimonial / transformation** — if client only runs static images
2. **New hook angle** — based on which demographic segment is converting best
3. **Placement-native format** — if Instagram Reels is cheapest placement, test vertical video
4. **Competitor counter-angle** — based on what competitors are NOT doing (gap in the market)
5. **Winner expansion** — if one creative is a winner, test 2-3 variations of the same angle
