PORTAL / LIBRARY / meta-ads-analyze

[ PAID MEDIA ]

/meta-ads-analyze

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

Download the skill file (.md)

Placeholders like 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

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

# 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

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:

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:

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:
  1. For videos: analyze thumbnail frame + read hook text from body field
  2. 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:

TierConditionAction
WINNERCPL < 70% of avg AND frequency < 3 AND spend > €50Scale budget
LEARNERCPL within 130% of avg AND spend < €50Let run
LOSERCPL > 150% of avg AND spend > €30Pause
FATIGUEDFrequency ≥ 3 AND CPL trending upRefresh creative
LEARNING0 leads AND spend < €30Give more time

Video benchmark thresholds (when data available):

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:

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:

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:

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

FilePurpose
ACME Agency/scripts/meta_ads_analyze.mjsMain script + runMetaAdsAnalyze() export
ACME Agency/scripts/lib/meta_ads.mjsShared Meta API library: enrichRow, computeTotals, fetchInsights, fetchAdCreatives, downloadThumbnail, searchAdLibrary, <id>, classifyCreative
ACME Agency/scripts/lib/slack.mjsSlack posting: postMessage, getClientChannel
ACME Agency/scripts/clickup_executor.mjsRoutes meta_ads_optimize ClickUp tasks to this skill
ACME Agency/clients/clients.jsonClient registry: ad_account, folder, country, competitors
ACME Agency/clients/<folder>/CLIENT.mdPer-client business context (auto-created if missing)
ACME Agency/clients/<folder>/meta_analysis_YYYY-MM.jsonSaved output

Important rules

Example ClickUp tasks that trigger this skill

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