[ PAID MEDIA ]
/meta-ads-analyze
Check `ACME Agency/clients/<folder>/CLIENT.md`.
ACME Agency, <id> and you@example.com mark values that are per-agency — your install fills them with YOUR clients and accounts. If a section references a helper script you don't have yet, it ships with that workflow's install.Meta Ads Deep Analyzer
Triggers
/meta-ads-analyze ACME Agency— run interactively for a specific client/meta-ads-analyze— will use--clientarg or ask if not provided- ClickUp task tagged
claude-codewith name/description matching: analiz.*facebook,analiz.*meta,facebook.*report,meta.*reportprovjeri.*facebook,pogledaj.*meta,koliki.*cpl,meta.*analizoglas.*analiz,analiz.*oglas,performance.*meta,facebook.*performance
What this skill does
- Identifies the client from the argument or ClickUp task context
- Loads
CLIENT.md(or synthesizes from Drive + website if missing) for business context, conversion goal, competitors - Pulls data for 5 time windows: last 7d, 14d, 30d + last 3 full calendar months
- Fetches at 4 levels: campaign → ad set → ad → account breakdowns (placement, age/gender, device)
- Retrieves creative asset details (copy, thumbnails) for top 10 ads by spend
- Optionally downloads thumbnails and runs Claude vision analysis on creative visuals
- Optionally searches the Meta Ad Library for competitor ad angles and hooks
- Applies Ben Heath framework: classifies each creative as winner / learner / loser / fatigued
- Analyzes video metrics (hook rate, hold rate) when video data is available
- Generates prioritized, honest recommendations (budget, placement, creative, audience)
- Posts a structured Slack report (main message + threaded sections) to the client's channel
- Saves full raw + computed data to
ACME Agency/clients/<folder>/meta_analysis_YYYY-MM.json
How to run
# 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--clientarg case-insensitively - Resolve
ad_account(single) orad_accounts(array — some clients have multiple) - Get Slack channel via
getClientChannel()fromlib/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:
- Try Google Drive: search
Klijenti/<ClientName>/for a file with "upitnik" in the name → download - Fetch client website from
landing_pagefield in clients.json - 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:
GET /{ad_id}?fields=creative{body,title,description,thumbnail_url,image_url,video_id,object_story_spec}for each- Extract: hook text (first line of body), headline, CTA, format (video/carousel/single-image)
- Download thumbnail to temp dir using
downloadThumbnail()fromlib/meta_ads.mjs - 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?
- For videos: analyze thumbnail frame + read hook text from
bodyfield - 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:
- Trend check — 7d / 14d / 30d pulse: is CPL improving or worsening right now?
- MoM comparison — 3-month table with % deltas
- Campaign analysis — each active campaign with tier label and brief "why"
- Ad set analysis — best/worst performers, audience breakdown
- Creative analysis — ranked by CPL, with tiers and visual notes from Step 5
- Placement analysis — budget efficiency, HIGH CPL / NO LEADS flags
- Demographics — age × gender CPL grid, flag strongest segments
- Device — mobile vs desktop CPL
- Competitor signals — (if --competitors) copy angles being used
- 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. 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.jsonAND itscampaigns[]array is non-empty - [ ] Analyst's
summaryblock 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
tsnon-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
--competitorswas 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_readpermission 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
diagnosticiansubagent 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:
- Video testimonial / transformation — if client only runs static images
- New hook angle — based on which demographic segment is converting best
- Placement-native format — if Instagram Reels is cheapest placement, test vertical video
- Competitor counter-angle — based on what competitors are NOT doing (gap in the market)
- Winner expansion — if one creative is a winner, test 2-3 variations of the same angle