Claude Skill

meta-ads

Plan and create Meta (Facebook + Instagram) advertising campaigns end-to-end via the Hyper MCP, defaulting to Advantage+ automation. Use when the user wants to launch Meta ads, Facebook ads, Instagram ads, Advantage+ campaigns, carousel ads, dynamic creative ads, set up Meta conv

LLM Mart · 0 points · 3 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download hyperfx-ai-marketing-skills-skills_meta-ads-abd711a.zip · 46 KB
Part of hyperfx-ai/marketing-skills — 30 skills

Install

skills CLI npx skills add https://github.com/hyperfx-ai/marketing-skills/tree/main/skills/meta-ads
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install hyperfx-ai-marketing-skills@llmmart
Git git clone https://github.com/hyperfx-ai/marketing-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole hyperfx-ai/marketing-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Meta Ads

Strategic guide for creating and managing Meta advertising campaigns, analyzing performance, and building dashboards from cached data. Default to Advantage+ unless the user explicitly requests manual control.

Out of scope — defer to other skills

Request Send them to
Competitor or public ad research (Meta Ads Library) meta-ads-library
Ad creative generation (images, copy variants) ad-creative-generation
Google Ads campaigns google-ads
Pinterest / TikTok / Amazon paid campaigns pinterest-ads, tiktok-ads, amazon-ads

Requirements

  • Hyper MCP installed and connected. https://app.hyperfx.ai/mcp
  • Meta Business integration connected (Facebook + Instagram, with at least one ad account and one Page) at https://app.hyperfx.ai/apps.
  • Firecrawl integration connected for site research and screenshot grounding (discovery phase).

If search("meta_ads_adaccount_list") does not find meta_ads_adaccount_list, stop and tell the user to enable Hyper MCP and connect Meta Business.

If you suspect a connection issue (missing ad accounts, page publishing failures, or permission errors), call meta_ads_health_check() and report the diagnostics before proceeding.

How to run the tools in this skill

Every tool in this skill is named by its canonical tool name. Run it with the call your surface gives you:

Surface Find a tool Run it
MCP client (Claude, Cursor, Codex, ChatGPT) search("<what you want to do>"), then describe("<name>") call("<name>", {...})
Hyper CLI hyperai search "<what you want to do>", then hyperai describe <name> hyperai call <name> --json '{...}'

If a tool is not found, its integration is not connected or not enabled for the workspace: stop and tell the user which integration to connect.

Tool names

Use the exact tool name from your connected tool list. Canonical names are meta_ads_* (listed below). On Hyper platform chat, legacy meta_business_* names (e.g. meta_ads_adaccount_list) and the retired plural names (e.g. meta_ads_campaign_create) resolve to the same tools via aliases — if a call fails with "tool not found", search the live catalog for the canonical name.

Group Tools
Discovery meta_ads_adaccount_list, meta_ads_owned_pages_list, meta_ads_pages_search, meta_accounts_list, meta_ads_instagram_accounts_list
Health & sync meta_ads_health_check, meta_ads_health_get
Tracking assets meta_ads_ad_pixels_list, meta_ads_ad_pixels_get, meta_ads_custom_audiences_list, meta_ads_lookalike_audiences_list, meta_ads_targeting_search
Step-by-step creation (preferred) meta_ads_campaign_create, meta_ads_adset_create, meta_ads_ad_create, meta_ads_ad_images_upload, meta_ads_creative_create
Read & preview meta_ads_campaign_get, meta_ads_campaigns_search, meta_ads_adset_list, meta_ads_ad_list, meta_ads_ad_get, meta_ads_ad_previews_get
Insights & dashboards meta_ads_insights_get, data_apps_build, database_query
Launch & edits meta_ads_campaigns_activate, meta_ads_campaign_update, meta_ads_adset_update, meta_ads_ad_update
Automated rules meta_ads_adrule_create, meta_ads_adrule_list, meta_ads_adrule_get, meta_ads_adrule_update, meta_ads_adrule_delete, meta_ads_adrule_history_list
Site research firecrawl_branding_extract, firecrawl_screenshots_create

CLI users: translate tool names with the hyper-cli skill (hyperai search "<tool name>").


Rules that must never be forgotten

BUDGETS IN CENTS: $20.00 = 2000. $5.50 = 550. $100 = 10000. Never pass dollar amounts directly.

ACTIVATE, DON'T UPDATE: Use meta_ads_campaigns_activate(campaign_id) to go live. Never meta_ads_campaign_update(status="ACTIVE") — that silently leaves ad sets and ads PAUSED so nothing serves.

ALWAYS START PAUSED: Create campaigns with status="PAUSED". Never launch live without user review.

BUILD STEP BY STEP: Create campaigns with the individual tools — meta_ads_campaign_create → meta_ads_adset_create → meta_ads_creative_create → meta_ads_ad_create, capturing each id from the previous response. (The old blueprint tools were removed.) The tools validate requests before sending — campaign objective rules, bid-strategy/bid-amount pairing, billing-event/optimization-goal compatibility, budget coherence — but objective-specific ad-set fields (optimization_goal, promoted_object) are YOUR responsibility: match them to the campaign objective using the reference file for the campaign type.

REGULATED ADVERTISERS NEED special_ad_categories: For gambling, financial, housing, employment, credit, or political advertisers, declare the category on meta_ads_campaign_create (e.g. special_ad_categories=["ONLINE_GAMBLING_AND_GAMING"]).

FIXED RUN WINDOWS GO ON THE AD SET: When the user gives a run duration or dates ("run it for 7 days", "through end of month"), set start_time AND end_time (ISO 8601) on meta_ads_adset_create. A daily-budget ad set with no end_time runs continuously until manually paused — the requested window is silently lost.

GROUND COPY IN THE DESTINATION PAGE (RESEARCH FIRST): When the brief centers on a URL ("build a campaign for https://..."), fetch that page FIRST (web_pages_fetch) — before discovery and before writing any copy — and ground ad copy in what the page actually says (product name, value props, offer). Never invent copy for a page you have not read; even when the user supplies exact copy or headlines, fetch the page to verify the destination matches the offer.

EU-TARGETED AD SETS NEED DSA FIELDS: If an ad set targets the EU, set dsa_beneficiary and dsa_payor on meta_ads_adset_create (who benefits from / pays for the ad) — required under the EU Digital Services Act, or delivery is restricted.

UTMs ON EVERY DESTINATION AD (url_tags): Set url_tags (UTM params, e.g. utm_source=meta&utm_medium=paid&utm_campaign=...) on every creative that drives to a destination — downstream measurement (e.g. AppsFlyer + a data warehouse) stitches on these, so an ad without UTMs is effectively unmeasurable. Use the advertiser's canonical template; if you don't have one, ask rather than ship untracked.

See references/constraints.md for the full constraint set.

All reference files live in references/. Read them at references/<file> (e.g. references/discovery.md). They are not in the same directory as this SKILL.md.


Core process

Every task follows this sequence. Do not skip steps.

  1. Identify the goal — creation, analysis, or both?
  2. Check the routing table and read the referenced files before calling any tools
  3. Make a written plan — state campaign type, budget in cents, optimization goal, and sequence of steps; show it before acting
  4. Execute step by step, re-checking references/constraints.md at each creation step
  5. Show ad previews before activation
  6. Activate only when the user approves using meta_ads_campaigns_activate()

Automated / scheduled runs: If there is no user present to interact with (e.g. a scheduled task), skip steps 3 and 6. Do not write a plan and wait for confirmation — proceed directly. Do not activate unless activation was explicitly included in the task instructions.


Routing table

