All skills
hoodini avatar

/meta-ads

@2ac517a

Pull, analyze, manage, and CREATE Meta ads (Facebook, Instagram, Messenger, Threads, Click-to-WhatsApp) via the Marketing API. Use when the user mentions Meta/FB/IG ads, Ads Manager, ROAS, CPA, CPM, CTR, ad spend, campaign or ad set performance, creative fatigue, frequency, CTWA, audience/placement breakdowns, A/B ad tests, or wants to pause an ad, change a budget, duplicate a winner, or launch a new campaign from scratch. Trigger on Hebrew terms like פרסום בפייסבוק/באינסטגרם, פרסומות מטא, ביצועי קמפיין, פרסומות, צור קמפיין, השק קמפיין, A/B טסט. Trigger on questions like how are my ads doing, which creative is winning, is my ad fatigued, what's my CPA, drop a 30-day report, launch an A/B test for my course, build a campaign with these 3 angles. Trigger even if auth isn't set up — the skill walks through one-time setup. Do NOT use for organic Instagram analytics, Reels view counts on non-promoted posts, or organic Threads engagement; those need the Instagram Graph API.

Use this Skill: https://skilld.dev/gh/hoodini/ai-agents-skills/meta-ads

This session only. Nothing lands on disk.

referencescampaign-creation.md

≈2.6k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Creating campaigns from scratch

scripts/create_campaign.py builds an entire campaign tree (campaign → ad sets → creatives → ads) in one call from a spec JSON. This is a write operation — read write-actions.md first; the "Campaign creation" section at the bottom of that file is mandatory before you call --confirm for the first time in a session.

The workflow

  1. Write a spec JSON that describes the campaign you want.
  2. Run --dry-run. This reads the spec, queries Meta for account currency, resolves interest names to IDs, and prints the plan — no writes.
  3. Walk the plan back to the user. They name the change to confirm.
  4. Run --confirm. The script creates everything PAUSED by default and writes a state JSON with every ID.
  5. If the user wants to activate: they toggle ACTIVE in Ads Manager (or via the API). Never flip ACTIVE programmatically as part of creation — that removes the final checkpoint.

Spec format (minimum viable)

{
  "campaign_name": "My Campaign - Q2 Flight",
  "objective": "OUTCOME_TRAFFIC",
  "status": "PAUSED",
  "special_ad_categories": [],
  "identity": {
    "page_id": "1234567890",
    "instagram_user_id": "17841400000000000"
  },
  "landing_url": "https://example.com",
  "ad_sets": [
    {
      "name": "Angle A — Broad",
      "daily_budget": 50.00,
      "optimization_goal": "LINK_CLICKS",
      "billing_event": "LINK_CLICKS",
      "targeting": {
        "countries": ["IL"],
        "age_min": 25,
        "age_max": 45,
        "interests": ["Artificial intelligence", "Productivity"],
        "publisher_platforms": ["instagram"],
        "instagram_positions": ["stream", "story", "reels"]
      },
      "image_path": "./creative/angle_a.png",
      "ads": [
        {
          "name": "A1_Direct",
          "message": "Primary body copy — can be multi-line.",
          "headline": "Short punchy headline",
          "description": "Optional secondary line",
          "cta": "LEARN_MORE"
        }
      ]
    }
  ]
}

Field reference

Top level