The user wants to… Read these files first
Create a sales / conversion campaign references/discovery.md → references/campaigns/sales.md
Create a leads campaign references/discovery.md → references/campaigns/leads.md
Create a traffic campaign references/discovery.md → references/campaigns/traffic.md
Create an awareness or engagement campaign references/discovery.md → references/campaigns/awareness-engagement.md
Create an app promotion campaign references/discovery.md → references/campaigns/app-promotion.md
Create a campaign (any objective) references/discovery.md → the matching references/campaigns/*.md above, then build step by step
Analyze performance / query insights references/analytics.md
Audit an account / find optimization opportunities references/account-audit.md
Set up automated rules (auto-pause, budget guards, alerts) references/automated-rules.md
Build a Meta dashboard or data app references/analytics.md → references/dashboards.md
Analyze performance, then create a campaign references/analytics.md → references/discovery.md → relevant campaign file
Build a funnel / multiple campaigns at once (TOF/MOF/BOF) references/multi-campaign-funnel.md → references/discovery.md → per-tier campaign files
Objective not yet known references/discovery.md — discovery clarifies the goal

Worked examples

Files (marketing-skills)
  • references
    • campaigns
      • app-promotion.md 8.8 KB
        # App Promotion Campaign Workflow (OUTCOME_APP_PROMOTION)
        
        ## When to use
        
        Use for campaigns that drive mobile app installs, in-app events, or app re-engagement for a mobile application registered in Meta's App Dashboard.
        
        ## Before starting
        
        Re-read [../constraints.md](../constraints.md). Most relevant:
        - Budget in cents (×100)
        - `promoted_object` required for OUTCOME_APP_PROMOTION — missing it causes a cryptic API error
        - `targeting_automation` inside `targeting`
        - Budget at campaign level for Advantage+
        - `meta_ads_adset_create` takes `ad_account_id` (numeric, no `act_` prefix) + a `body` dict — all ad set fields inside `body`
        - `meta_ads_ad_create` takes `ad_account_id` + a `body` dict — no separate top-level args
        - Use `meta_ads_campaigns_activate()`, not `update_campaign(status="ACTIVE")`
        
        ---
        
        ## Required inputs
        
        | Input | How to get it |
        |---|---|
        | Ad account ID | Discovery step 1 |
        | Facebook Page ID | Discovery step 2 (`meta_ads_owned_pages_list`) |
        | Mobile App ID | Ask the user — from Meta App Dashboard (not the app store ID) |
        | App store URL | Ask the user — full Apple App Store or Google Play URL with https:// |
        | Optimization goal | App installs (default) or specific in-app events |
        | Budget amount + currency | Ask the user |
        | Daily or lifetime | Ask; lifetime needs start + end dates |
        | Ad creative (image_hash) | Upload via `meta_ads_ad_images_upload` |
        
        `promoted_object` with both `application_id` **and** `object_store_url` is required. Missing either will fail at the API.
        
        ---
        
        ## Decision point: installs vs re-engagement
        
        ```
        User wants app promotion
          ├── New installs (most common)
          │     optimization_goal: APP_INSTALLS
          │     call_to_action: DOWNLOAD
          │
          └── Re-engagement (existing users)
                optimization_goal: APP_EVENT (confirm specific event with user)
        ```
        
        Default to APP_INSTALLS unless re-engagement is specified.
        
        ---
        
        ## Step-by-step creation (default)
        
        ### Pre-build checklist
        
        - [ ] App ID confirmed (from Meta App Dashboard — NOT the app store numeric ID)
        - [ ] App store URL confirmed (full URL with https://) — this exact URL becomes both the ad set's `object_store_url` AND the creative's CTA `link`
        - [ ] Budget confirmed and converted to cents
        - [ ] Page ID captured explicitly from discovery
        - [ ] Creative assets ready or will use app store screenshots
        - [ ] Adding to an **existing** ad set? Call `meta_ads_adset_get` first and copy `promoted_object.object_store_url` into the creative link verbatim
        
        ### 1. Create campaign
        
        ```python
        meta_ads_campaign_create(
            ad_account_id="123456789",
            body={
                "name": "App Installs - [App Name] - [Date]",
                "objective": "OUTCOME_APP_PROMOTION",
                "status": "PAUSED",
                "daily_budget": 2000,              # $20/day in cents — Advantage+ only; omit for manual
                "is_skadnetwork_attribution": True # REQUIRED if the ad set targets iOS 14+ (see note in step 2);
                                                # campaign-level and immutable — can't be added later
            }
        )
        ```
        
        → Capture `campaign_id`.
        
        ### 2. Create ad set
        
        > `meta_ads_adset_create` takes `ad_account_id` (numeric, no `act_` prefix) plus a `body` dict. Every ad set field goes inside `body`.
        
        **Advantage+ (default):**
        
        ```json
        {
          "ad_account_id": "123456789",
          "body": {
            "name": "US Broad - App Installs",
            "campaign_id": "<campaign_id>",
            "optimization_goal": "APP_INSTALLS",
            "billing_event": "IMPRESSIONS",
            "targeting": {
              "geo_locations": {
                "countries": [
                  "US"
                ]
              },
              "targeting_automation": {
                "advantage_audience": 1
              }
            },
            "promoted_object": {
              "application_id": "<app_id>",
              "object_store_url": "https://apps.apple.com/app/example/id123456789"
            }
          }
        }
        ```
        
        > **CRITICAL**: `promoted_object` with both `application_id` AND `object_store_url` is required. Omitting either causes a cryptic API error.
        
        > **CRITICAL**: `application_id` is the Meta App Dashboard ID, not the numeric App Store ID.
        
        **Manual (only when user explicitly requests):**
        
        ```json
        {
          "ad_account_id": "123456789",
          "body": {
            "name": "US 18-35 - App Installs",
            "campaign_id": "<campaign_id>",
            "optimization_goal": "APP_INSTALLS",
            "billing_event": "IMPRESSIONS",
            "daily_budget": 2000,
            "targeting": {
              "geo_locations": {
                "countries": [
                  "US"
                ]
              },
              "age_min": 18,
              "age_max": 35
            },
            "promoted_object": {
              "application_id": "<app_id>",
              "object_store_url": "https://apps.apple.com/app/example/id123456789"
            }
          }
        }
        ```
        
        → Capture `adset_id`.
        
        > **iOS 14+ targeting (SKAdNetwork).** To target "iOS 14 and above" set
        > `targeting.user_os: ["iOS_ver_14.0_and_above"]` on the ad set **and** create the parent
        > campaign with `is_skadnetwork_attribution: true` (a **campaign-level** flag on
        > `meta_ads_campaign_create`, not an ad-set field). Without it Meta silently clamps the
        > target to `iOS_ver_14.0_to_14.4` and the ad set shows as **"Apple App Store (iOS 13.7 or
        > earlier)"** in Ads Manager. The flag is **immutable after the campaign is created** — if
        > you forgot it, delete the campaign and recreate it. Creating an open-ended-iOS app ad set
        > under a non-SKAdNetwork campaign is now rejected with this guidance rather than shipping
        > the broken target. **Android is unaffected** — this is iOS-only (SKAdNetwork is Apple's
        > framework).
        >
        > **user_os value format.** The ONLY valid values are `iOS_ver_<v>_and_above` /
        > `Android_ver_<v>_and_above` (open-ended), `iOS_ver_<min>_to_<max>` (range), or bare
        > `iOS` / `Android` (all versions). Do **not** use `iOS_14`, `iOS 14+`, or a
        > `user_os_version` field — Meta rejects them with *"Invalid User_os Value"* / *"not a
        > valid target spec field"*. (Shorthand is auto-normalized as a safety net, but emit the
        > canonical value.)
        
        ### 3. Upload image
        
        ```python
        meta_ads_ad_images_upload(account_id="act_123456789", image_url="<url>")
        ```
        
        → Capture `image_hash`.
        
        ### 4. Create ad
        
        > **CRITICAL**: `meta_ads_ad_create` takes `ad_account_id` plus a single `body` dict. No separate top-level args.
        
        > **CRITICAL — the creative link MUST match the ad set's `object_store_url` exactly.** Meta rejects the ad with *"Object store URL does not match promoted object"* if the creative's CTA destination differs from the store URL on the ad set's `promoted_object`. **Never guess or hand-type the store URL.** If you did not just create the ad set yourself (e.g. you are adding creatives to an existing ad set), call `meta_ads_adset_get(<adset_id>)` **first**, read `promoted_object.object_store_url`, and copy that exact value into:
        > - `link_data.link`, and
        > - `link_data.call_to_action.value.link` (if you set a CTA value)
        >
        > They must be character-for-character identical to the ad set's `object_store_url`.
        
        ```json
        {
          "ad_account_id": "123456789",
          "body": {
            "name": "App Install Ad - [Creative]",
            "adset_id": "<adset_id>",
            "creative": {
              "object_story_spec": {
                "page_id": "<page_id>",
                "link_data": {
                  "link": "<EXACT object_store_url from the ad set's promoted_object>",
                  "image_hash": "<image_hash>",
                  "call_to_action": {
                    "type": "DOWNLOAD"
                  },
                  "message": "Download [App Name] and [key benefit].",
                  "name": "Get the App"
                }
              }
            }
          }
        }
        ```
        
        > Omit `status` — ads default to PAUSED.
        
        → Capture `ad_id`.
        
        ### 5. Preview and activate
        
        ```python
        # creative_id is in the ad creation response: response.creative.id
        meta_ads_ad_previews_get(creative_ids=["<creative_id>"])
        meta_ads_campaigns_activate(campaign_id="<campaign_id>")  # when user approves
        ```
        
        ---
        
        ## Common failure points
        
        | Symptom | Cause | Fix |
        |---|---|---|
        | `Object store URL does not match promoted object` (code 100) | Creative CTA link ≠ ad set's `promoted_object.object_store_url` | Read `promoted_object.object_store_url` via `meta_ads_adset_get` and copy it exactly into the creative's `link` (don't guess the URL) |
        | Cryptic API error on ad set | Missing `promoted_object` | Add `application_id` + `object_store_url` |
        | "App not found" error | Wrong `application_id` | Verify in Meta App Dashboard, not app store |
        | `object_store_url` invalid | URL format wrong or missing https:// | Use full URL with https:// |
        | Budget rejected | Passed dollars not cents | Multiply by 100 |
        | Ad set error: unexpected argument | Fields outside `body` | All fields must be inside the `body` dict |
        | Campaign ACTIVE but no installs | Used `update_campaign(status="ACTIVE")` | Use `meta_ads_campaigns_activate()` |
        | Ad set shows "iOS 13.7 or earlier" / `user_os` reads `iOS_ver_14.0_to_14.4` | Parent **campaign** not created as iOS-14 SKAdNetwork, so Meta clamped the open-ended target | Recreate the campaign with `meta_ads_campaign_create(..., is_skadnetwork_attribution=true)` — it's a campaign-level flag, immutable after creation |
        
      • awareness-engagement.md 4.6 KB
        # Awareness & Engagement Campaign Workflow
        
        ## When to use
        
        **OUTCOME_AWARENESS**: Maximize brand reach and impressions. No user action required.
        
        **OUTCOME_ENGAGEMENT**: Drive interactions — post likes, shares, comments, Page follows, or video views.
        
        These are the simplest campaign types. No pixel required. No complex `promoted_object` setup.
        
        ## Before starting
        
        Re-read [../constraints.md](../constraints.md). Most relevant:
        - Budget in cents (×100)
        - `targeting_automation` inside `targeting`
        - Budget at campaign level for Advantage+
        - `meta_ads_adset_create` takes `ad_account_id` (numeric, no `act_` prefix) + a `body` dict — all ad set fields inside `body`
        - `meta_ads_ad_create` takes `ad_account_id` + a `body` dict — no separate top-level args
        - Use `meta_ads_campaigns_activate()`, not `update_campaign(status="ACTIVE")`
        
        ---
        
        ## Required inputs
        
        | Input | How to get it |
        |---|---|
        | Ad account ID | Discovery step 1 |
        | Facebook Page ID | Discovery step 2 (`meta_ads_owned_pages_list`) |
        | Goal: awareness or engagement? | Ask the user if not stated |
        | Budget amount + currency | Ask the user |
        | Daily or lifetime | Ask; lifetime needs start + end dates |
        | Ad creative (image_hash) | Upload via `meta_ads_ad_images_upload` |
        
        No pixel needed. `promoted_object` is optional for engagement.
        
        ---
        
        ## Optimization goals
        
        | Objective | optimization_goal |
        |---|---|
        | OUTCOME_AWARENESS | `REACH` |
        | OUTCOME_ENGAGEMENT | `POST_ENGAGEMENT` |
        
        ---
        
        ## Step-by-step creation (default)
        
        ### 1. Create campaign
        
        **Awareness:**
        ```python
        meta_ads_campaign_create(
            ad_account_id="123456789",
            body={
                "name": "Awareness - [Brand] - [Date]",
                "objective": "OUTCOME_AWARENESS",
                "status": "PAUSED",
                "daily_budget": 2000
            }
        )
        ```
        
        **Engagement:**
        ```python
        meta_ads_campaign_create(
            ad_account_id="123456789",
            body={
                "name": "Engagement - [Brand] - [Date]",
                "objective": "OUTCOME_ENGAGEMENT",
                "status": "PAUSED",
                "daily_budget": 2000
            }
        )
        ```
        
        → Capture `campaign_id`.
        
        ### 2. Create ad set
        
        > `meta_ads_adset_create` takes `ad_account_id` (numeric, no `act_` prefix) plus a `body` dict. Every ad set field goes inside `body`.
        
        **Advantage+ awareness:**
        
        ```json
        {
          "ad_account_id": "123456789",
          "body": {
            "name": "US Broad - Brand Awareness",
            "campaign_id": "<campaign_id>",
            "optimization_goal": "REACH",
            "billing_event": "IMPRESSIONS",
            "targeting": {
              "geo_locations": {
                "countries": [
                  "US"
                ]
              },
              "targeting_automation": {
                "advantage_audience": 1
              }
            }
          }
        }
        ```
        
        **Advantage+ engagement:**
        
        ```json
        {
          "ad_account_id": "123456789",
          "body": {
            "name": "US Broad - Page Engagement",
            "campaign_id": "<campaign_id>",
            "optimization_goal": "POST_ENGAGEMENT",
            "billing_event": "IMPRESSIONS",
            "targeting": {
              "geo_locations": {
                "countries": [
                  "US"
                ]
              },
              "targeting_automation": {
                "advantage_audience": 1
              }
            }
          }
        }
        ```
        
        → Capture `adset_id`.
        
        ### 3. Upload image
        
        ```python
        meta_ads_ad_images_upload(account_id="act_123456789", image_url="<url>")
        ```
        
        → Capture `image_hash`.
        
        ### 4. Create ad
        
        > **CRITICAL**: `meta_ads_ad_create` takes `ad_account_id` plus a single `body` dict. No separate top-level args.
        
        ```json
        {
          "ad_account_id": "123456789",
          "body": {
            "name": "Awareness Ad - [Creative]",
            "adset_id": "<adset_id>",
            "creative": {
              "object_story_spec": {
                "page_id": "<page_id>",
                "link_data": {
                  "link": "https://example.com",
                  "image_hash": "<image_hash>",
                  "call_to_action": {
                    "type": "LEARN_MORE"
                  },
                  "message": "<primary_text>",
                  "name": "<headline>"
                }
              }
            }
          }
        }
        ```
        
        > Omit `status` — ads default to PAUSED.
        
        → Capture `ad_id`.
        
        ### 5. Preview and activate
        
        ```python
        # creative_id is in the ad creation response: response.creative.id
        meta_ads_ad_previews_get(creative_ids=["<creative_id>"])
        meta_ads_campaigns_activate(campaign_id="<campaign_id>")  # when user approves
        ```
        
        ---
        
        ## Manual path (only when user explicitly requests)
        
        ```json
        {
          "ad_account_id": "123456789",
          "body": {
            "name": "AU 18-35 Awareness",
            "campaign_id": "<campaign_id>",
            "optimization_goal": "REACH",
            "billing_event": "IMPRESSIONS",
            "daily_budget": 2000,
            "targeting": {
              "geo_locations": {
                "countries": [
                  "AU"
                ]
              },
              "age_min": 18,
              "age_max": 35
            }
          }
        }
        ```
        
        Budget at **ad set level** for manual. No `targeting_automation`.
        
      • leads.md 14.2 KB
        # Leads Campaign Workflow (OUTCOME_LEADS)
        
        ## When to use
        
        Use for campaigns optimized for lead generation: native Meta lead forms, website form submissions, or any event where a user provides contact information.
        
        ## Before starting
        
        Re-read [../constraints.md](../constraints.md). Every rule there applies here. Most relevant:
        - Budget in cents (×100)
        - `promoted_object` required for OUTCOME_LEADS — missing it, or missing any required field within it, causes a validation error
        - `targeting_automation` inside `targeting`
        - Budget at campaign level for Advantage+
        - `meta_ads_adset_create` takes `ad_account_id` (numeric, no `act_` prefix) + a `body` dict — all ad set fields inside `body`
        - `meta_ads_ad_create` takes `ad_account_id` + a `body` dict — no separate top-level args
        - Use `meta_ads_campaigns_activate()`, not `update_campaign(status="ACTIVE")`
        - **"Bid amount required" errors MUST be surfaced to the user — do NOT change optimization_goal as a workaround**
        
        ---
        
        ## Required inputs
        
        | Input | How to get it |
        |---|---|
        | Ad account ID | Discovery step 1 |
        | Facebook Page ID | Discovery step 2 — see fallback note below |
        | Lead capture method | Ask: native lead form OR website pixel? |
        | Pixel ID (if website leads) | `meta_ads_ad_pixels_list` |
        | Landing page URL (if website leads) | Ask the user |
        | Budget amount + currency | Ask the user |
        | Daily or lifetime | Ask; lifetime needs start + end dates |
        | Ad creative (image_hash or creative_id) | Upload via `meta_ads_ad_images_upload`, or list existing via `meta_ads_creative_list` |
        
        **Never proceed to campaign creation without all required inputs confirmed.**
        
        ---
        
        ## Decision point: lead form vs website pixel
        
        ```
        User wants leads
          ├── Native Meta Lead Form
          │     Users fill form directly on Facebook/Instagram
          │     promoted_object: {"page_id": "<page_id>"}
          │
          └── Website pixel (landing page)
                Users click through to your website and submit a form
                promoted_object: {
                  "pixel_id": "<pixel_id>",
                  "custom_event_type": "LEAD",
                  "page_id": "<page_id>"       ← required on BOTH paths
                }
        ```
        
        If the user hasn't specified, ask:
        > "Do you want users to fill out a form directly within Facebook/Instagram, or drive them to a landing page on your website?"
        
        ---
        
        ## Step-by-step creation (default)
        
        ### Pre-build checklist
        
        - [ ] Lead capture method confirmed (form or website)
        - [ ] Budget confirmed and converted to cents
        - [ ] Page ID captured — see note below if `meta_ads_owned_pages_list` fails
        - [ ] Pixel ID captured (if website leads)
        - [ ] Destination URL confirmed (if website leads)
        - [ ] Creative ready: image_hash from upload, or existing creative_id confirmed with correct destination URL
        - [ ] Health check run: `meta_ads_health_check` — **required before first campaign on any account**
        
        ### 1. Run health check
        
        ```python
        meta_ads_health_check(account_id="act_123456789")
        ```
        
        Surface any non-PASS items before continuing. A failing pixel health check means `LEAD_GENERATION` optimization may not be available for this account.
        
        ### 2. Create campaign
        
        ```python
        meta_ads_campaign_create(
            ad_account_id="123456789",
            body={
                "name": "Leads - [Business] - [Date]",
                "objective": "OUTCOME_LEADS",
                "status": "PAUSED",
                "daily_budget": 2000    # $20/day in cents — Advantage+ only; omit for manual
            }
        )
        ```
        
        → Capture `campaign_id` from the response.
        
        ### 3. Look up pixel ID (website leads only)
        
        ```python
        meta_ads_ad_pixels_list(account_id="act_123456789")
        ```
        
        → Capture `pixel_id`. Required for `promoted_object`.
        
        Skip if using the native lead form path — `promoted_object` uses only `page_id` there.
        
        ### 4. Create ad set
        
        > `meta_ads_adset_create` takes `ad_account_id` (numeric, no `act_` prefix) plus a `body` dict. Every ad set field goes inside `body`.
        
        > **Bid strategy warning**: If the account has a non-default bid strategy (e.g. LOWEST_COST_WITH_BID_CAP), ad set creation will fail with "Bid Amount Required." If you see this error, **stop immediately and surface it to the user** — do NOT add `bid_amount` or change `optimization_goal` as workarounds. See constraints.md section 14.
        
        **Advantage+ — website pixel path:**
        
        ```json
        {
          "ad_account_id": "123456789",
          "body": {
            "name": "US Broad - Demo Requests",
            "campaign_id": "<campaign_id>",
            "optimization_goal": "LEAD_GENERATION",
            "billing_event": "IMPRESSIONS",
            "targeting": {
              "geo_locations": {
                "countries": [
                  "US"
                ]
              },
              "targeting_automation": {
                "advantage_audience": 1
              }
            },
            "promoted_object": {
              "pixel_id": "<pixel_id>",
              "custom_event_type": "LEAD",
              "page_id": "<page_id>"
            }
          }
        }
        ```
        
        > **CRITICAL**: `promoted_object` requires `pixel_id`, `custom_event_type`, **and** `page_id` for the website pixel path. The local validator will reject the call if any of these three fields is missing.
        
        **Advantage+ — native lead form path:**
        
        ```json
        {
          "ad_account_id": "123456789",
          "body": {
            "name": "US Broad - Lead Form",
            "campaign_id": "<campaign_id>",
            "optimization_goal": "LEAD_GENERATION",
            "billing_event": "IMPRESSIONS",
            "targeting": {
              "geo_locations": {
                "countries": [
                  "US"
                ]
              },
              "targeting_automation": {
                "advantage_audience": 1
              }
            },
            "promoted_object": {
              "page_id": "<page_id>"
            }
          }
        }
        ```
        
        > **CRITICAL**: `targeting_automation` is inside `targeting`, not at the ad set top level.
        
        > **CRITICAL**: No `daily_budget` on the ad set for Advantage+ — campaign controls budget.
        
        **Manual (only when user explicitly requests):**
        
        ```json
        {
          "ad_account_id": "123456789",
          "body": {
            "name": "UK Decision Makers 30-55",
            "campaign_id": "<campaign_id>",
            "optimization_goal": "LEAD_GENERATION",
            "billing_event": "IMPRESSIONS",
            "daily_budget": 3000,
            "targeting": {
              "geo_locations": {
                "countries": [
                  "GB"
                ]
              },
              "age_min": 30,
              "age_max": 55
            },
            "promoted_object": {
              "pixel_id": "<pixel_id>",
              "custom_event_type": "LEAD",
              "page_id": "<page_id>"
            }
          }
        }
        ```
        
        → Capture `adset_id` from the response.
        
        #### If LEAD_GENERATION optimization goal is rejected (error 2490408)
        
        This error means the account's bid strategy (`LOWEST_COST_WITH_BID_CAP`) is incompatible with `LEAD_GENERATION`. **Adding `bid_amount` will not fix it** — the optimization goal is simply unavailable under this bid strategy, regardless of what values you pass.
        
        1. **Stop. Do not substitute `LINK_CLICKS` or change the objective to `OUTCOME_TRAFFIC`.** That silently creates a traffic campaign, not a leads campaign.
        2. Surface the constraint to the user with the available resolution paths:
        
           > "Meta's `LEAD_GENERATION` optimization goal isn't available on this account due to its current bid strategy (`LOWEST_COST_WITH_BID_CAP`). Here are your options:
           > - **Recommended**: Change the account's bid strategy to 'Lowest Cost' (no cap) in Meta Business Manager, then I'll retry.
           > - **Workaround**: Run as a traffic campaign with link click optimization — this will drive clicks to your landing page but won't optimize for lead events.
           > - **Alternative**: Use a native Meta Lead Form instead of the website landing page, which may work under this bid strategy.
           > Which would you like to do?"
        
        3. Do not proceed until the user has chosen a path.
        
        ### 5. (Native lead form path only) List lead forms
        
        If using a native lead form, you need a `lead_gen_form_id` from the page:
        
        ```python
        meta_ads_lead_forms_list(page_id="<page_id>")
        ```
        
        > **Warning**: This tool requires a **Page Access Token** and will fail with error 190 ("This method must be called with a Page Access Token") when using a User Access Token. If this happens:
        > - Ask the user to supply the form ID manually from their Facebook Page → Publishing Tools → Forms Library
        > - Or proceed without a form ID for testing purposes (the ad will link to the Page, not a form — it will not collect leads until a form is attached)
        > - Do not proceed silently without informing the user of this limitation
        
        ### 5. Upload image or prepare creative
        
        ```python
        meta_ads_ad_images_upload(
            account_id="act_123456789",
            image_url="<public_image_url>"    # or file_id if from Hyper file storage
        )
        ```
        
        → Capture `image_hash`.
        
        #### Creating a creative from scratch with `meta_ads_creative_create`
        
        Use this when you need full control over the creative spec (e.g., native lead form ads). `name` is a **required** positional argument — it will fail silently if omitted.
        
        ```python
        meta_ads_creative_create(
            ad_account_id="123456789",
            body={
                "name": "Creative Name",        # REQUIRED — not optional
                "object_story_spec": {
                    "page_id": "<page_id>", # REQUIRED at top level — never inside link_data
                    "link_data": {
                        "message": "<primary ad copy>",
                        "link": "<destination_url>",
                        "description": "<secondary copy or headline>",
                        "picture": "<image_url>",
                        "call_to_action": {"type": "SIGN_UP"}
                    }
                }
            }
        )
        ```
        
        **Valid `link_data` fields:**
        
        | Field | Purpose | Notes |
        |---|---|---|
        | `message` | Primary ad copy | Required |
        | `link` | Destination URL | Required |
        | `description` | Secondary copy / headline text | Use this for headline-style text |
        | `picture` | Image URL | Use image_hash instead if available |
        | `call_to_action` | CTA button | `{"type": "SIGN_UP"}` or similar |
        | `caption` | Display URL shown under ad | **Must be a URL** (e.g. `"hyperfx.ai"`), NOT copy text |
        
        **Fields that do NOT work in `link_data`:**
        - `headline` — not a valid field; causes "unsupported field" API error. Use `description` instead.
        - `page_id` — must be at the **top level** of `object_story_spec`, not inside `link_data`.
        
        → Capture `creative_id` from the response.
        
        If the user has provided or selected an **existing creative** (`creative_id`), skip the upload — but verify the destination URL first.
        
        `meta_ads_creative_list` returns `link_url: null` in its list response, so you cannot verify the destination from the list alone. Call `meta_ads_creative_get(creative_id)` on the selected creative to retrieve the actual `link_url` before attaching it:
        
        ```python
        meta_ads_creative_get(creative_id="33892633203717215")
        # Check the returned link_url matches the user's intended landing page
        ```
        
        If the destination doesn't match, tell the user and either select a different creative or create a new inline one.
        
        **Selecting among multiple existing creatives:** When `meta_ads_creative_list` returns several options, choose based on:
        1. Body copy alignment with campaign goal (lead-focused language > viral/social tone)
        2. Headline relevance to the offer or CTA
        3. Destination URL (verify via `meta_ads_creative_get` — the list shows `null`)
        
        If you're unsure, show the user the top 2–3 options (name + body copy) and let them choose.
        
        ### 6. Create ad
        
        > **CRITICAL**: `meta_ads_ad_create` takes `ad_account_id` plus a single `body` dict. No separate top-level args.
        
        **With new inline creative (image_hash):**
        
        ```json
        {
          "ad_account_id": "123456789",
          "body": {
            "name": "Lead Ad - [Creative Name]",
            "adset_id": "<adset_id>",
            "creative": {
              "object_story_spec": {
                "page_id": "<page_id>",
                "link_data": {
                  "link": "https://example.com/contact",
                  "image_hash": "<image_hash>",
                  "call_to_action": {
                    "type": "LEARN_MORE"
                  },
                  "message": "<primary_text>",
                  "name": "<headline>",
                  "description": "<description>"
                }
              }
            }
          }
        }
        ```
        
        **With existing creative (creative_id):**
        
        ```json
        {
          "ad_account_id": "123456789",
          "body": {
            "name": "Lead Ad - [Creative Name]",
            "adset_id": "<adset_id>",
            "creative": {
              "creative_id": "<creative_id>"
            }
          }
        }
        ```
        
        > Omit `status` — ads default to PAUSED. Never pass `status="ACTIVE"` on creation.
        
        → Capture `ad_id` from the response.
        
        ### 7. Preview and activate
        
        ```python
        # creative_id comes from the ad creation response (response.creative.id)
        # or from the creative you created separately via create_ad_creative
        # ad_id is NOT a valid parameter — creative_ids (list) is always required
        meta_ads_ad_previews_get(creative_ids=["<creative_id>"])
        ```
        
        Summarize preview formats to user — never render iframe inline.
        Wait for explicit user approval before activating.
        
        ```python
        meta_ads_campaigns_activate(campaign_id="<campaign_id>")
        # Activates campaign + ad sets + ads together
        # NOT: meta_ads_campaign_update(status="ACTIVE") — that leaves ad sets PAUSED
        ```
        
        ---
        
        ## Page ID fallback
        
        `meta_ads_owned_pages_list` can fail with "No business ID found for this ad account" when the account is not connected to a Business Manager with a recognized business ID.
        
        **If this happens**, call `meta_accounts_list` as a fallback:
        
        ```python
        meta_accounts_list(account_id="act_123456789")
        ```
        
        This returns connected pages and accounts through an alternate lookup path. Capture `page_id` from the response.
        
        ---
        
        ## Common failure points
        
        | Symptom | Cause | Fix |
        |---|---|---|
        | Local validation error: "page_id required in promoted_object" | Missing page_id on website pixel path | Add `page_id` alongside `pixel_id` and `custom_event_type` |
        | "No business ID found" from `list_owned_pages` | Account not linked to a Business Manager | Use `meta_accounts_list` as fallback |
        | "Bid amount required" | Account-level bid strategy requires explicit bid | **Stop. Surface to user. Do NOT add bid_amount or change optimization_goal.** |
        | "Performance goal isn't available" (subcode 2490408) | LEAD_GENERATION rejected by account | Run health check, stop, ask user. Do NOT substitute LINK_CLICKS. |
        | Cryptic API error on ad set | Missing `promoted_object` entirely | Add promoted_object with all required fields |
        | Budget at wrong level | Set budget on ad set for Advantage+ | Move to campaign level; remove from ad set |
        | Nothing serves after activation | Used `update_campaign(status="ACTIVE")` | Use `meta_ads_campaigns_activate()` |
        | Creative destination URL wrong | Existing creative linked to different URL | Verify creative destination before attaching |
        
        ---
        
        ## Full worked example
        
        See [../examples/leads-form.md](../examples/leads-form.md).
        
      • sales.md 9.7 KB
        # Sales Campaign Workflow (OUTCOME_SALES)
        
        ## When to use
        
        Use for campaigns optimized for website purchases, checkout completions, or any offsite conversion event tracked via Meta Pixel.
        
        ## Before starting
        
        Re-read [../constraints.md](../constraints.md). Every rule there applies here. Most relevant:
        - Budget in cents (×100)
        - `promoted_object` required for OUTCOME_SALES
        - `targeting_automation` inside `targeting`
        - Budget at campaign level for Advantage+
        - `meta_ads_adset_create` takes `ad_account_id` (numeric, no `act_` prefix) + a `body` dict — all ad set fields inside `body`
        - `meta_ads_ad_create` takes `ad_account_id` + a `body` dict — no separate top-level args
        - Use `meta_ads_campaigns_activate()`, not `update_campaign(status="ACTIVE")`
        
        ---
        
        ## Required inputs
        
        | Input | How to get it |
        |---|---|
        | Ad account ID | Discovery step 1 |
        | Facebook Page ID | Discovery step 2 (`meta_ads_owned_pages_list`) |
        | Meta Pixel ID | `meta_ads_ad_pixels_list` — step 2 of creation below |
        | Conversion event type | Ask user — default is `PURCHASE` |
        | Budget amount + currency | Ask the user |
        | Daily or lifetime | Ask; lifetime needs start + end dates |
        | Destination URL | Ask the user |
        | Ad creative (image_hash) | Upload via `meta_ads_ad_images_upload` |
        
        ---
        
        ## Decision point: conversion type
        
        ```
        User wants sales
          ├── Website purchases (most common)
          │     optimization_goal: OFFSITE_CONVERSIONS
          │     promoted_object: {pixel_id, custom_event_type: "PURCHASE"}
          ├── Custom conversion event (add to cart, checkout, etc.)
          │     optimization_goal: OFFSITE_CONVERSIONS
          │     promoted_object: {pixel_id, custom_event_type: "<EVENT>"}
          └── Catalog / dynamic product ads
                promoted_object: {pixel_id, product_catalog_id}  ← confirm with user first
        ```
        
        Default to PURCHASE unless the user specifies otherwise.
        
        ---
        
        ## Step-by-step creation (default)
        
        Use this for most campaigns. Each step is visible, failures are easy to diagnose and retry.
        
        ### Pre-build checklist
        
        - [ ] Budget confirmed and converted to cents
        - [ ] Page ID captured explicitly from discovery
        - [ ] Destination URL confirmed
        - [ ] Creative assets ready or will generate from site
        
        ### 1. Create campaign
        
        ```python
        meta_ads_campaign_create(
            ad_account_id="123456789",
            body={
                "name": "Sales - [Business] - [Date]",
                "objective": "OUTCOME_SALES",
                "status": "PAUSED",
                "daily_budget": 2000    # $20/day in cents — Advantage+ only; omit for manual
            }
        )
        ```
        
        → Capture `campaign_id` from the response.
        
        ### 2. Look up pixel ID
        
        ```python
        meta_ads_ad_pixels_list(account_id="act_123456789")
        ```
        
        → Capture `pixel_id`. Required for `promoted_object`.
        
        ### 3. Create ad set
        
        > `meta_ads_adset_create` takes `ad_account_id` (numeric, no `act_` prefix) plus a `body` dict. Every ad set field goes inside `body`.
        
        **Advantage+ (default):**
        
        ```json
        {
          "ad_account_id": "123456789",
          "body": {
            "name": "US Broad - Purchases",
            "campaign_id": "<campaign_id>",
            "optimization_goal": "OFFSITE_CONVERSIONS",
            "billing_event": "IMPRESSIONS",
            "targeting": {
              "geo_locations": {
                "countries": [
                  "US"
                ]
              },
              "targeting_automation": {
                "advantage_audience": 1
              }
            },
            "promoted_object": {
              "pixel_id": "<pixel_id>",
              "custom_event_type": "PURCHASE"
            }
          }
        }
        ```
        
        > **CRITICAL**: `promoted_object` is required for OUTCOME_SALES. Missing it causes a cryptic API error.
        
        > **CRITICAL**: `targeting_automation` is inside `targeting`, not at the ad set top level.
        
        > **CRITICAL**: No `daily_budget` on the ad set for Advantage+ — campaign controls budget.
        
        **Manual (only when user explicitly requests):**
        
        ```json
        {
          "ad_account_id": "123456789",
          "body": {
            "name": "US Women 25-44",
            "campaign_id": "<campaign_id>",
            "optimization_goal": "OFFSITE_CONVERSIONS",
            "billing_event": "IMPRESSIONS",
            "daily_budget": 2000,
            "targeting": {
              "geo_locations": {
                "countries": [
                  "US"
                ]
              },
              "age_min": 25,
              "age_max": 44,
              "genders": [
                2
              ]
            },
            "promoted_object": {
              "pixel_id": "<pixel_id>",
              "custom_event_type": "PURCHASE"
            }
          }
        }
        ```
        
        → Capture `adset_id` from the response.
        
        ### 4. Upload image
        
        ```python
        meta_ads_ad_images_upload(
            account_id="act_123456789",
            image_url="<public_image_url>"    # or file_id if from Hyper file storage
        )
        ```
        
        → Capture `image_hash` from the response.
        
        ### 5. Create ad
        
        > **CRITICAL**: `meta_ads_ad_create` takes `ad_account_id` plus a single `body` dict containing all ad fields. Do not pass `name` or `adset_id` as separate top-level arguments and do not pass `body` as a JSON string.
        
        ```
        ❌ WRONG: meta_ads_ad_create(account_id="act_...", adset_id="123", name="My Ad")
        ❌ WRONG: meta_ads_ad_create(ad_account_id="...", body='{"adset_id": "123"}')  # string not dict
        ✅ RIGHT:  meta_ads_ad_create(ad_account_id="111222333", body={"adset_id": "123", ...})
        ```
        
        ```json
        {
          "ad_account_id": "123456789",
          "body": {
            "name": "Sales Ad - [Creative Name]",
            "adset_id": "<adset_id>",
            "creative": {
              "object_story_spec": {
                "page_id": "<page_id>",
                "link_data": {
                  "link": "https://example.com/shop",
                  "image_hash": "<image_hash>",
                  "call_to_action": {
                    "type": "SHOP_NOW"
                  },
                  "message": "<primary_text>",
                  "name": "<headline>",
                  "description": "<description>"
                }
              }
            }
          }
        }
        ```
        
        > Omit `status` — ads default to PAUSED. Never pass `status="ACTIVE"` on creation.
        
        → Capture `ad_id` from the response.
        
        ### 6. Preview and activate
        
        ```python
        # creative_id is in the ad creation response: response.creative.id
        meta_ads_ad_previews_get(creative_ids=["<creative_id>"])
        # Summarize formats to user — never render iframe inline
        # Wait for explicit user approval
        
        meta_ads_campaigns_activate(campaign_id="<campaign_id>")
        # Activates campaign + ad sets + ads together
        # NOT: meta_ads_campaign_update(status="ACTIVE") — that leaves ad sets PAUSED
        ```
        
        ---
        
        ## Other creative formats (step 5 variations)
        
        The single-image `object_story_spec` above is the most common. `meta_ads_ad_create` also supports carousel and dynamic creative natively — just change the `creative` block.
        
        **Carousel** (2–10 cards) — use `child_attachments` inside `link_data`:
        
        ```json
        {
          "ad_account_id": "123456789",
          "body": {
            "name": "Sales Carousel Ad",
            "adset_id": "<adset_id>",
            "creative": {
              "object_story_spec": {
                "page_id": "<page_id>",
                "link_data": {
                  "link": "https://example.com/shop",
                  "message": "Browse our collection",
                  "child_attachments": [
                    {
                      "link": "https://example.com/p1",
                      "name": "Product 1",
                      "description": "Desc 1",
                      "image_hash": "<hash1>"
                    },
                    {
                      "link": "https://example.com/p2",
                      "name": "Product 2",
                      "description": "Desc 2",
                      "image_hash": "<hash2>"
                    }
                  ],
                  "multi_share_optimized": true
                }
              }
            }
          }
        }
        ```
        
        **Dynamic creative** (Meta auto-optimizes combinations) — pair `asset_feed_spec` with a minimal `object_story_spec` that carries the `page_id`:
        
        ```json
        {
          "ad_account_id": "123456789",
          "body": {
            "name": "Sales Dynamic Ad",
            "adset_id": "<adset_id>",
            "creative": {
              "object_story_spec": {
                "page_id": "<page_id>"
              },
              "asset_feed_spec": {
                "images": [
                  {
                    "hash": "<hash1>"
                  },
                  {
                    "hash": "<hash2>"
                  }
                ],
                "titles": [
                  {
                    "text": "Headline A"
                  },
                  {
                    "text": "Headline B"
                  }
                ],
                "bodies": [
                  {
                    "text": "Copy option 1"
                  },
                  {
                    "text": "Copy option 2"
                  }
                ],
                "descriptions": [
                  {
                    "text": "Free shipping over $40"
                  }
                ],
                "link_urls": [
                  {
                    "website_url": "https://example.com/shop"
                  }
                ],
                "call_to_action_types": [
                  "SHOP_NOW"
                ],
                "ad_formats": [
                  "SINGLE_IMAGE"
                ]
              }
            }
          }
        }
        ```
        
        > Dynamic creative requirements:
        > - `ad_formats` is **required** — use `["SINGLE_IMAGE"]` (or `["SINGLE_VIDEO"]` for video). Omitting it makes Meta default to SINGLE_IMAGE and reject a feed that carries video.
        > - Keep a minimal `object_story_spec` with just `page_id` alongside `asset_feed_spec` (the page_id comes from there).
        > - The ad set must be created with `is_dynamic_creative: true` — pass it to `meta_ads_adset_create`.
        >
        > For non-dynamic ads, `creative` uses a single `object_story_spec` (or `creative_id` to reuse an existing creative).
        
        ---
        
        ## Common failure points
        
        | Symptom | Cause | Fix |
        |---|---|---|
        | Cryptic API error on ad set creation | Missing `promoted_object` | Add `pixel_id` + `custom_event_type` to ad set |
        | Budget rejected | Passed dollars not cents | Multiply by 100 |
        | Ad set error: unexpected argument | Fields passed outside `body` | All fields must be inside the `body` dict |
        | Ad creation error | Separate top-level args used | Use `ad_account_id` + `body` |
        | Campaign ACTIVE but nothing serves | Used `update_campaign(status="ACTIVE")` | Use `meta_ads_campaigns_activate()` |
        | "Bid amount required" | Optimization goal requires explicit bid | Do NOT change goal — ask user |
        | `targeting_automation` error | Placed at ad set top level | Move inside `targeting` object |
        
        ---
        
        ## Full worked example
        
        See [../examples/sales-ecommerce.md](../examples/sales-ecommerce.md).
        
      • traffic.md 5.3 KB
        # Traffic Campaign Workflow (OUTCOME_TRAFFIC)
        
        ## When to use
        
        Use for campaigns that send users to a website, landing page, or specific content destination. No conversion tracking required. Good for top-of-funnel content promotion, event pages, or when a pixel is not yet installed.
        
        ## Before starting
        
        Re-read [../constraints.md](../constraints.md). Most relevant:
        - Budget in cents (×100)
        - `targeting_automation` inside `targeting`
        - Budget at campaign level for Advantage+
        - `meta_ads_adset_create` takes `ad_account_id` (numeric, no `act_` prefix) + a `body` dict — all ad set fields inside `body`
        - `meta_ads_ad_create` takes `ad_account_id` + a `body` dict — no separate top-level args
        - Use `meta_ads_campaigns_activate()`, not `update_campaign(status="ACTIVE")`
        
        `promoted_object` is **not required** for OUTCOME_TRAFFIC. No pixel needed.
        
        ---
        
        ## Required inputs
        
        | Input | How to get it |
        |---|---|
        | Ad account ID | Discovery step 1 |
        | Facebook Page ID | Discovery step 2 (`meta_ads_owned_pages_list`) |
        | Destination URL | Ask the user |
        | Budget amount + currency | Ask the user |
        | Daily or lifetime | Ask; lifetime needs start + end dates |
        | CTA type | Choose from table below |
        | Ad creative (image_hash) | Upload via `meta_ads_ad_images_upload` |
        
        ---
        
        ## CTA selection
        
        | Destination | Recommended CTA |
        |---|---|
        | Blog post / article | `LEARN_MORE` |
        | Product or collection page | `SHOP_NOW` |
        | Event or sign-up page | `SIGN_UP` |
        | Resource / download | `DOWNLOAD` |
        | Booking / appointment page | `BOOK_NOW` |
        | Promotional offer | `GET_OFFER` |
        | Contact page | `CONTACT_US` |
        
        ---
        
        ## Step-by-step creation (default)
        
        ### Pre-build checklist
        
        - [ ] Budget confirmed and converted to cents
        - [ ] Page ID captured explicitly from discovery
        - [ ] Destination URL confirmed
        - [ ] CTA selected
        - [ ] Creative assets ready or will generate from site
        
        ### 1. Create campaign
        
        ```python
        meta_ads_campaign_create(
            ad_account_id="123456789",
            body={
                "name": "Traffic - [Business] - [Date]",
                "objective": "OUTCOME_TRAFFIC",
                "status": "PAUSED",
                "daily_budget": 2000    # $20/day in cents — Advantage+ only; omit for manual
            }
        )
        ```
        
        → Capture `campaign_id` from the response.
        
        ### 2. Create ad set
        
        > `meta_ads_adset_create` takes `ad_account_id` (numeric, no `act_` prefix) plus a `body` dict. Every ad set field goes inside `body`.
        
        **Advantage+ (default):**
        
        ```json
        {
          "ad_account_id": "123456789",
          "body": {
            "name": "US Broad - Website Traffic",
            "campaign_id": "<campaign_id>",
            "optimization_goal": "LINK_CLICKS",
            "billing_event": "IMPRESSIONS",
            "targeting": {
              "geo_locations": {
                "countries": [
                  "US"
                ]
              },
              "targeting_automation": {
                "advantage_audience": 1
              }
            }
          }
        }
        ```
        
        > No `promoted_object` needed for OUTCOME_TRAFFIC.
        
        **Manual (only when user explicitly requests):**
        
        ```json
        {
          "ad_account_id": "123456789",
          "body": {
            "name": "US Adults 25-44",
            "campaign_id": "<campaign_id>",
            "optimization_goal": "LINK_CLICKS",
            "billing_event": "IMPRESSIONS",
            "daily_budget": 2000,
            "targeting": {
              "geo_locations": {
                "countries": [
                  "US"
                ]
              },
              "age_min": 25,
              "age_max": 44
            }
          }
        }
        ```
        
        > **Bid cap requested?** Pass `bid_strategy` and `bid_amount` (cents) inside `body` — the toolkit validates the pairing up front. See [../constraints.md](../constraints.md) section 16.
        
        > **Targeting regions / cities / DMAs (not whole countries)?** Resolve the numeric geo keys with `meta_ads_targeting_search` first — never hand-write them. See [../constraints.md](../constraints.md) section 17.
        
        → Capture `adset_id` from the response.
        
        ### 3. Upload image
        
        ```python
        meta_ads_ad_images_upload(
            account_id="act_123456789",
            image_url="<public_image_url>"
        )
        ```
        
        → Capture `image_hash` from the response.
        
        ### 4. Create ad
        
        > **CRITICAL**: `meta_ads_ad_create` takes `ad_account_id` plus a single `body` dict. No separate top-level args.
        
        ```json
        {
          "ad_account_id": "123456789",
          "body": {
            "name": "Traffic Ad - [Page Name]",
            "adset_id": "<adset_id>",
            "creative": {
              "object_story_spec": {
                "page_id": "<page_id>",
                "link_data": {
                  "link": "https://example.com/page",
                  "image_hash": "<image_hash>",
                  "call_to_action": {
                    "type": "LEARN_MORE"
                  },
                  "message": "<primary_text>",
                  "name": "<headline>",
                  "description": "<description>"
                }
              }
            }
          }
        }
        ```
        
        > Omit `status` — ads default to PAUSED.
        
        → Capture `ad_id` from the response.
        
        ### 5. Preview and activate
        
        ```python
        # creative_id is in the ad creation response: response.creative.id
        meta_ads_ad_previews_get(creative_ids=["<creative_id>"])
        # Summarize formats to user — never render iframe inline
        
        meta_ads_campaigns_activate(campaign_id="<campaign_id>")
        # Only when user explicitly approves
        ```
        
        ---
        
        ## Common failure points
        
        | Symptom | Cause | Fix |
        |---|---|---|
        | Budget rejected | Passed dollars not cents | Multiply by 100 |
        | Ad set error: unexpected argument | Fields outside `body` | All fields must be inside the `body` dict |
        | Campaign ACTIVE but no traffic | Used `update_campaign(status="ACTIVE")` | Use `meta_ads_campaigns_activate()` |
        | `targeting_automation` error | Placed at top level of ad set | Move inside `targeting` object |
        
    • examples
      • leads-form.md 6.4 KB
        # Example: Leads Campaign — Website Landing Page (B2B SaaS, Step-by-Step)
        
        This example shows the full sequence: user request → clarification → written plan → step-by-step execution.
        
        ---
        
        ## User request
        
        > "I want to run a Facebook lead gen campaign for my B2B SaaS. Budget is $50/day."
        
        ---
        
        ## Step 1: Clarification questions
        
        Before touching any tools, ask:
        
        ```
        Before I build this, a few questions:
        
        1. Do you want users to fill out a form directly within Facebook/Instagram (native lead form),
           or drive them to a landing page on your website?
        2. What's your website URL / landing page?
        3. Do you have a Meta Pixel installed that's firing Lead events?
        4. What geography should I target? (Default: United States)
        5. How long should this run — ongoing daily budget, or a fixed period?
        ```
        
        ---
        
        ## Step 2: User answers
        
        > "Landing page. URL is example.com/demo. Yes, pixel is installed tracking lead events. US only. Ongoing."
        
        ---
        
        ## Step 3: Written plan (shown before any tool calls)
        
        State the plan explicitly before acting.
        
        ```
        Here's my plan:
        
        OBJECTIVE: OUTCOME_LEADS (website pixel path)
        CAMPAIGN TYPE: Advantage+ (default)
        BUDGET: $50/day = 5000 cents at campaign level
        OPTIMIZATION GOAL: LEAD_GENERATION
        PROMOTED OBJECT: pixel_id + custom_event_type: LEAD + page_id (all three required on website pixel path)
        TARGETING: Broad US, Advantage+ audience automation
        STATUS: PAUSED until you approve
        
        Steps:
        1. Select ad account (meta_ads_adaccount_list)
        2. Get Page ID (meta_ads_owned_pages_list)
        3. Run health check (meta_ads_health_check)
        4. Screenshot example.com/demo for creative reference (firecrawl_screenshots_create)
        5. Extract branding (firecrawl_branding_extract)
        6. Generate ad creative from screenshot
        7. Upload image → get image_hash (meta_ads_ad_images_upload)
        8. Create campaign → campaign_id (meta_ads_campaign_create)
        9. Get Pixel ID → pixel_id (meta_ads_ad_pixels_list)
        10. Create ad set → adset_id (meta_ads_adset_create)
        11. Create ad → ad_id (meta_ads_ad_create)
        12. Show ad previews — wait for your review
        13. Activate only when you say to
        
        Key constraints:
        - $50/day = 5000 cents (not 50)
        - promoted_object REQUIRED: pixel_id + LEAD event + page_id (all three required on website pixel path)
        - targeting_automation inside targeting object
        - Budget at campaign level (ad set has no budget)
        - create_ad_set takes ad_account_id + body
        - create_ad takes ad_account_id + body
        - Will use meta_ads_campaigns_activate(), not update(status="ACTIVE")
        ```
        
        ---
        
        ## Step 4: Execution
        
        ### Discovery
        
        ```python
        meta_ads_adaccount_list({"detail": "id_only"})
        # → account: act_111222333
        
        meta_ads_owned_pages_list({"account_id": "act_111222333", "detail": "id_only"})
        # → page_id: 632278516639981  ← captured explicitly
        
        meta_ads_health_check({"account_id": "act_111222333"})
        # → All checks PASS
        ```
        
        ### Research
        
        ```python
        firecrawl_screenshots_create({"url": "https://example.com/demo"})
        # → Screenshot: dark navy, "Book a Demo" hero CTA, B2B product screenshots
        
        firecrawl_branding_extract({"url": "https://example.com"})
        # → Colors: #0D1B2A (navy), #00C2FF (electric blue)
        # → Font: Inter | Tone: professional, direct, B2B
        ```
        
        ### Creative
        
        ```python
        meta_ads_ad_images_upload({"account_id": "act_111222333", "image_url": "<generated_image_url>"})
        # → image_hash: xyz789abc012
        ```
        
        ### Campaign creation (step-by-step)
        
        ```python
        # Step 1: Campaign
        meta_ads_campaign_create(
            ad_account_id="111222333",
            body={
                "name": "Leads - ExampleSaaS - TEST - 2026-06",
                "objective": "OUTCOME_LEADS",
                "status": "PAUSED",
                "daily_budget": 5000    # $50/day in cents
            }
        )
        # → campaign_id: 987654321
        
        # Step 2: Pixel
        meta_ads_ad_pixels_list(account_id="act_111222333")
        # → pixel_id: 9876543210
        
        # Step 3: Ad set
        meta_ads_adset_create(
            ad_account_id="111222333",
            body={
                "name": "US Broad - Demo Requests - TEST",
                "campaign_id": "987654321",
                "optimization_goal": "LEAD_GENERATION",
                "billing_event": "IMPRESSIONS",
                "targeting": {
                    "geo_locations": {"countries": ["US"]},
                    "targeting_automation": {"advantage_audience": 1}
                },
                "promoted_object": {
                    "pixel_id": "9876543210",
                    "custom_event_type": "LEAD",
                    "page_id": "632278516639981"
                }
                # No daily_budget — campaign controls budget for Advantage+
            }
        )
        # → adset_id: 334455667
        
        # Step 4: Ad
        meta_ads_ad_create(
            ad_account_id="111222333",
            body={
                "name": "Demo Request - Image Ad - TEST",
                "adset_id": "334455667",
                "creative": {
                    "object_story_spec": {
                        "page_id": "632278516639981",
                        "link_data": {
                            "link": "https://example.com/demo",
                            "image_hash": "xyz789abc012",
                            "call_to_action": {"type": "LEARN_MORE"},
                            "message": "See how ExampleSaaS helps B2B teams close deals faster.",
                            "name": "Book a Free Demo",
                            "description": "No commitment. 30-minute walkthrough."
                        }
                    }
                }
                # No status field — ads default to PAUSED
            }
        )
        # → ad_id: 556677889
        ```
        
        ### Preview and summary
        
        ```python
        # creative_id from ad creation response: response.creative.id
        meta_ads_ad_previews_get(creative_ids=["<creative_id>"])
        # Summarize preview formats to user — never render iframe inline
        ```
        
        ```
        Campaign created and paused. Here's what was built:
        
        Campaign: "Leads - ExampleSaaS - TEST - 2026-06"
        Status: PAUSED
        Budget: $50/day
        Objective: Leads (LEAD pixel event from example.com/demo)
        Optimization: LEAD_GENERATION
        Targeting: United States, Advantage+ broad audience
        Ad previews: [available in the dashboard]
        
        When you're ready to go live, reply "activate".
        ```
        
        ### Activation (when user approves)
        
        ```python
        # RIGHT
        meta_ads_campaigns_activate(campaign_id="987654321")
        
        # WRONG — do not use
        # meta_ads_campaign_update(campaign_id="987654321", status="ACTIVE")
        ```
        
        ---
        
        ## What if no pixel is installed?
        
        If `meta_ads_ad_pixels_list` returns nothing:
        
        ```
        I couldn't find a Meta Pixel on your account. To track leads from your website,
        you'll need a pixel installed and firing Lead events on the form confirmation.
        
        Would you like to:
        a) Set up a pixel first (I can walk you through it), or
        b) Use a native Meta Lead Form instead (users fill the form within Facebook/Instagram)?
        ```
        
        Do not proceed with a website-pixel OUTCOME_LEADS campaign if no pixel exists.
        
      • sales-ecommerce.md 5.9 KB
        # Example: Sales Campaign — Ecommerce (Advantage+, Step-by-Step)
        
        This example shows the full sequence: user request → clarification → written plan → step-by-step execution.
        
        ---
        
        ## User request
        
        > "I want to run Facebook ads for my online skincare store. I have a $30/day budget and want to drive purchases."
        
        ---
        
        ## Step 1: Clarification questions
        
        Before touching any tools, ask:
        
        ```
        Before I build this, a few quick details:
        
        1. What's your website URL and main landing page for the ads?
        2. Do you have a Meta Pixel installed and is it firing Purchase events?
        3. What geography should I target? (Default: United States)
        4. How long should this run — ongoing daily budget, or a fixed period?
        5. Do you have existing product images or should I generate creatives from your website?
        ```
        
        ---
        
        ## Step 2: User answers
        
        > "URL is skinfirst.com. Yes, pixel is installed and tracking purchases. US only. Ongoing. Generate from site."
        
        ---
        
        ## Step 3: Written plan (shown before any tool calls)
        
        State the plan explicitly before acting. Do not start calling tools until this is visible.
        
        ```
        Here's my plan:
        
        OBJECTIVE: OUTCOME_SALES (website purchases via pixel)
        CAMPAIGN TYPE: Advantage+ (default)
        BUDGET: $30/day = 3000 cents at campaign level
        OPTIMIZATION GOAL: OFFSITE_CONVERSIONS
        PROMOTED OBJECT: pixel_id + custom_event_type: PURCHASE
        TARGETING: Broad US, Advantage+ audience automation
        STATUS: PAUSED until you approve
        
        Steps:
        1. Select ad account (meta_ads_adaccount_list)
        2. Get Page ID (meta_ads_owned_pages_list)
        3. Run health check (meta_ads_health_check)
        4. Screenshot skinfirst.com for creative reference (firecrawl_screenshots_create)
        5. Extract branding (firecrawl_branding_extract)
        6. Generate ad creative from screenshot
        7. Upload image → get image_hash (meta_ads_ad_images_upload)
        8. Create campaign → campaign_id (meta_ads_campaign_create)
        9. Get Pixel ID → pixel_id (meta_ads_ad_pixels_list)
        10. Create ad set → adset_id (meta_ads_adset_create)
        11. Create ad → ad_id (meta_ads_ad_create)
        12. Show ad previews — wait for your review
        13. Activate only when you say to
        
        Key constraints:
        - $30/day = 3000 cents (not 30)
        - promoted_object required: pixel_id + PURCHASE
        - targeting_automation inside targeting object
        - Budget at campaign level (ad set has no budget)
        - create_ad_set takes ad_account_id + body
        - create_ad takes ad_account_id + body
        - Will use meta_ads_campaigns_activate(), not update(status="ACTIVE")
        ```
        
        ---
        
        ## Step 4: Execution
        
        ### Discovery
        
        ```python
        meta_ads_adaccount_list({"detail": "id_only"})
        # → account: act_111222333
        
        meta_ads_owned_pages_list({"account_id": "act_111222333", "detail": "id_only"})
        # → page_id: 632278516639981  ← captured explicitly
        
        meta_ads_health_check({"account_id": "act_111222333"})
        # → All checks PASS
        ```
        
        ### Research
        
        ```python
        firecrawl_screenshots_create({"url": "https://skinfirst.com"})
        # → Screenshot: clean minimal layout, soft pastel palette, hero product shot
        
        firecrawl_branding_extract({"url": "https://skinfirst.com"})
        # → Colors: #F5EDE4 (cream), #9B7D6E (warm brown)
        # → Font: Playfair Display | Tone: elegant, natural
        ```
        
        ### Creative
        
        ```python
        # Generate ad image using screenshot as visual reference
        meta_ads_ad_images_upload({"account_id": "act_111222333", "image_url": "<generated_image_url>"})
        # → image_hash: abc123def456
        ```
        
        ### Campaign creation (step-by-step)
        
        ```python
        # Step 1: Campaign
        meta_ads_campaign_create(
            ad_account_id="111222333",
            body={
                "name": "Sales - SkinFirst - TEST - 2026-06",
                "objective": "OUTCOME_SALES",
                "status": "PAUSED",
                "daily_budget": 3000    # $30/day in cents
            }
        )
        # → campaign_id: 987654321
        
        # Step 2: Pixel
        meta_ads_ad_pixels_list(account_id="act_111222333")
        # → pixel_id: 9876543210
        
        # Step 3: Ad set
        meta_ads_adset_create(
            ad_account_id="111222333",
            body={
                "name": "US Broad - Purchases - TEST",
                "campaign_id": "987654321",
                "optimization_goal": "OFFSITE_CONVERSIONS",
                "billing_event": "IMPRESSIONS",
                "targeting": {
                    "geo_locations": {"countries": ["US"]},
                    "targeting_automation": {"advantage_audience": 1}
                },
                "promoted_object": {
                    "pixel_id": "9876543210",
                    "custom_event_type": "PURCHASE"
                }
            }
        )
        # → adset_id: 223344556
        
        # Step 4: Ad
        meta_ads_ad_create(
            ad_account_id="111222333",
            body={
                "name": "SkinFirst - Shop Clean Beauty - TEST",
                "adset_id": "223344556",
                "creative": {
                    "object_story_spec": {
                        "page_id": "632278516639981",
                        "link_data": {
                            "link": "https://skinfirst.com/shop",
                            "image_hash": "abc123def456",
                            "call_to_action": {"type": "SHOP_NOW"},
                            "message": "Your skin deserves better. Discover clean skincare that actually works.",
                            "name": "Shop SkinFirst",
                            "description": "Free shipping on orders over $40."
                        }
                    }
                }
                # No status field — ads default to PAUSED
            }
        )
        # → ad_id: 112233445
        ```
        
        ### Preview and summary
        
        ```python
        # creative_id from ad creation response: response.creative.id
        meta_ads_ad_previews_get(creative_ids=["<creative_id>"])
        # Summarize preview formats to user — never render iframe inline
        ```
        
        ```
        Campaign created and paused. Here's what was built:
        
        Campaign: "Sales - SkinFirst - TEST - 2026-06"
        Status: PAUSED
        Budget: $30/day
        Objective: Purchases (PURCHASE pixel event)
        Optimization: OFFSITE_CONVERSIONS
        Targeting: United States, Advantage+ broad audience
        Ad previews: [available in the dashboard]
        
        When you're ready to go live, reply "activate" and I'll launch it.
        ```
        
        ### Activation (when user approves)
        
        ```python
        # RIGHT — activates campaign + ad sets + ads
        meta_ads_campaigns_activate(campaign_id="987654321")
        
        # WRONG — do not use this
        # meta_ads_campaign_update(campaign_id="987654321", status="ACTIVE")
        ```
        
    • account-audit.md 3.5 KB
      # Meta Ads account audit
      
      Comprehensive audit of a Meta Ads account: structure, performance, and optimization opportunities. This is a read-only workflow — it produces findings and recommendations, never applies changes. If the user approves a recommendation, route the change through the normal update tools with per-change approval.
      
      For query mechanics (insights args, date presets, cached data vs live API), see [analytics.md](analytics.md).
      
      ## Step 1: Account overview
      
      1. `meta_ads_adaccount_list` — list all ad accounts.
      2. For each account in scope, note spend limits, timezone, and currency.
      3. Check account status and any restrictions. If accounts are missing or errors look like permission problems, run `meta_ads_health_check()` and surface the diagnostics.
      
      Confirm with the user which account(s) to audit if more than one is available.
      
      ## Step 2: Campaign structure review
      
      For each account:
      
      1. `meta_ads_campaigns_search` — list campaigns (filter to active, or include paused if the user wants a full audit).
      2. Group campaigns by objective (awareness, traffic, conversions/sales, leads, app promotion).
      3. Note naming conventions, duplicates, and organization — inconsistent naming and overlapping objectives are audit findings in themselves.
      
      ## Step 3: Performance analysis
      
      Pull performance for the audit window (default: last 30 days — confirm with the user):
      
      1. `meta_ads_insights_get` at account level (see [analytics.md](analytics.md) for args and valid `date_preset` values; prefer cached data for long windows).
      2. Key metrics to capture:
         - Total spend
         - Impressions and reach
         - Click-through rate (CTR)
         - Cost per result (CPR)
         - Return on ad spend (ROAS) — only when conversion tracking is verified
      3. Identify top and bottom performers by cost per result.
      
      ## Step 4: Ad set analysis
      
      For top-spending campaigns:
      
      1. `meta_ads_adset_list` — get ad set details.
      2. Review targeting settings:
         - Audience sizes (too narrow → high CPM; overlapping → auction competition)
         - Age/gender targeting
         - Interest and behavior targeting
         - Lookalike audiences
      3. Check budget distribution — many small ad-set budgets under one campaign usually underperform consolidated budgets (CBO/Advantage+).
      4. Check attribution windows are consistent across ad sets you compare (see the `attribution_spec` section in [constraints.md](constraints.md)).
      
      ## Step 5: Creative review
      
      For top ad sets:
      
      1. `meta_ads_ad_list` — list ads; `meta_ads_ad_get` for details.
      2. Review creative types in use: static images, videos, carousels, dynamic creative.
      3. Check ad fatigue: frequency > 3 over the window is a refresh signal.
      4. Check UTM coverage: creatives driving to a destination without `url_tags` are unmeasurable downstream — flag them.
      
      ## Step 6: Recommendations
      
      Based on findings, provide:
      
      1. **Quick wins** — changes that can be made immediately (pause zero-conversion spenders, fix missing UTMs, refresh fatigued creatives).
      2. **Structural changes** — longer-term improvements (consolidation, objective realignment, budget rebalancing).
      3. **Testing opportunities** — new approaches to try (audiences, creative formats, placements).
      
      Do not apply any change without explicit per-change user approval.
      
      ## Output format
      
      Create a summary report with:
      
      - Executive summary
      - Key metrics table (spend, CTR, CPR, ROAS by campaign)
      - Top/bottom performing campaigns
      - Prioritized recommendations (quick wins → structural → tests)
      
      If the user wants an interactive view, build a dashboard via [dashboards.md](dashboards.md).
      
    • analytics.md 7 KB
      # Meta Ads: Performance Analytics
      
      Use this for querying performance data, ad-level insights, and historical reporting.
      
      ---
      
      ## Where Meta performance data comes from
      
      The Meta API insights tools are the source of truth. There is no Hyper-managed Meta cache
      table and no Meta sync tool. Do not query one and do not try to refresh one.
      
      | Situation | Use |
      |---|---|
      | Any performance check, trend, or drilldown | `meta_ads_insights_get` |
      | Campaign created in this session | `meta_ads_insights_get` or `meta_ads_campaign_get` |
      | History longer than the API returns conveniently, or a join against non-Meta data | A warehouse table the workspace loads itself — see "Querying warehouse tables" below |
      
      > **Important**: always name the date range you queried when you report numbers back. Meta
      > attribution shifts as conversions land, so the same window can return different totals on
      > different days.
      
      ---
      
      ## Querying insights via the Meta API
      
      ### Account-level campaign summary
      
      ```json
      {
        "object_id": "act_123456789",
        "object_type": "account",
        "level": "campaign",
        "date_preset": "last_30d",
        "include_actions": true
      }
      ```
      
      ### Ad set breakdown
      
      ```json
      {
        "object_id": "act_123456789",
        "object_type": "account",
        "level": "adset",
        "date_preset": "last_30d",
        "include_actions": true
      }
      ```
      
      ### Ad-level breakdown (use for historical or drilldown)
      
      Use `level: "ad"` to get ad-level data across the full account. Do **not** iterate every ad ID individually — that is slow and will hit rate limits.
      
      ```json
      {
        "object_id": "act_123456789",
        "object_type": "account",
        "level": "ad",
        "date_preset": "last_90d",
        "include_actions": true
      }
      ```
      
      Use `object_type: "ad"` only when drilling into a single specific ad.
      
      Use `time_increment: "1"` only when daily rows are needed (daily spend trends, delivery dates). It significantly increases response size — avoid for summary queries.
      
      ---
      
      ## Valid date presets
      
      `date_preset` accepts only Meta's fixed preset values. Do **not** invent values — they will be rejected by the API with no clear error message.
      
      **Valid presets:**
      
      ```
      today               yesterday           last_3d             last_7d
      last_14d            last_28d            last_30d            last_90d
      last_week_mon_sun   last_week_sun_sat   last_month          last_quarter
      last_year           this_week_mon_today this_week_sun_today this_month
      this_quarter        this_year           maximum             data_maximum
      ```
      
      **These do NOT exist and will be rejected:**
      `last_60d`, `last_1d`, `last_180d`, `last_6_months`, `last_45d`
      
      **For any window without a matching preset, use `time_range` instead:**
      
      ```json
      {
        "time_range": {
          "since": "2026-04-01",
          "until": "2026-05-31"
        }
      }
      ```
      
      `since`/`until` are `YYYY-MM-DD`. `time_range` overrides `date_preset` when both are present.
      
      For all-time / lifetime data: `"date_preset": "maximum"` (optionally with `"time_increment": "all_days"`).
      
      Do not claim Meta only supports 7 or 28 days unless an actual API response says so.
      
      ---
      
      ## Querying warehouse tables (optional)
      
      This section applies only when the workspace loads Meta data into its own database or
      warehouse. Read it through the `database` toolkit — the single database path in Hyper:
      
      1. Call `database_tables_list` to discover what the connected database actually holds.
      2. Call `database_tables_describe` on the table you found to get its real columns.
      3. Query it with `database_query`.
      
      **Never guess a table name or a column name.** The queries below are shapes, not literals —
      substitute the real table and the real column names you got from steps 1 and 2. If the
      connected database holds no Meta data, use `meta_ads_insights_get` instead.
      
      ### Daily spend trend
      
      ```sql
      SELECT
        campaign_name,
        date_start,
        SUM(spend) AS total_spend,
        SUM(impressions) AS total_impressions
      FROM <table from database_tables_list>
      GROUP BY campaign_name, date_start
      ORDER BY date_start DESC
      LIMIT 100;
      ```
      
      ### Campaign performance summary
      
      ```sql
      SELECT
        campaign_name,
        SUM(spend) AS spend,
        SUM(impressions) AS impressions,
        SUM(clicks) AS clicks,
        ROUND(SUM(clicks)::numeric / NULLIF(SUM(impressions), 0) * 100, 2) AS ctr_pct,
        ROUND(SUM(spend)::numeric / NULLIF(SUM(clicks), 0), 2) AS cpc
      FROM <table from database_tables_list>
      GROUP BY campaign_name
      ORDER BY spend DESC;
      ```
      
      ### Ad set cost-per-conversion
      
      ```sql
      SELECT
        adset_name,
        SUM(spend) AS spend,
        SUM(conversions) AS conversions,
        CASE
          WHEN SUM(conversions) > 0 THEN ROUND(SUM(spend)::numeric / SUM(conversions), 2)
          ELSE NULL
        END AS cost_per_conversion
      FROM <table from database_tables_list>
      GROUP BY adset_name
      ORDER BY spend DESC;
      ```
      
      Default to `meta_ads_insights_get`. Reach for a warehouse table only when the workspace has
      one and the request genuinely needs it.
      
      ---
      
      ## Replicating an existing campaign (analyze → create)
      
      When the task is "find the best performer and build a new campaign modelled on it," inspect the source with `meta_ads_campaign_get` and `meta_ads_adset_list`, then build the new campaign via the matching objective workflow ([discovery.md](discovery.md) → relevant `campaigns/<objective>.md`).
      
      > **`get_ad_sets` often returns `promoted_object: null` (and `bid_amount`/`bid_strategy: null`) even when the source ad set actually uses pixel tracking.** Do not assume the source had no pixel just because the GET response shows null. When replicating a sales or leads campaign, re-derive `promoted_object` yourself: look up the pixel with `meta_ads_ad_pixels_list`, infer the `custom_event_type` from the conversion events visible in the source's insights (e.g. PURCHASE, LEAD, COMPLETE_REGISTRATION), and set it explicitly on the new ad set per the objective workflow.
      
      Carry forward from the source: objective, targeting (age/geo/advantage_audience), and budget mode (campaign-level CBO vs ad-set). Re-derive everything pixel/promoted_object-related rather than trusting the GET response.
      
      ### Duplicating a campaign and swapping creatives
      
      For "duplicate this campaign exactly but with new creatives":
      
      1. **Read the source structure**: `meta_ads_campaign_get`, `meta_ads_adset_list`, `meta_ads_ad_list`. For the creative, `meta_ads_creative_get` (the list view shows `link_url: null` — get the full creative to see the real destination).
      2. **Recreate** campaign → ad set → ad via the matching objective workflow, copying objective, targeting, and budget mode. Re-derive `promoted_object` (see warning above). Name the new campaign as the user specified.
      3. **Swap creatives**: upload the new images (`meta_ads_ad_images_upload`), then either build a fresh inline `object_story_spec` on the new ad or create new creatives with `meta_ads_creative_create` and attach by `creative_id`.
      4. **Verify before deleting anything**: confirm the new ads were created and the new creatives are attached (`meta_ads_ad_list` / `meta_ads_creative_get`).
      5. **Only then** delete old draft creatives with `meta_ads_creative_delete` (or `meta_ads_ad_delete` for ads). Never delete the source until the replacement is confirmed.
      
      Leave the new campaign PAUSED unless the user said to activate.
      
    • automated-rules.md 3.2 KB
      # Automated Rules (Ad Rules Engine)
      
      Meta-native automation: rules live on the ad account and run **inside Meta's
      infrastructure** — once created they keep working with no agent involved. Use
      them for standing guards (auto-pause on bad CPA, budget caps, alerts), not for
      one-off actions you can do directly with the update tools.
      
      ## Tools
      
      | Tool | Purpose |
      | --- | --- |
      | `meta_ads_adrule_create` | Create a rule (`ad_account_id` + `body`) |
      | `meta_ads_adrule_list` | List rules on the account |
      | `meta_ads_adrule_get` | Read one rule (`adrule_id`, `fields`) |
      | `meta_ads_adrule_update` | Change a rule (e.g. `body={"status": "DISABLED"}`) |
      | `meta_ads_adrule_delete` | Permanently remove a rule |
      | `meta_ads_adrule_history_list` | Audit log: what rules changed/paused/notified, and when |
      
      ## Rule anatomy
      
      A rule = **evaluation_spec** (what to watch) + **execution_spec** (what to do)
      + optional **schedule_spec**. Two evaluation types:
      
      - `TRIGGER` — evaluated in real time when metadata or Insights change (the
        `trigger` object is required inside `evaluation_spec`).
      - `SCHEDULE` — evaluated on an interval (pair with `schedule_spec`).
      
      `evaluation_spec.filters` select which objects the rule inspects — always
      include an entity filter (e.g. `entity_type` or an id list) so the rule
      doesn't scan the whole account.
      
      Common `execution_spec.execution_type` values: `PAUSE`, `UNPAUSE`,
      `CHANGE_BUDGET`, `CHANGE_BID`, `NOTIFICATION`, `REBALANCE_BUDGET`, `ROTATE`.
      `status` on the rule itself: `ENABLED` / `DISABLED` (the create tool's schema
      carries the full enum).
      
      ## Example — pause ad sets whose 3-day CPA exceeds $30
      
      ```python
      meta_ads_adrule_create(
          ad_account_id="1122334455",
          body={
              "name": "Guard: pause ad sets with CPA > $30 (3d)",
              "status": "ENABLED",
              "evaluation_spec": {
                  "evaluation_type": "SCHEDULE",
                  "filters": [
                      {"field": "entity_type", "value": "ADSET", "operator": "EQUAL"},
                      {"field": "time_preset", "value": "LAST_3_DAYS", "operator": "EQUAL"},
                      {"field": "cost_per", "value": 3000, "operator": "GREATER_THAN"}
                  ]
              },
              "execution_spec": {
                  "execution_type": "PAUSE",
                  "execution_options": [
                      {"field": "user_ids", "value": [], "operator": "EQUAL"}
                  ]
              },
              "schedule_spec": {"schedule_type": "SEMI_HOURLY"}
          }
      )
      ```
      
      > Money fields inside rule filters are in **cents**, same as budgets
      > ($30 = 3000). Verify filter field names against the account's metrics —
      > `meta_ads_adrule_history_list` shows whether a rule is actually firing.
      
      ## Rules of thumb
      
      - **Prefer a rule over polling**: if the user wants "pause X when Y happens",
        create a rule instead of a scheduled agent that polls insights.
      - **Surface every rule you create** — name rules descriptively (prefix with the
        user's intent) and report the rule id; they are standing automations the
        user must be able to find and disable.
      - **Check history before debugging delivery**: an ad set that "randomly
        paused" is often a rule firing — `meta_ads_adrule_history_list` answers it
        in one call.
      - Disabling (`status: "DISABLED"`) is reversible; delete only on explicit
        request.
      
    • constraints.md 19.3 KB
      # Meta Ads: Critical Constraints
      
      Read this before any campaign creation step. These rules are non-negotiable and apply to every workflow.
      
      ---
      
      ## 1. Budgets: always in cents
      
      All budget and bid values are in **cents** (hundredths of the currency unit).
      
      | Dollar amount | Correct cents value |
      |---|---|
      | $5.00/day | 500 |
      | $20.00/day | 2000 |
      | $50.00/day | 5000 |
      | $100.00/day | 10000 |
      | $5.50/day | 550 |
      
      ```
      # WRONG
      "daily_budget": 20
      
      # RIGHT
      "daily_budget": 2000   # = $20.00/day
      ```
      
      Applies to: `daily_budget`, `lifetime_budget`, `bid_amount`, and any other budget field.
      
      **Wrong field names that will cause Pydantic errors:**
      - `daily_budget_cents` → use `daily_budget`
      - `budget` → use `daily_budget` or `lifetime_budget`
      
      ---
      
      ## 2. Budget placement: campaign vs ad set
      
      | Campaign type | Budget lives at | Ad set budget |
      |---|---|---|
      | Advantage+ | **Campaign level** | Must be `null` / omitted |
      | Manual | **Ad set level** | Required here |
      
      Never set budget at both levels simultaneously.
      
      If you have multiple ad sets under one Advantage+ campaign, the campaign `daily_budget` is shared across all of them — Meta distributes spend automatically based on performance. Each ad set has no budget field; only the campaign does.
      
      ### CBO lock: setting campaign budget blocks ad-set budgets
      
      The moment a campaign is created with a `daily_budget` (or `lifetime_budget`), Meta activates **Campaign Budget Optimization (CBO)** for that campaign. Every child ad set is then **forbidden** from carrying its own `daily_budget` / `lifetime_budget`. Attempting it fails with:
      
      ```
      Cannot set daily_budget/lifetime_budget on ad set when campaign has budget (CBO).
      ```
      
      > The error message suggests `daily_spend_cap` / `lifetime_spend_cap`. **Do not chase that** — those fields are NOT in the `AdSetCreateInput` schema and will fail Pydantic validation. The message is misleading.
      
      **Correct build order when you need ad-set-level budgets (manual campaigns):**
      
      1. Create the campaign with **no budget**.
      2. Create each ad set with its own `daily_budget`.
      
      **Correct build order for Advantage+ (campaign-level budget):**
      
      1. Create the campaign with **no budget**.
      2. Create ad sets with **no budget** (just `bid_amount` if the account's bid strategy requires it).
      3. Add `daily_budget` to the campaign via `meta_ads_campaign_update` **after** the ad sets exist.
      
      Deciding budget placement per campaign *before* creating anything avoids a delete-and-rebuild cycle. This matters most in multi-campaign builds that mix Advantage+ and manual modes — see [multi-campaign-funnel.md](multi-campaign-funnel.md).
      
      ---
      
      ## 3. Activation: use activate(), not update()
      
      ```
      # WRONG — only flips the campaign flag; ad sets and ads stay PAUSED → nothing serves
      meta_ads_campaign_update(campaign_id="...", body={"status": "ACTIVE"})
      
      # RIGHT — activates campaign + all ad sets + all ads in one call
      meta_ads_campaigns_activate(campaign_id)
      ```
      
      `update_campaign(status="ACTIVE")` creates a silent non-delivery failure: the dashboard shows ACTIVE but nothing is actually serving because ad sets and ads remain PAUSED.
      
      ---
      
      ## 4. Campaign status: always start PAUSED
      
      Always create campaigns with `status="PAUSED"`. Never launch live without user review.
      
      Omit `status` when creating ads — ads default to PAUSED automatically. Never pass `status="ACTIVE"` on ad creation; always go through `meta_ads_campaigns_activate` as a deliberate launch step.
      
      ---
      
      ## 5. Targeting automation placement
      
      `targeting_automation` must be **nested inside** the `targeting` object — not at the top level of the ad set.
      
      ```json
      // WRONG — targeting_automation at ad set top level
      {
        "targeting": {"geo_locations": {"countries": ["US"]}},
        "targeting_automation": {"advantage_audience": 1}
      }
      
      // RIGHT — targeting_automation inside targeting
      {
        "targeting": {
          "geo_locations": {"countries": ["US"]},
          "targeting_automation": {"advantage_audience": 1}
        }
      }
      ```
      
      ### Advantage+ rejects narrow age bands
      
      With `advantage_audience: 1`, Meta requires `age_min` ≤ 25 and `age_max` ≥ 65 — anything narrower is rejected. A request like "Advantage+, ages 25–54" cannot be honored as stated.
      
      If a strict age band matters to the user, do not use Advantage+ for that ad set: omit `targeting_automation.advantage_audience` and set `age_min`/`age_max` exactly. Surface the trade-off: Advantage+ audience expansion vs. a precise age band — you can't have both.
      
      ---
      
      ## 6. One calling convention: `ad_account_id` + `body`
      
      Every create tool takes the ad account as `ad_account_id` plus a single `body` dict with the object fields; update tools take the object id (`campaign_id` / `adset_id` / `ad_id` / `creative_id`) plus `body`. There is no `mode` or `input_data` parameter.
      
      ```
      # WRONG — flat params
      meta_ads_adset_create(account_id="act_...", campaign_id="123", name="...")
      
      # RIGHT — ad_account_id + body
      meta_ads_adset_create(
          ad_account_id="1122334455",
          body={"campaign_id": "123", "name": "...", "optimization_goal": "...", ...}
      )
      ```
      
      For an Advantage+ audience, set `targeting_automation.advantage_audience` inside `body.targeting`; for a strict manual audience, omit it (see the Advantage+ note above).
      
      ---
      
      ## 7. create_ad: fields go in `body`
      
      `meta_ads_ad_create` takes `ad_account_id` plus a `body` dict — do not pass `name`, `adset_id` etc. as separate top-level arguments, and do not pass JSON as a string.
      
      ```
      ❌ WRONG: meta_ads_ad_create(account_id="act_...", adset_id="123", name="My Ad")
      ❌ WRONG: meta_ads_ad_create(ad_account_id="...", body='{"adset_id": "123"}')  # string not dict
      ✅ RIGHT:  meta_ads_ad_create(ad_account_id="1122334455", body={"adset_id": "123", "name": "My Ad", "creative": {"creative_id": "..."}, "status": "PAUSED"})
      ```
      
      ### object_story_spec / link_data fields (applies to every objective)
      
      When building an inline creative (`creative.object_story_spec`):
      
      - `page_id` goes at the **top level** of `object_story_spec` — never inside `link_data`. Omitting it → "Facebook Page is Missing."
      
      Valid `link_data` fields: `message`, `link`, `description`, `picture`, `image_hash`, `call_to_action`, `name`, `caption`.
      
      Common footguns:
      - `headline` is **not** a valid `link_data` field → "field headline is not supported." Put the headline text in `name` (or `description`).
      - `caption` must be a **URL** (e.g. `"example.com"`), not copy text → "Link data caption is not an Url."
      
      ---
      
      ## 8. Optimization goal must match objective
      
      `optimization_goal` in the ad set must match the campaign objective. Using the wrong goal is the most common cause of Meta API 400 rejections on ad set creation.
      
      | Campaign Objective | optimization_goal | billing_event |
      |---|---|---|
      | OUTCOME_SALES | `OFFSITE_CONVERSIONS` | `IMPRESSIONS` |
      | OUTCOME_TRAFFIC | `LINK_CLICKS` | `IMPRESSIONS` |
      | OUTCOME_LEADS | `LEAD_GENERATION` | `IMPRESSIONS` |
      | OUTCOME_ENGAGEMENT | `POST_ENGAGEMENT` | `IMPRESSIONS` |
      | OUTCOME_AWARENESS | `REACH` | `IMPRESSIONS` |
      | OUTCOME_APP_PROMOTION | `APP_INSTALLS` | `IMPRESSIONS` |
      
      `billing_event` is almost always `IMPRESSIONS`. Only change it if you have a specific reason.
      
      ---
      
      ## 9. promoted_object requirements by objective
      
      These objectives **require** `promoted_object` on the ad set. Omitting it causes a cryptic API error.
      
      | Objective | promoted_object | Example |
      |---|---|---|
      | OUTCOME_LEADS (lead form) | **Required** | `{"page_id": "632278516639981"}` |
      | OUTCOME_LEADS (website pixel) | **Required — 3 fields** | `{"pixel_id": "9876543210", "custom_event_type": "LEAD", "page_id": "632278516639981"}` |
      | OUTCOME_APP_PROMOTION | **Required** | `{"application_id": "APP_ID", "object_store_url": "APP_STORE_URL"}` |
      | OUTCOME_SALES (conversions) | **Required** | `{"pixel_id": "9876543210", "custom_event_type": "PURCHASE"}` |
      | OUTCOME_TRAFFIC | Not required | — |
      | OUTCOME_AWARENESS | Not required | — |
      | OUTCOME_ENGAGEMENT | Optional | `{"page_id": "PAGE_ID"}` |
      
      > **App promotion — the creative link must match `object_store_url`.** For OUTCOME_APP_PROMOTION, the ad's creative CTA destination (`link_data.link`, and `call_to_action.value.link` if set) must equal the ad set's `promoted_object.object_store_url` **exactly**, or Meta rejects the ad with *"Object store URL does not match promoted object"* (code 100). When you didn't just author the ad set yourself, call `meta_ads_adset_get` and copy `promoted_object.object_store_url` verbatim — never guess the store URL.
      
      ### Attribution window (`attribution_spec`)
      
      For conversion ad sets, set the attribution window explicitly with `attribution_spec` rather than relying on Meta's default — the window defines what counts as a conversion and is how the advertiser measures cost-per-result. It's an ad-set field (inside `body` on `meta_ads_adset_create`), and it reads back on `meta_ads_adset_get`.
      
      ```python
      # 1-day click (common for app installs / FTD / direct-response)
      attribution_spec=[{"event_type": "CLICK_THROUGH", "window_days": 1}]
      
      # 7-day click + 1-day view (common for web registration / e-commerce)
      attribution_spec=[
          {"event_type": "CLICK_THROUGH", "window_days": 7},
          {"event_type": "VIEW_THROUGH", "window_days": 1},
      ]
      ```
      
      Match the window to the play — don't mix windows across ad sets you intend to compare, or the results aren't comparable.
      
      ---
      
      ## 10. Never change campaign objective or optimization goal without explicit user approval
      
      Once an ad set is created under a campaign, the `objective` field is immutable. If the objective is wrong, create a new campaign.
      
      **More importantly:** never silently substitute a different objective or optimization goal at any point — not during planning, not during error recovery. Changing `OUTCOME_LEADS` to `OUTCOME_TRAFFIC` to work around an API error fundamentally alters what the campaign optimizes for. The user asked for leads; a traffic campaign does not deliver that.
      
      If you cannot create the campaign as requested due to an API or account constraint:
      1. Stop.
      2. Explain what the constraint is.
      3. Present options to the user.
      4. Wait for explicit approval before changing anything.
      
      ---
      
      ## 11. page_id must be passed explicitly
      
      Always populate `page_id` on each ad set from `meta_ads_owned_pages_list`. Auto-resolution only works when exactly one Page is linked to the Business Manager. Pass it explicitly.
      
      ---
      
      ## 12. Lifetime budgets require dates
      
      ```json
      {
        "lifetime_budget": 50000,
        "start_time": "2026-07-01T00:00:00+0000",
        "end_time":   "2026-07-31T23:59:59+0000"
      }
      ```
      
      Lifetime budget without `end_time` will fail validation.
      
      ---
      
      ## 13. Ad preview handling
      
      `meta_ads_ad_previews_get` requires `creative_ids` (a list). `ad_id` is **not** a valid parameter and will fail.
      
      ```python
      # After meta_ads_ad_create, extract creative_id from the response:
      # response.creative.id
      
      meta_ads_ad_previews_get(creative_ids=["<creative_id>"])
      
      # When previewing an existing creative before ad creation:
      meta_ads_ad_previews_get(creative_ids=["<existing_creative_id>"])
      ```
      
      - Never paste or render iframe/html snippets directly in chat.
      - Summarize which preview formats succeeded or failed.
      - Direct the user to the UI artifact to view previews.
      
      ---
      
      ## 14. Errors that require user input — do not auto-fix
      
      | Error | Do NOT do this | Do this instead |
      |---|---|---|
      | "Bid amount required" (subcode 1815857) — **and the user did NOT ask for a bid cap** | Change `optimization_goal` or invent a `bid_amount` | **Stop.** Surface to user: "This account requires an explicit bid amount due to its bid strategy. How would you like to proceed?" Do NOT change the optimization goal as a workaround — that silently misconfigures the campaign. (If the user *did* specify a bid strategy/cap, just set it — see section 16.) |
      | "Performance goal isn't available" (subcode 2490408) | Substitute a different `optimization_goal` (e.g. LINK_CLICKS) | **Stop.** See note below — this is caused by an account-level bid strategy incompatibility, not a fixable parameter error. |
      | "No Facebook Pages found" | Guess a page_id | Run `meta_ads_health_check`, surface results |
      | "No business ID found" from `list_owned_pages` | Skip and ask user | Try `meta_accounts_list` as fallback |
      | "image_hash is not valid" or "Link data image_hash" | Invalid or wrong-account hash | Call `meta_ads_ad_images_list(account_id)` to find valid hashes in this account, then re-upload if needed |
      | "Cannot set daily_budget on ad set when campaign has budget (CBO)" | Campaign was created with a budget | Recreate the campaign with no budget, OR put budget at ad-set level only. Do NOT use `daily_spend_cap` — not a valid field. See section 2. |
      | Pydantic error on `daily_spend_cap` / `lifetime_spend_cap` | Field not in `AdSetCreateInput` schema | These fields don't exist on the ad set tool. Use the build order in section 2 instead. |
      | `list_ad_images` rejects `detail` value | Invalid enum | `detail` accepts only `"id_only"`, `"core"`, `"summary"`, `"full"`. Not `"minimal"`. |
      | Pydantic "Unexpected keyword argument" | Guess a different field name | Re-read constraints and the relevant campaign workflow |
      
      **OUTCOME_LEADS + LOWEST_COST_WITH_BID_CAP incompatibility (error 2490408):**
      
      When an account's default bid strategy is `LOWEST_COST_WITH_BID_CAP`, the `LEAD_GENERATION` optimization goal is unavailable for `OUTCOME_LEADS` — even if you provide a valid `bid_amount`. This is a Meta platform constraint, not a parameter error. Adding `bid_amount` will not resolve it.
      
      Resolution paths (present to user, get approval before proceeding):
      
      1. **Preferred**: Change the account's default bid strategy to "Lowest Cost" (no cap) in Meta Business Manager → then retry with `LEAD_GENERATION`, no `bid_amount` needed.
      2. **Degraded workaround**: Use `OUTCOME_TRAFFIC` with `optimization_goal: LINK_CLICKS`. This runs the campaign but optimizes for clicks to the page, not lead events. **Must disclose this to the user** — it is not a leads campaign.
      3. **Alternative**: Try the native lead form path (`destination_type: ON_AD`) which may have different bid strategy requirements.
      
      Never choose a resolution path without telling the user what the constraint is and what the tradeoff is.
      
      ---
      
      ## 15. Editing existing campaigns
      
      When the user wants to update, pause, adjust budget, or change targeting on an existing campaign:
      
      | What to update | Tool | Key params |
      |---|---|---|
      | Campaign status / budget | `meta_ads_campaign_update` | `campaign_id`, `body` (`status`, `daily_budget`) |
      | Ad set status / budget / targeting | `meta_ads_adset_update` | `adset_id`, `body` (`status`, `daily_budget`, `targeting`) |
      | Ad status / creative | `meta_ads_ad_update` | `ad_id`, `body` (`status`) |
      | Take everything live at once | `meta_ads_campaigns_activate` | `campaign_id` |
      
      Before updating, fetch current state with `meta_ads_campaign_get` or `meta_ads_adset_get` — do not guess at current values.
      
      **To pause:** `meta_ads_campaign_update(campaign_id="...", body={"status": "PAUSED"})`
      **To increase budget:** `meta_ads_campaign_update(campaign_id="...", body={"daily_budget": 5000})` (in cents)
      **To go live:** always `meta_ads_campaigns_activate(campaign_id)`, not `update_campaign(status="ACTIVE")`
      
      ---
      
      ## 16. Bid strategy & bid_amount
      
      `bid_strategy` and `bid_amount` (in cents) are both ad-set `body` fields. The toolkit validates their pairing before sending (e.g. `LOWEST_COST_WITH_BID_CAP` without `bid_amount` is rejected up front).
      
      **When the user explicitly asks for a bid cap, set it and proceed** — do not stop and ask (they already told you):
      
      ```python
      meta_ads_adset_create(
          ad_account_id="1122334455",
          body={
              "campaign_id": "...",
              "name": "...",
              "optimization_goal": "LINK_CLICKS",
              "billing_event": "IMPRESSIONS",
              "bid_strategy": "LOWEST_COST_WITH_BID_CAP",
              "daily_budget": 7500,                  # $75/day, ad-set level for manual
              "bid_amount": 250,                     # $2.50 cap
              "targeting": { ... }
          }
      )
      ```
      
      - `LOWEST_COST_WITH_BID_CAP` and `COST_CAP` require `bid_amount`.
      - For Advantage+ / CBO, `bid_strategy` belongs on the **campaign** (`meta_ads_campaign_create`) and `bid_amount` on the **ad set**.
      - The "stop and surface" rule in section 14 applies only when a bid-amount error appears that the user did **not** request (an account default strategy you didn't choose). Never invent a cap in that case.
      
      ---
      
      ## 17. Geo targeting beyond country (regions, cities, DMAs)
      
      `geo_locations.countries` accepts ISO codes directly (`["US", "CA"]`). But regions, cities, and DMAs (`geo_markets`) require Meta's internal **numeric keys** — you **cannot guess them**. The validator rejects hand-written keys like `"US-CA-803"`.
      
      Resolve keys first with `meta_ads_targeting_search`:
      
      ```python
      meta_ads_targeting_search(
          search_type="adgeolocation",
          location_types=["region"],      # or ["city"], ["geo_market"] for DMAs
          country_code="US",
          q="California"
      )
      ```
      
      Then pass the returned `key` values:
      
      ```json
      "geo_locations": {
        "regions":     [{"key": "<key from search>"}],
        "cities":      [{"key": "<key from search>"}],
        "geo_markets": [{"key": "<key from search>"}]
      }
      ```
      
      Never write geo keys by hand — always resolve them through the search tool first.
      
      ---
      
      ## 18. Audience exclusions — interest/behavior/demographic are deprecated
      
      As of early 2024, Meta deprecated interest-based, behavior-based, and demographic-based audience **exclusions**. The `exclusions` field in targeting now only supports:
      
      - `exclusions.custom_audiences` — exclude a custom audience by ID
      - `exclusions.lookalike_audience` — exclude a lookalike audience
      
      The following exclusion types **no longer work** and will return an API error:
      
      ```
      exclusions.interests        ← REMOVED
      exclusions.behaviors        ← REMOVED
      exclusions.demographics     ← REMOVED
      ```
      
      This is a **Meta platform policy change**, not a toolkit limitation. When a user asks to exclude interests or demographics from targeting, inform them that only custom audience exclusions are available and offer to set up a custom audience for the exclusion instead.
      
      ---
      
      ## 19. Dynamic creative (`asset_feed_spec`) requires the two-step pattern
      
      `asset_feed_spec` (multiple text/headline/image variations for dynamic creative) **cannot be passed inline** inside the `creative` dict of `meta_ads_ad_create`. It must be created as a standalone creative first.
      
      **Two-step pattern (required):**
      
      ```python
      # Step 1 — create the dynamic creative with asset_feed_spec
      creative = meta_ads_creative_create(
          ad_account_id="1122334455",
          body={
              "name": "My Dynamic Creative",
              "object_story_spec": {
                  "page_id": "<page_id>",
                  "link_data": {
                      "link": "https://example.com",
                      "message": "Primary copy A",
                  }
              },
              "asset_feed_spec": {
                  "bodies": [{"text": "Copy A"}, {"text": "Copy B"}, {"text": "Copy C"}],
                  "titles": [{"text": "Headline 1"}, {"text": "Headline 2"}],
                  "images": [{"hash": "<image_hash_1>"}, {"hash": "<image_hash_2>"}],
                  "link_urls": [{"website_url": "https://example.com"}],
                  "call_to_action_types": [{"type": "LEARN_MORE"}]
              }
          }
      )
      # → capture creative_id
      
      # Step 2 — attach by creative_id
      meta_ads_ad_create(ad_account_id="1122334455", body={
          "adset_id": "<adset_id>",
          "name": "My Dynamic Ad",
          "creative": {"creative_id": "<creative_id from step 1>"},
          "status": "PAUSED"
      })
      ```
      
      Passing `asset_feed_spec` directly inside `meta_ads_ad_create`'s `creative` dict is not supported — the inline creative spec only accepts `object_story_spec` or `creative_id`.
      
    • dashboards.md 2.8 KB
      # Meta Ads: Dashboard Building
      
      When the user asks for a Meta dashboard or performance report interface, follow the workflow below.
      
      ---
      
      ## Workflow: cached data first, then build
      
      ### 1. Use cached data
      
      Read the Meta context block in the toolkit for the table name and last sync timestamp. Query cached data via `database_query` (the canonical SQL tool) before making any live Meta API calls.
      
      ### 2. Build the dashboard
      
      Build the dashboard using `data_apps_build` with `tool_data_sources` and `sql_data_sources` (see the custom pattern below). Inspect the live tool schema before calling — it documents the accepted data-source shapes and UI components. Do not invent dashboard patterns.
      
      ### 3. Cache refresh policy
      
      Data syncs automatically every 30 minutes. If data is stale or the user needs up-to-the-minute numbers, pull them live with `meta_ads_insights_get`; there is no manual sync tool.
      
      If no cached data exists yet, use Meta API tools directly as a fallback and cache the results.
      
      ---
      
      ## Custom dashboard pattern
      
      Use `tool_data_sources` to fetch from Meta API and save to a cache table. Use `sql_data_sources` to query the cache for UI variables. Pass `scalar` or `rows` shapes explicitly — do not embed raw SQL in UI props.
      
      ```python
      data_apps_build(
          name="Meta Ads Performance",
          tool_data_sources={
              "meta_campaigns": {
                  "tool_name": "meta_ads_insights_get",
                  "tool_args": {
                      "object_id": "act_123456789",
                      "object_type": "account",
                      "level": "campaign",
                      "date_preset": "last_30d",
                      "include_actions": True
                  },
                  "cache_table": "meta_campaign_perf_30d",
                  "mode": "replace"
              }
          },
          sql_data_sources={
              "spend_by_campaign": {
                  "query": "SELECT campaign_name, SUM(spend) as spend FROM meta_campaign_perf_30d GROUP BY campaign_name ORDER BY spend DESC",
                  "shape": "rows"
              },
              "total_spend": {
                  "query": "SELECT SUM(spend) as total FROM meta_campaign_perf_30d",
                  "shape": "scalar"
              }
          },
          prefab_python="..."
      )
      ```
      
      Do not inject Meta API credentials into the dashboard/data app runtime.
      
      Keep custom dashboards focused on the user's question. Do not add extra panels or metrics unless asked.
      
      Do not mention internal dashboard implementation details to the user unless they explicitly ask.
      
      ---
      
      ## Ad preview handling
      
      After campaign creation, call `meta_ads_ad_previews_get(creative_ids=["<creative_id>"])` — the creative_id comes from the ad creation response (`response.creative.id`). Note: `ad_id` is not a valid parameter.
      
      - Never paste or render iframe/html preview snippets directly in chat.
      - Summarize which preview formats succeeded or failed.
      - Direct the user to the UI artifact to view previews.
      
    • discovery.md 6.4 KB
      # Meta Ads: Account Discovery & Research
      
      Run this phase before any campaign creation. It establishes account context, verifies health, and gathers research needed for targeting and creative decisions.
      
      ---
      
      ## Step 1: Account selection
      
      Call `meta_ads_adaccount_list` with `{"detail": "id_only"}`.
      
      - **Multiple accounts returned**: Ask the user to select one before proceeding. Do not assume.
      - **Single account returned**: Inform the user and proceed.
      
      Capture the selected `account_id` (format: `act_XXXXXXXXX`). All subsequent tool calls require it.
      
      ---
      
      ## Step 2: Page discovery
      
      Call `meta_ads_owned_pages_list` with `{"account_id": "<selected_act_id>", "detail": "id_only"}`.
      
      > `account_id` is REQUIRED — never call this tool without it.
      
      **Capture the returned `page_id` explicitly.** Pass it on every ad set. Do not rely on auto-resolution.
      
      **If the call fails with "No business ID found for this ad account":**
      
      First check if you've already run the health check (step 4). If so, the page_id is available in the health check response under `page_publish_access.values[].id` — use that directly, no extra API call needed.
      
      If you haven't run the health check yet, or if it didn't return a page_id, use this fallback:
      
      ```python
      meta_accounts_list(account_id="act_123456789")
      ```
      
      This returns connected pages through an alternate lookup path. Capture `page_id` from the response and continue.
      
      **If no pages are returned at all:**
      1. Run `meta_ads_health_check`.
      2. If "Page publish access" fails: the Page must be linked to this ad account's Business Manager in Meta Business Settings. This is a user-side fix. Surface the issue and stop.
      
      ---
      
      ## Step 3: Pixel discovery (for Sales and Leads campaigns)
      
      If the user intends to track website conversions or leads, call `meta_ads_ad_pixels_list` with `{"account_id": "<act_id>"}`.
      
      Capture the `pixel_id`. It goes in `promoted_object` on the ad set.
      
      **If no pixels are found:** Inform the user. Pixel setup is required for conversion-based campaigns. The user must install and verify the pixel before the campaign can be created.
      
      **If multiple pixels are returned:** Do not guess. Either:
      - Ask the user: "I found [N] pixels ([list names]). Which one should I use for this campaign?"
      - Or call `meta_ads_ad_pixels_get(pixel_id)` on candidates to check which ones are actively receiving the relevant events (e.g. Lead, Purchase) before selecting.
      
      ---
      
      ## Step 3b: Audience discovery & validation (when using custom/lookalike audiences)
      
      If the campaign targets a custom or lookalike audience, call `meta_ads_custom_audiences_list` and/or `meta_ads_lookalike_audiences_list`.
      
      > `meta_ads_lookalike_audiences_list` returns **all** audiences in the account, not just lookalikes. Filter by `subtype` / name yourself.
      
      **Validate any audience before using it.** A discovered audience is not necessarily usable. Check before building the ad set:
      
      - `operation_status.code` must be `200` (Normal). Code `433` means the audience is broken ("couldn't create, delete and retry").
      - `delivery_status.code` must be `200` (ready). Code `300` means too small to deliver.
      - `approximate_count` should be comfortably above ~1000 for reliable delivery.
      
      If a selected audience fails validation, **do not silently substitute a different one.** Surface it to the user:
      > "The lookalike audience you wanted is currently broken (status 433) / too small to deliver. Options: pick a different audience, create a new one, or proceed with [closest alternative]. How would you like to handle it?"
      
      The same applies to **audience parameters that don't match the brief** — e.g. the user asks for a 14-day retargeting window but only a 90-day audience exists. Surface the mismatch and let the user choose; do not quietly use the closest match.
      
      ---
      
      ## Step 4: Health check (recommended)
      
      Call `meta_ads_health_check` before a first campaign on any account.
      
      It verifies: token permissions, ad account access, Page publish access, and pixel access.
      
      Surface any non-PASS items to the user before continuing. Do not proceed with creation if critical items are failing.
      
      ---
      
      ## Step 5: Website research (mandatory for any campaign linking to a website)
      
      **Never skip this phase for website-based campaigns.**
      
      ### a) Screenshot capture (required)
      
      Call `firecrawl_screenshots_create` on the primary site URL and/or the specific landing page URL.
      
      The screenshot is your visual grounding for creative direction — layout, hierarchy, tone, imagery style, hero content. Treat it as required input for image generation.
      
      ### b) Branding extract
      
      Call `firecrawl_branding_extract` on the primary site URL.
      
      This captures brand colors, fonts, tone of voice, and key messaging. Use it to ensure generated ad creatives match the brand.
      
      ### c) Strategic assessment
      
      After scanning, identify:
      - Primary conversion goal
      - Main buyer persona
      - Key differentiators and value proposition
      - Best CTA for this objective — choose from: `LEARN_MORE`, `SHOP_NOW`, `SIGN_UP`, `DOWNLOAD`, `BOOK_NOW`, `GET_OFFER`, `CONTACT_US`, `GET_QUOTE`
      
      Do **not** ask the user about target audiences for Advantage+ campaigns — Advantage+ uses broad targeting by default.
      
      ---
      
      ## Step 6: Budget confirmation
      
      Confirm before building anything:
      
      - Budget amount and currency (e.g., "$30/day USD")
      - Daily or lifetime budget
      - If lifetime: start date and end date
      
      Convert to cents immediately when confirmed. Multiply by 100 — $30/day = 3000.
      
      ---
      
      ## Step 7: Goal clarification (if not already known)
      
      If the campaign objective hasn't been stated, ask:
      
      ```
      What is the primary goal for this campaign?
      
      1. Drive website purchases or conversions → Sales (OUTCOME_SALES)
      2. Generate leads or form submissions → Leads (OUTCOME_LEADS)
      3. Send traffic to a website or page → Traffic (OUTCOME_TRAFFIC)
      4. Build brand awareness or maximize reach → Awareness (OUTCOME_AWARENESS)
      5. Drive engagement with content or the Page → Engagement (OUTCOME_ENGAGEMENT)
      6. Promote a mobile app → App Promotion (OUTCOME_APP_PROMOTION)
      ```
      
      The objective determines which campaign workflow to follow. Do not guess.
      
      Once confirmed, proceed to the appropriate campaign file:
      - Sales → [campaigns/sales.md](campaigns/sales.md)
      - Leads → [campaigns/leads.md](campaigns/leads.md)
      - Traffic → [campaigns/traffic.md](campaigns/traffic.md)
      - Awareness/Engagement → [campaigns/awareness-engagement.md](campaigns/awareness-engagement.md)
      - App Promotion → [campaigns/app-promotion.md](campaigns/app-promotion.md)
      
    • multi-campaign-funnel.md 5.8 KB
      # Multi-Campaign / Funnel Builds
      
      ## When to use
      
      Use this when the user asks for **more than one campaign in a single request** — most commonly a funnel:
      
      - TOF (top of funnel): awareness / reach to cold audiences
      - MOF (middle of funnel): traffic / engagement retargeting warm audiences
      - BOF (bottom of funnel): sales / conversions to lookalikes or customer lists
      
      This file covers **orchestration** — planning, sequencing, and budget-mode decisions across campaigns. It does **not** replace the per-objective workflows. Each individual campaign is still built using its objective file:
      
      - Awareness/Engagement tier → [campaigns/awareness-engagement.md](campaigns/awareness-engagement.md)
      - Traffic tier → [campaigns/traffic.md](campaigns/traffic.md)
      - Sales tier → [campaigns/sales.md](campaigns/sales.md)
      - Leads tier → [campaigns/leads.md](campaigns/leads.md)
      
      Read the relevant objective file for each tier before building it.
      
      ---
      
      ## Why this needs its own plan
      
      Multi-campaign builds fail in ways single campaigns don't, because decisions made on campaign A constrain campaign B. The two big traps:
      
      1. **Mixed budget modes** — a funnel often mixes Advantage+ (campaign-level budget) with manual (ad-set-level budget). Get the build order wrong and you hit the CBO lock (see [constraints.md](constraints.md) section 2) and have to delete and rebuild.
      2. **Audience readiness** — each tier targets a different audience. If one is broken, too small, or doesn't match the brief, you should know *before* you've built two other campaigns around it.
      
      ---
      
      ## Plan before building (required)
      
      Before any tool call, produce a written plan with a row per tier:
      
      | Tier | Objective | Budget | Budget mode | Audience | Audience status |
      |---|---|---|---|---|---|
      | TOF | OUTCOME_AWARENESS | $30/day | Advantage+ (campaign) | Broad US 25-54 | n/a |
      | MOF | OUTCOME_TRAFFIC | $30/day | Manual (ad set) | Website visitors 14d | verify |
      | BOF | OUTCOME_SALES | $50/day | Manual (ad set) | Purchase lookalike | verify |
      
      Filling this table forces the two decisions that prevent rebuilds: **budget mode per tier** and **audience validity per tier**.
      
      ---
      
      ## Step 1: Validate audiences first
      
      For every tier that targets a custom or lookalike audience, run the audience validation from [discovery.md](discovery.md) step 3b **before creating any campaign**:
      
      - `operation_status.code` = 200, `delivery_status.code` = 200, `approximate_count` > ~1000
      - Confirm the audience parameters match the brief (e.g. retention window)
      
      If any audience is broken, too small, or mismatched, **surface it to the user and resolve it before building.** Do not build two campaigns and then silently swap in a customer list for the broken BOF lookalike — the user asked for a specific structure.
      
      ---
      
      ## Step 2: Decide budget mode per tier
      
      | Tier intent | Recommended mode | Budget goes |
      |---|---|---|
      | Advantage+ broad targeting | Advantage+ | Campaign level (added last) |
      | Manual / specific audience targeting | Manual | Ad set level |
      
      A funnel commonly mixes both. That's fine — but it dictates the build order below.
      
      ---
      
      ## Step 3: Build in the order that avoids the CBO lock
      
      This is the critical sequence. Setting a campaign budget activates CBO and blocks ad-set budgets, so budget timing matters.
      
      1. **Create ALL campaigns first with no budget.** Every campaign, no `daily_budget`.
      2. **Create the manual ad sets** (MOF, BOF) with their `daily_budget` at the ad set level. With no campaign budget, there's no CBO lock.
      3. **Create the Advantage+ ad sets** (TOF) with no budget (add `bid_amount` only if the account's bid strategy requires it — see below).
      4. **Add `daily_budget` to the Advantage+ campaigns** via `meta_ads_campaign_update`, now that their ad sets exist.
      5. **Create the ads** for each ad set.
      6. **Preview, then leave everything PAUSED** for user review.
      
      See [constraints.md](constraints.md) section 2 for the full CBO explanation.
      
      ---
      
      ## Bid strategy applies to every tier
      
      If the account's default bid strategy is `LOWEST_COST_WITH_BID_CAP`, **every** ad set across **every** tier will require a `bid_amount`, and the error will repeat per tier.
      
      > **CRITICAL**: If you hit "Bid Amount Required" (subcode 1815857), do NOT auto-fix with a placeholder bid and do NOT change the optimization goal. Stop and surface it to the user once — it affects the whole build, so resolve it before continuing the remaining tiers. See [constraints.md](constraints.md) section 14.
      
      ---
      
      ## Recovering from a partial failure
      
      Because each tier is built step by step, a mid-build failure leaves the earlier tiers already created. There is no automatic rollback — clean up manually before retrying:
      
      1. Note which campaigns/ad sets/ads were created before the failure (the responses you captured).
      2. Delete the incomplete artifacts with `meta_ads_campaign_delete` (deleting a campaign removes its ad sets and ads).
      3. Fix the root cause (validate audiences, resolve the bid strategy, correct budget mode).
      4. Rebuild from a clean state.
      
      This is exactly why the plan-first + audience-validation steps above matter: resolving CBO, bid strategy, and audience issues *before* the first tool call is what prevents a half-built funnel in the first place.
      
      ---
      
      ## Common failure points
      
      | Symptom | Cause | Fix |
      |---|---|---|
      | Ad set rejects `daily_budget` (CBO error) | Campaign created with a budget | Follow the build order above — campaigns first with no budget |
      | "Bid Amount Required" on every tier | Account bid strategy is bid-cap | Stop, surface once, resolve before building remaining tiers |
      | Built funnel doesn't match brief | Audience swapped or window mismatched silently | Validate + surface audience issues before building (step 1) |
      | Had to delete and rebuild campaigns | Budget mode decided after creation | Decide budget mode per tier in the plan, before any tool call |
      
  • SKILL.md 10.4 KB
    ---
    name: meta-ads
    description: Plan and create Meta (Facebook + Instagram) advertising campaigns end-to-end via the Hyper MCP, defaulting to Advantage+ automation. Use when the user wants to launch Meta ads, Facebook ads, Instagram ads, Advantage+ campaigns, carousel ads, dynamic creative ads, set up Meta conversion tracking, analyze performance, audit a Meta ads account, or build Meta performance dashboards. Also triggers on phrases like meta campaign, facebook campaign, advantage+, or meta account audit.
    requires_toolkits:
      - meta_ads
      - meta_business
    icon: meta_ads
    short_description: Plan and create Meta ad campaigns with Advantage+ defaults, audits, and dashboards.
    ---
    
    # Meta Ads
    
    Strategic guide for creating and managing Meta advertising campaigns, analyzing performance, and building dashboards from cached data. **Default to Advantage+** unless the user explicitly requests manual control.
    
    ## Out of scope — defer to other skills
    
    | Request | Send them to |
    | --- | --- |
    | Competitor or public ad research (Meta Ads Library) | `meta-ads-library` |
    | Ad creative generation (images, copy variants) | `ad-creative-generation` |
    | Google Ads campaigns | `google-ads` |
    | Pinterest / TikTok / Amazon paid campaigns | `pinterest-ads`, `tiktok-ads`, `amazon-ads` |
    
    ## Requirements
    
    - **Hyper MCP installed and connected.** [https://app.hyperfx.ai/mcp](https://app.hyperfx.ai/mcp)
    - **Meta Business integration connected** (Facebook + Instagram, with at least one ad account and one Page) at [https://app.hyperfx.ai/apps](https://app.hyperfx.ai/apps).
    - **Firecrawl integration connected** for site research and screenshot grounding (discovery phase).
    
    If `search("meta_ads_adaccount_list")` does not find `meta_ads_adaccount_list`, stop and tell the user to enable Hyper MCP and connect Meta Business.
    
    If you suspect a connection issue (missing ad accounts, page publishing failures, or permission errors), call `meta_ads_health_check()` and report the diagnostics before proceeding.
    
    ### How to run the tools in this skill
    
    Every tool in this skill is named by its canonical tool name. Run it with the call your surface gives you:
    
    | Surface | Find a tool | Run it |
    | --- | --- | --- |
    | MCP client (Claude, Cursor, Codex, ChatGPT) | `search("<what you want to do>")`, then `describe("<name>")` | `call("<name>", {...})` |
    | Hyper CLI | `hyperai search "<what you want to do>"`, then `hyperai describe <name>` | `hyperai call <name> --json '{...}'` |
    
    If a tool is not found, its integration is not connected or not enabled for the workspace: stop and tell the user which integration to connect.
    
    ## Tool names
    
    Use the **exact tool name from your connected tool list**. Canonical names are `meta_ads_*` (listed below). On Hyper platform chat, legacy `meta_business_*` names (e.g. `meta_ads_adaccount_list`) and the retired plural names (e.g. `meta_ads_campaign_create`) resolve to the same tools via aliases — if a call fails with "tool not found", search the live catalog for the canonical name.
    
    | Group | Tools |
    | --- | --- |
    | Discovery | `meta_ads_adaccount_list`, `meta_ads_owned_pages_list`, `meta_ads_pages_search`, `meta_accounts_list`, `meta_ads_instagram_accounts_list` |
    | Health & sync | `meta_ads_health_check`, `meta_ads_health_get` |
    | Tracking assets | `meta_ads_ad_pixels_list`, `meta_ads_ad_pixels_get`, `meta_ads_custom_audiences_list`, `meta_ads_lookalike_audiences_list`, `meta_ads_targeting_search` |
    | Step-by-step creation (preferred) | `meta_ads_campaign_create`, `meta_ads_adset_create`, `meta_ads_ad_create`, `meta_ads_ad_images_upload`, `meta_ads_creative_create` |
    | Read & preview | `meta_ads_campaign_get`, `meta_ads_campaigns_search`, `meta_ads_adset_list`, `meta_ads_ad_list`, `meta_ads_ad_get`, `meta_ads_ad_previews_get` |
    | Insights & dashboards | `meta_ads_insights_get`, `data_apps_build`, `database_query` |
    | Launch & edits | `meta_ads_campaigns_activate`, `meta_ads_campaign_update`, `meta_ads_adset_update`, `meta_ads_ad_update` |
    | Automated rules | `meta_ads_adrule_create`, `meta_ads_adrule_list`, `meta_ads_adrule_get`, `meta_ads_adrule_update`, `meta_ads_adrule_delete`, `meta_ads_adrule_history_list` |
    | Site research | `firecrawl_branding_extract`, `firecrawl_screenshots_create` |
    
    CLI users: translate tool names with the `hyper-cli` skill (`hyperai search "<tool name>"`).
    
    ---
    
    ## Rules that must never be forgotten
    
    > **BUDGETS IN CENTS**: $20.00 = 2000. $5.50 = 550. $100 = 10000. Never pass dollar amounts directly.
    
    > **ACTIVATE, DON'T UPDATE**: Use `meta_ads_campaigns_activate(campaign_id)` to go live. Never `meta_ads_campaign_update(status="ACTIVE")` — that silently leaves ad sets and ads PAUSED so nothing serves.
    
    > **ALWAYS START PAUSED**: Create campaigns with `status="PAUSED"`. Never launch live without user review.
    
    > **BUILD STEP BY STEP**: Create campaigns with the individual tools — `meta_ads_campaign_create` → `meta_ads_adset_create` → `meta_ads_creative_create` → `meta_ads_ad_create`, capturing each id from the previous response. (The old blueprint tools were removed.) The tools validate requests before sending — campaign objective rules, bid-strategy/bid-amount pairing, billing-event/optimization-goal compatibility, budget coherence — but objective-specific ad-set fields (`optimization_goal`, `promoted_object`) are YOUR responsibility: match them to the campaign objective using the reference file for the campaign type.
    
    > **REGULATED ADVERTISERS NEED `special_ad_categories`**: For gambling, financial, housing, employment, credit, or political advertisers, declare the category on `meta_ads_campaign_create` (e.g. `special_ad_categories=["ONLINE_GAMBLING_AND_GAMING"]`).
    
    > **FIXED RUN WINDOWS GO ON THE AD SET**: When the user gives a run duration or dates ("run it for 7 days", "through end of month"), set `start_time` AND `end_time` (ISO 8601) on `meta_ads_adset_create`. A daily-budget ad set with no `end_time` runs continuously until manually paused — the requested window is silently lost.
    
    > **GROUND COPY IN THE DESTINATION PAGE (RESEARCH FIRST)**: When the brief centers on a URL ("build a campaign for https://..."), fetch that page FIRST (`web_pages_fetch`) — before discovery and before writing any copy — and ground ad copy in what the page actually says (product name, value props, offer). Never invent copy for a page you have not read; even when the user supplies exact copy or headlines, fetch the page to verify the destination matches the offer.
    
    > **EU-TARGETED AD SETS NEED DSA FIELDS**: If an ad set targets the EU, set `dsa_beneficiary` and `dsa_payor` on `meta_ads_adset_create` (who benefits from / pays for the ad) — required under the EU Digital Services Act, or delivery is restricted.
    
    > **UTMs ON EVERY DESTINATION AD (`url_tags`)**: Set `url_tags` (UTM params, e.g. `utm_source=meta&utm_medium=paid&utm_campaign=...`) on every creative that drives to a destination — downstream measurement (e.g. AppsFlyer + a data warehouse) stitches on these, so an ad without UTMs is effectively unmeasurable. Use the advertiser's canonical template; if you don't have one, ask rather than ship untracked.
    
    See [references/constraints.md](references/constraints.md) for the full constraint set.
    
    > **All reference files live in `references/`.** Read them at `references/<file>` (e.g. `references/discovery.md`). They are not in the same directory as this SKILL.md.
    
    ---
    
    ## Core process
    
    Every task follows this sequence. Do not skip steps.
    
    1. **Identify the goal** — creation, analysis, or both?
    2. **Check the routing table** and read the referenced files before calling any tools
    3. **Make a written plan** — state campaign type, budget in cents, optimization goal, and sequence of steps; show it before acting
    4. **Execute step by step**, re-checking [references/constraints.md](references/constraints.md) at each creation step
    5. **Show ad previews** before activation
    6. **Activate only when the user approves** using `meta_ads_campaigns_activate()`
    
    **Automated / scheduled runs:** If there is no user present to interact with (e.g. a scheduled task), skip steps 3 and 6. Do not write a plan and wait for confirmation — proceed directly. Do not activate unless activation was explicitly included in the task instructions.
    
    ---
    
    ## Routing table
    
    | The user wants to… | Read these files first |
    |---|---|
    | Create a sales / conversion campaign | [references/discovery.md](references/discovery.md) → [references/campaigns/sales.md](references/campaigns/sales.md) |
    | Create a leads campaign | [references/discovery.md](references/discovery.md) → [references/campaigns/leads.md](references/campaigns/leads.md) |
    | Create a traffic campaign | [references/discovery.md](references/discovery.md) → [references/campaigns/traffic.md](references/campaigns/traffic.md) |
    | Create an awareness or engagement campaign | [references/discovery.md](references/discovery.md) → [references/campaigns/awareness-engagement.md](references/campaigns/awareness-engagement.md) |
    | Create an app promotion campaign | [references/discovery.md](references/discovery.md) → [references/campaigns/app-promotion.md](references/campaigns/app-promotion.md) |
    | Create a campaign (any objective) | [references/discovery.md](references/discovery.md) → the matching `references/campaigns/*.md` above, then build step by step |
    | Analyze performance / query insights | [references/analytics.md](references/analytics.md) |
    | Audit an account / find optimization opportunities | [references/account-audit.md](references/account-audit.md) |
    | Set up automated rules (auto-pause, budget guards, alerts) | [references/automated-rules.md](references/automated-rules.md) |
    | Build a Meta dashboard or data app | [references/analytics.md](references/analytics.md) → [references/dashboards.md](references/dashboards.md) |
    | Analyze performance, then create a campaign | [references/analytics.md](references/analytics.md) → [references/discovery.md](references/discovery.md) → relevant campaign file |
    | Build a funnel / multiple campaigns at once (TOF/MOF/BOF) | [references/multi-campaign-funnel.md](references/multi-campaign-funnel.md) → [references/discovery.md](references/discovery.md) → per-tier campaign files |
    | Objective not yet known | [references/discovery.md](references/discovery.md) — discovery clarifies the goal |
    
    ---
    
    ## Worked examples
    
    - Full sales campaign (ecommerce, Advantage+, step-by-step): [references/examples/sales-ecommerce.md](references/examples/sales-ecommerce.md)
    - Full leads campaign (B2B SaaS, website pixel, step-by-step): [references/examples/leads-form.md](references/examples/leads-form.md)
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related