Field Required Notes
campaign_name yes Shows up in Ads Manager exactly as typed. Put identifiers (e.g. test name, date) in here for sanity later.
objective yes Meta's modern objectives: OUTCOME_TRAFFIC, OUTCOME_AWARENESS, OUTCOME_ENGAGEMENT, OUTCOME_LEADS, OUTCOME_SALES, OUTCOME_APP_PROMOTION. For courses/products without a pixel, use OUTCOME_TRAFFIC. For courses/products with a pixel firing Purchase, use OUTCOME_SALES.
status no Default PAUSED. Only override with ACTIVE if you're absolutely sure — you lose the review step.
special_ad_categories no Default []. Required ["CREDIT"], ["EMPLOYMENT"], ["HOUSING"], ["ISSUES_ELECTIONS_POLITICS"], or ["FINANCIAL_PRODUCTS_SERVICES"] for those regulated verticals. Wrong category here can get the whole campaign rejected.
buying_type no Default AUCTION. RESERVED is for Reach & Frequency (needs account eligibility).
identity.page_id yes The Facebook Page running the ads. Discover via GET /me/accounts or GET /act_.../promote_pages.
identity.instagram_user_id no The IG account running the ads (discover via the page's instagram_business_account field). Required if you want ads to show with your IG handle instead of a generic "Sponsored by Page Name".
landing_url yes for traffic Where clicks go.

Ad set level

Field Required Notes
name yes Descriptive. Good habits: include angle + audience descriptor, e.g. "Angle 1 — Broad IL 25-45 AI".
daily_budget one of Major units in account currency (50.00 for ₪50). Script queries the currency, converts to minor units (agorot/cents).
lifetime_budget + end_time one of Use when you want a total spend cap instead of daily pacing. end_time is ISO 8601.
billing_event no Default LINK_CLICKS. IMPRESSIONS if optimizing for reach.
optimization_goal no Default LINK_CLICKS. Common: LANDING_PAGE_VIEWS, OFFSITE_CONVERSIONS (needs pixel), REACH, IMPRESSIONS.
bid_strategy no Default LOWEST_COST_WITHOUT_CAP. Use COST_CAP with bid_amount when you want to hold a CPA ceiling.
bid_amount conditional Major units. Required if bid_strategy=COST_CAP or LOWEST_COST_WITH_BID_CAP.
status no Default PAUSED.
targeting.countries yes ISO-2 list, e.g. ["IL"]. Or use full geo_locations dict for regions/cities.
targeting.age_min / age_max no Defaults to Meta's (18-65). Narrow deliberately.
targeting.genders no [1] male, [2] female, omit for all.
targeting.interests no List of names — script resolves to IDs via /search?type=adinterest. Names with no match are dropped and warned. For stability across runs, use interest_ids instead.
targeting.interest_ids no Pre-resolved numeric IDs. Bypasses the search lookup.
targeting.publisher_platforms no Default ["instagram"]. Add "facebook", "messenger", "audience_network".
targeting.instagram_positions no Default ["stream", "story", "reels"].
targeting.facebook_positions no Default ["feed", "story", "video_feeds"] when facebook is in publisher_platforms.
targeting.advantage_audience no Default true. Allows Meta to expand beyond your interests when it finds high-intent users outside them. Turn off with false for strict targeting tests.
targeting.custom_audiences no List of {"id": "..."}.
targeting.excluded_custom_audiences no Same shape. Useful for excluding existing customers.
image_path one of Path to image file, uploaded to /act_.../adimages once per ad set. Recommended: 4:5 or 1:1, PNG, under 4 MB.
image_hash one of Pre-uploaded image hash (skip upload).

Ad level

Each ad inside an ad set uses the ad set's image.

Field Required Notes
name yes Ad is named Ad_<name> and its creative Creative_<name>. Keep short and unique within the campaign.
message yes Primary body text. Multi-line allowed. No length limit enforced by the API, but Meta truncates in-feed around 125 characters before "See More".
headline yes Bold headline below the image. Keep under 40 characters for mobile.
description no Small gray line below headline. Often cut by placements.
cta no Default LEARN_MORE. Others: SIGN_UP, SHOP_NOW, DOWNLOAD, GET_OFFER, GET_QUOTE, SUBSCRIBE, CONTACT_US, APPLY_NOW, WATCH_MORE, INSTALL_MOBILE_APP, USE_APP, MESSAGE_PAGE, WHATSAPP_MESSAGE, NO_BUTTON.
standard_enhancements no Default false (opts out). Meta's auto-enhancements alter the creative — brightness, cropping, text overlays. Leave opt-out if you care about pixel-for-pixel fidelity.
status no Default PAUSED.

Currency handling

The script queries /act_.../?fields=currency once at the start and converts daily_budget / lifetime_budget / bid_amount from major units to whatever minor unit the account uses:

  • ILS / USD / EUR / GBP etc. → × 100 (agorot, cents, pence)
  • JPY / KRW / VND / ISK / TWD / XAF / XOF / CLP → × 1 (no minor unit)

Write budgets in major units only. Never try to pre-convert.

Interest resolution

targeting.interests names are resolved via Meta's /search?type=adinterest endpoint. The script takes the top-ranked hit per name. This is convenient for readable specs but has two caveats:

  1. The search ranking can change. A spec that resolved to "Productivity" today might resolve to "Productivity (software category)" tomorrow.
  2. Search can return no match. Those names are dropped with a [warn] log.

For specs you'll re-run months later and want identical targeting, do one dry-run, copy the resolved_interests IDs into interest_ids, and delete interests. Now the targeting is frozen.

State file and rollback

Every --confirm run writes <spec>_state_<timestamp>.json with every object ID:

{
  "ok": true,
  "campaign_id": "120...",
  "ad_sets": [
    {"adset_id": "120...", "image_hash": "...", "ads": [{"ad_id": "120...", "creative_id": "120..."}]}
  ],
  "objects": [{"type": "campaign", "id": "120...", "name": "..."}, ...]
}

scripts/rollback_creation.py --state <file> --pause pauses everything. --delete permanently deletes in reverse order (ads → creatives are orphaned → adsets → campaign). Prefer --pause unless you really want the objects gone from Ads Manager history.

Common errors and how to handle them

error_subcode 1885036 — Ad account is unsettled. The campaign objects get created fine but nothing will deliver until the account balance is paid. Don't block on this at creation time; surface it to the user and continue.

The ad creative spec is invalid. Usually missing page_id or a malformed call_to_action value. Double-check the cta is in VALID_CTAS and the landing URL is fully qualified (https://...).

Invalid parameter on flexible_spec. Means an interest ID is stale. Re-run the dry-run so search re-resolves names, or remove the offending interest.

special_ad_categories mismatch. If your ad is in a regulated vertical and you forgot to mark it, Meta rejects the whole ad set. Reviewable in the ad set's delivery_info in Ads Manager.

Account not admin of page. The ad account owner must have advertiser-or-higher role on the Facebook Page. Fix in Business Settings → Pages → assign.

Instagram identity missing. If instagram_user_id is omitted, ads still run on Instagram placements but show "Sponsored by <Page Name>" instead of your IG handle. Almost always you want to set it.

When to NOT use this script

  • You only want to duplicate an existing ad. Use scripts/duplicate_ad.py — it copies targeting, creative, and settings in one shot and is one API call instead of six.
  • You're iterating on creative for an existing ad set. Creating a new campaign per iteration pollutes Ads Manager. Add new ads to the existing ad set via a partial spec or do it in the UI.
  • You want dynamic creative / catalog ads / collection ads. Those have different creative shapes (asset_feed_spec, product_set_id, template_data). This script only handles single-image link ads. Extend it or build a sibling script.

Source: SKILL.md on GitHub

2 warnings15d3 checks · Risk SAFE
  • Gen Agent Trust Hub15d

    The skill is generally safe, providing scripts to manage Meta ads via the official Marketing API. It contains comprehensive safety protocols for financial operations. A low-risk surface for indirect prompt injection exists due to the processing of external ad data, but this is mitigated by mandatory user confirmation steps.

  • Socket15d

    1 alert: gptAnomaly

  • Snyk15d

    Risk: MEDIUM · 1 issue

Signed by skilld at 2ac517a. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 days ago.

Steadyupdated 5 months ago

README badge

README badge for hoodini/ai-agents-skills/meta-ads