
Meta Ads
- 91 installs
- 67 repo stars
- Updated August 4, 2026
- hyperfx-ai/marketing-skills
Helps with ai & agent building tasks.
About
meta-ads is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- meta-ads
- AI & Agent Building
- AI-coding skill
Meta Ads by the numbers
- 91 all-time installs (skills.sh)
- +9 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #4,765 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/hyperfx-ai/marketing-skills --skill meta-adsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 91 |
|---|---|
| repo stars | ★ 67 |
| Last updated | August 4, 2026 |
| Repository | hyperfx-ai/marketing-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
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/integrations.
- Firecrawl integration connected for site research and screenshot grounding (discovery phase).
If meta_ads_ad_accounts_list is not in the tool 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.
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_* aliases (e.g. meta_business_list_ad_accounts) resolve to the same tools — if a call fails with "tool not found", search the live catalog for the canonical name.
| Group | Tools |
|---|---|
| Discovery | meta_ads_ad_accounts_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_business_sync |
| 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_campaigns_create, meta_ads_ad_sets_create, meta_ads_create, meta_ads_ad_images_upload, meta_ads_ad_creatives_create |
| Blueprint path (avoid — see rule below) | meta_ads_blueprints_preview, meta_ads_campaign_blueprints_create |
| Read & preview | meta_ads_campaigns_get, meta_ads_ad_sets_list, meta_ads_list, meta_ads_get, meta_ads_ad_previews_get |
| Insights & dashboards | meta_ads_insights_get, meta_business_sync, hyper_data_list_dashboard_templates, hyper_data_build_dashboard, database_query |
| Launch & edits | meta_ads_campaigns_activate, meta_ads_campaigns_update, meta_ads_ad_sets_update, meta_ads_update |
| Site research | firecrawl_extract_branding, firecrawl_screenshot |
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: Usemeta_ads_campaigns_activate(campaign_id)to go live. Nevermeta_ads_campaigns_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, NOT VIA BLUEPRINT: Create campaigns with the individual tools —meta_ads_campaigns_create→meta_ads_ad_sets_create→meta_ads_ad_creatives_create→meta_ads_create, capturing each id from the previous response. Do NOT use the blueprint tools (meta_ads_blueprints_preview/meta_ads_campaign_blueprints_create). The step-by-step tools validate each object against its campaign objective (e.g. they enforce the correctoptimization_goalandpromoted_object), so mistakes are caught up front instead of silently producing an invalid campaign.
See references/constraints.md for the full constraint set.
All reference files live in `references/`. Read them atreferences/<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 |
| 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
- Full sales campaign (ecommerce, Advantage+, step-by-step): references/examples/sales-ecommerce.md
- Full leads campaign (B2B SaaS, website pixel, step-by-step): references/examples/leads-form.md
Meta Ads: Performance Analytics
Use this for querying performance data, ad-level insights, and historical reporting.
---
When to use cached data vs the Meta API
| Situation | Use |
|---|---|
| User asks for a performance check or trend | Cached data first (faster, no rate limits) |
| User needs fields not in the cache | Meta API insights tools |
| Cache is stale or user explicitly requests fresh data | Call meta_business_sync, then query cache |
| No cached data exists yet for this account | Meta API as fallback |
| Campaign was just created in this session | Meta API directly — new campaigns are NOT in cache yet |
Important: The cache is not real-time. If the user just created a campaign in this conversation, it won't appear in cached data. Usemeta_ads_insights_getormeta_ads_campaigns_getdirectly for anything created in the current session.
Check the cache's latest date before trusting it for a recent window. The cache can lag by weeks or months. If the user asks for "last 30 days" but the cache's most recent row is older than that window, the cache cannot answer the question — go straight tometa_ads_insights_getfor live data. Always confirm the max date in the cache (e.g.SELECT MAX(date_start) FROM <table>) before relying on it for a time-bounded request.
Check the toolkit context for the cached table name and last sync timestamp before calling any API tools.
---
Querying insights via the Meta API
Account-level campaign summary
{
"object_id": "act_123456789",
"object_type": "account",
"level": "campaign",
"date_preset": "last_30d",
"include_actions": true
}Ad set breakdown
{
"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.
{
"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_maximumThese 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:
{
"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 cached data
Use the integration-scoped table name from the toolkit context. Query cached data with database_query (the canonical SQL tool; hyper_data_sql is a legacy alias and may not be exposed under that name). There is no other standalone raw-SQL tool — for dashboards, route SQL through hyper_data_build_dashboard's sql_data_sources instead.
Daily spend trend
SELECT
campaign_name,
date_start,
SUM(spend) AS total_spend,
SUM(impressions) AS total_impressions
FROM <cached_table>
GROUP BY campaign_name, date_start
ORDER BY date_start DESC
LIMIT 100;Campaign performance summary
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 <cached_table>
GROUP BY campaign_name
ORDER BY spend DESC;Ad set cost-per-conversion
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 <cached_table>
GROUP BY adset_name
ORDER BY spend DESC;Prefer cached data for reporting and dashboards. Use the Meta API directly only when a required field is missing from the cache, or for campaigns created in this session.
---
Cache refresh
Data syncs automatically every 30 minutes.
If the data appears stale or the user requests a refresh: call meta_business_sync with no parameters. This is a background operation — do not wait for completion.
---
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_campaigns_get and meta_ads_ad_sets_list, then build the new campaign via the matching objective workflow (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-derivepromoted_objectyourself: look up the pixel withmeta_ads_ad_pixels_list, infer thecustom_event_typefrom 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_campaigns_get, meta_ads_ad_sets_list, meta_ads_list. For the creative, meta_ads_ad_creatives_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_ad_creatives_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_list / meta_ads_ad_creatives_get). 5. Only then delete old draft creatives with meta_ads_ad_creatives_delete (or meta_ads_delete for ads). Never delete the source until the replacement is confirmed.
Leave the new campaign PAUSED unless the user said to activate.
Meta Ads: Blueprint Creation (Preferred Path)
Use the blueprint system whenever possible for new campaigns. It validates locally, fills smart defaults (optimization goal, bid strategy, page/pixel IDs), and rolls back on failure.
Manual step-by-step creation is the fallback when the blueprint system is insufficient (complex creative overrides, replication workflows, or per-step debugging). See the objective files under reference/campaigns/.
---
Workflow: Preview → Confirm → Create
1. Build the blueprint JSON from discovery research (discovery.md). 2. Call meta_ads_blueprints_preview(blueprint={...}) to validate and show the user. 3. Get explicit user approval (skip when running a scheduled task with activation already specified). 4. Call meta_ads_campaign_blueprints_create(blueprint={...}).
---
Blueprint structure
{
"account_id": "act_1234567890",
"name": "Campaign Name",
"objective": "OUTCOME_SALES",
"campaign_type": "advantage_plus",
"daily_budget": 2000,
"status": "PAUSED",
"url_tags": "utm_source=meta&utm_medium=paid",
"ad_sets": [
{
"name": "US Broad Audience",
"targeting": {
"geo_locations": {"countries": ["US"]},
"targeting_automation": {"advantage_audience": 1}
},
"pixel_id": null,
"page_id": null,
"custom_event_type": "PURCHASE",
"ads": [
{
"name": "Single Image Ad",
"link": "https://example.com",
"primary_text": "Your ad copy here",
"headline": "Headline here",
"description": "Description here",
"image_hash": "abc123",
"call_to_action": "SHOP_NOW"
}
]
}
]
}All budgets in cents. $20.00 = 2000.
---
Blueprint features
| Feature | Details |
|---|---|
| Smart defaults | Optimization goal, billing event, destination type auto-filled from objective. |
| Auto-resolution | page_id, pixel_id, instagram_user_id auto-resolved when only one exists. |
| Objective validation | Per-objective rules enforce required fields, allowed values, promoted objects. |
| Budget placement | CBO (campaign level) vs ABO (ad set level) auto-detected. |
| Cleanup on failure | Reports created, failed, and remaining for recovery. |
---
Creative formats in blueprints
Single image/video — standard link, primary_text, headline, image_hash, call_to_action.
Carousel (2–10 cards) — use carousel_cards array with per-card image_hash, headline, link.
Dynamic creative — use text_variations with arrays for primary_texts, headlines, descriptions, plus image_hash.
---
Advantage+ vs manual in blueprints
- Advantage+:
campaign_type: "advantage_plus", budget at campaign level, broad targeting withtargeting_automationinsidetargeting. - Manual:
campaign_type: "manual", budget at ad set level, detailed targeting allowed.
---
Creative asset preparation
Before building the blueprint:
1. Capture a website screenshot: firecrawl_screenshot on the landing page URL. 2. Generate an ad image using the screenshot as visual reference (see ad-creative-generation skill). 3. Upload: meta_ads_ad_images_upload(account_id="<account_id>", image_url="<url>") → image_hash. 4. Use image_hash in blueprint ad objects.
---
Previews after blueprint creation
1. For each ad ID returned, fetch the creative ID: meta_ads_get(ad_id="<ad_id>") → creative.id. 2. Call meta_ads_ad_previews_get(creative_ids=["<creative_id>"]). 3. Summarize which preview formats succeeded/failed — never paste iframe/html snippets in chat. 4. Activate only after user approval: meta_ads_campaigns_activate(campaign_id="<id>") — not meta_ads_campaigns_update(status="ACTIVE").
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. Most relevant:
- Budget in cents (×100)
promoted_objectrequired for OUTCOME_APP_PROMOTION — missing it causes a cryptic API errortargeting_automationinsidetargeting- Budget at campaign level for Advantage+
meta_ads_ad_sets_createusesmode+input_data— all fields insideinput_datameta_ads_createtakes a singleinput_datadict — no separate top-level args- Use
meta_ads_campaigns_activate(), notupdate_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://)
- [ ] Budget confirmed and converted to cents
- [ ] Page ID captured explicitly from discovery
- [ ] Creative assets ready or will use app store screenshots
1. Create campaign
meta_ads_campaigns_create(
account_id="act_123456789",
name="App Installs - [App Name] - [Date]",
objective="OUTCOME_APP_PROMOTION",
status="PAUSED",
daily_budget=2000 # $20/day in cents — Advantage+ only; omit for manual
)→ Capture campaign_id.
2. Create ad set
meta_ads_ad_sets_createuses amode+input_datapattern. Every ad set field goes insideinput_data.
Advantage+ (default):
{
"mode": "advantage_plus",
"input_data": {
"account_id": "act_123456789",
"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_objectwith bothapplication_idANDobject_store_urlis 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):
{
"mode": "manual",
"input_data": {
"account_id": "act_123456789",
"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.
3. Upload image
meta_ads_ad_images_upload(account_id="act_123456789", image_url="<url>")→ Capture image_hash.
4. Create ad
CRITICAL: meta_ads_create takes a single `input_data` dict. No separate top-level args.{
"input_data": {
"account_id": "act_123456789",
"name": "App Install Ad - [Creative]",
"adset_id": "<adset_id>",
"creative": {
"object_story_spec": {
"page_id": "<page_id>",
"link_data": {
"link": "https://apps.apple.com/app/example/id123456789",
"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
# 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 |
|---|---|---|
| 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 input_data | All fields must be inside input_data dict |
| Campaign ACTIVE but no installs | Used update_campaign(status="ACTIVE") | Use meta_ads_campaigns_activate() |
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. Most relevant:
- Budget in cents (×100)
targeting_automationinsidetargeting- Budget at campaign level for Advantage+
meta_ads_ad_sets_createusesmode+input_data— all fields insideinput_datameta_ads_createtakes a singleinput_datadict — no separate top-level args- Use
meta_ads_campaigns_activate(), notupdate_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:
meta_ads_campaigns_create(
account_id="act_123456789",
name="Awareness - [Brand] - [Date]",
objective="OUTCOME_AWARENESS",
status="PAUSED",
daily_budget=2000
)Engagement:
meta_ads_campaigns_create(
account_id="act_123456789",
name="Engagement - [Brand] - [Date]",
objective="OUTCOME_ENGAGEMENT",
status="PAUSED",
daily_budget=2000
)→ Capture campaign_id.
2. Create ad set
meta_ads_ad_sets_createuses amode+input_datapattern. Every ad set field goes insideinput_data.
Advantage+ awareness:
{
"mode": "advantage_plus",
"input_data": {
"account_id": "act_123456789",
"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:
{
"mode": "advantage_plus",
"input_data": {
"account_id": "act_123456789",
"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
meta_ads_ad_images_upload(account_id="act_123456789", image_url="<url>")→ Capture image_hash.
4. Create ad
CRITICAL: meta_ads_create takes a single `input_data` dict. No separate top-level args.{
"input_data": {
"account_id": "act_123456789",
"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
# 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)
{
"mode": "manual",
"input_data": {
"account_id": "act_123456789",
"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 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. Every rule there applies here. Most relevant:
- Budget in cents (×100)
promoted_objectrequired for OUTCOME_LEADS — missing it, or missing any required field within it, causes a validation errortargeting_automationinsidetargeting- Budget at campaign level for Advantage+
meta_ads_ad_sets_createusesmode+input_data— all fields insideinput_datameta_ads_createtakes a singleinput_datadict — no separate top-level args- Use
meta_ads_campaigns_activate(), notupdate_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_ad_creatives_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_listfails - [ ] 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
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
meta_ads_campaigns_create(
account_id="act_123456789",
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)
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_ad_sets_createuses amode+input_datapattern. Every ad set field goes insideinput_data.
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 addbid_amountor changeoptimization_goalas workarounds. See constraints.md section 14.
Advantage+ — website pixel path:
{
"mode": "advantage_plus",
"input_data": {
"account_id": "act_123456789",
"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_objectrequirespixel_id,custom_event_type, andpage_idfor the website pixel path. The local validator will reject the call if any of these three fields is missing.
Advantage+ — native lead form path:
{
"mode": "advantage_plus",
"input_data": {
"account_id": "act_123456789",
"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_automationis insidetargeting, 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):
{
"mode": "manual",
"input_data": {
"account_id": "act_123456789",
"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'sLEAD_GENERATIONoptimization 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:
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
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_ad_creatives_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.
meta_ads_ad_creatives_create(
account_id="act_123456789",
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. Usedescriptioninstead.page_id— must be at the top level ofobject_story_spec, not insidelink_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_ad_creatives_list returns link_url: null in its list response, so you cannot verify the destination from the list alone. Call meta_ads_ad_creatives_get(creative_id) on the selected creative to retrieve the actual link_url before attaching it:
meta_ads_ad_creatives_get(creative_id="33892633203717215")
# Check the returned link_url matches the user's intended landing pageIf 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_ad_creatives_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_ad_creatives_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_create takes a single `input_data` dict. No separate top-level args.With new inline creative (image_hash):
{
"input_data": {
"account_id": "act_123456789",
"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):
{
"input_data": {
"account_id": "act_123456789",
"name": "Lead Ad - [Creative Name]",
"adset_id": "<adset_id>",
"creative": {
"creative_id": "<creative_id>"
}
}
}Omitstatus— ads default to PAUSED. Never passstatus="ACTIVE"on creation.
→ Capture ad_id from the response.
7. Preview and activate
# 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.
meta_ads_campaigns_activate(campaign_id="<campaign_id>")
# Activates campaign + ad sets + ads together
# NOT: meta_ads_campaigns_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:
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.
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. Every rule there applies here. Most relevant:
- Budget in cents (×100)
promoted_objectrequired for OUTCOME_SALEStargeting_automationinsidetargeting- Budget at campaign level for Advantage+
meta_ads_ad_sets_createusesmode+input_data— all fields insideinput_datameta_ads_createtakes a singleinput_datadict — no separate top-level args- Use
meta_ads_campaigns_activate(), notupdate_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 firstDefault 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
meta_ads_campaigns_create(
account_id="act_123456789",
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
meta_ads_ad_pixels_list(account_id="act_123456789")→ Capture pixel_id. Required for promoted_object.
3. Create ad set
meta_ads_ad_sets_createuses amode+input_datapattern. Every ad set field goes insideinput_data.
Advantage+ (default):
{
"mode": "advantage_plus",
"input_data": {
"account_id": "act_123456789",
"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_automationis insidetargeting, 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):
{
"mode": "manual",
"input_data": {
"account_id": "act_123456789",
"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
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_createtakes a single `input_data` dict containing all fields. Do not passaccount_id,name,adset_idas separate top-level arguments and do not passinput_dataas a JSON string.
❌ WRONG: meta_ads_create(account_id="act_...", adset_id="123", name="My Ad")
❌ WRONG: meta_ads_create(input_data='{"account_id": "act_..."}')
✅ RIGHT: meta_ads_create(input_data={"account_id": "act_...", "adset_id": "123", ...}){
"input_data": {
"account_id": "act_123456789",
"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>"
}
}
}
}
}Omitstatus— ads default to PAUSED. Never passstatus="ACTIVE"on creation.
→ Capture ad_id from the response.
6. Preview and activate
# 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_campaigns_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_create also supports carousel and dynamic creative natively — just change the creative block.
Carousel (2–10 cards) — use child_attachments inside link_data:
{
"input_data": {
"account_id": "act_123456789",
"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:
{
"input_data": {
"account_id": "act_123456789",
"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_formatsis 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 minimalobject_story_specwith justpage_idalongsideasset_feed_spec(the page_id comes from there).
- The ad set must be created withis_dynamic_creative: true— pass it tometa_ads_ad_sets_create.
>
For non-dynamic ads,creativeuses a singleobject_story_spec(orcreative_idto 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 input_data | All fields must be inside input_data dict |
| Ad creation error | Separate top-level args used | Use single input_data dict |
| 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.
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. Most relevant:
- Budget in cents (×100)
targeting_automationinsidetargeting- Budget at campaign level for Advantage+
meta_ads_ad_sets_createusesmode+input_data— all fields insideinput_datameta_ads_createtakes a singleinput_datadict — no separate top-level args- Use
meta_ads_campaigns_activate(), notupdate_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
meta_ads_campaigns_create(
account_id="act_123456789",
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_ad_sets_createuses amode+input_datapattern. Every ad set field goes insideinput_data.
Advantage+ (default):
{
"mode": "advantage_plus",
"input_data": {
"account_id": "act_123456789",
"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):
{
"mode": "manual",
"input_data": {
"account_id": "act_123456789",
"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? Passbid_strategyas a top-level param (not ininput_data) andbid_amountinsideinput_data— see ../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 section 17.→ Capture adset_id from the response.
3. Upload image
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_create takes a single `input_data` dict. No separate top-level args.{
"input_data": {
"account_id": "act_123456789",
"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
# 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 input_data | All fields must be inside input_data 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 |
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/dayApplies to: daily_budget, lifetime_budget, bid_amount, and any other budget field.
Wrong field names that will cause Pydantic errors:
daily_budget_cents→ usedaily_budgetbudget→ usedaily_budgetorlifetime_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 suggestsdaily_spend_cap/lifetime_spend_cap. Do not chase that — those fields are NOT in theAdSetCreateInputschema 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_campaigns_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.
---
3. Activation: use activate(), not update()
# WRONG — only flips the campaign flag; ad sets and ads stay PAUSED → nothing serves
meta_ads_campaigns_update(campaign_id, 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.
// 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+ silently overrides narrow age bands
When advantage_audience: 1, the tool clamps age_min to ≤25 and age_max to ≥65 (Meta rejects anything narrower for Advantage+). A request like "Advantage+, ages 25–54" is silently widened to 25–65 — the user does NOT get 25–54, and no error is raised.
If a strict age band matters to the user, do not use Advantage+ for that ad set. Use manual targeting (mode="manual", no advantage_audience), where age_min/age_max are honored exactly. Surface the trade-off: Advantage+ audience expansion vs. a precise age band — you can't have both.
---
6. create_ad_set: mode + input_data pattern
meta_ads_ad_sets_create uses a different calling pattern from meta_ads_campaigns_create. Every ad set field goes inside input_data.
# WRONG — flat params like create_campaign
meta_ads_ad_sets_create(account_id="act_...", campaign_id="123", name="...")
# RIGHT — mode + input_data
meta_ads_ad_sets_create(
mode="advantage_plus",
input_data={"account_id": "act_...", "campaign_id": "123", "name": "...", ...}
)Prefer `meta_ads_ad_sets_create` with `mode` — it's the unified path the rest of this skill uses. Themeta_ads_advantage_plus_ad_sets_create/meta_ads_manual_ad_sets_createvariants do the same job but take flat positional arguments instead ofmode+input_data. They aren't broken or deprecated, but if you reach for one and apply theinput_datashape you learned here, it fails with "missing required positional arguments." Stick tometa_ads_ad_sets_createso there's only one calling convention to track.
---
7. create_ad: single input_data dict
meta_ads_create takes a single `input_data` parameter — a dict containing all fields. Do not pass account_id, name, adset_id etc. as separate top-level arguments.
❌ WRONG: meta_ads_create(account_id="act_...", adset_id="123", name="My Ad")
❌ WRONG: meta_ads_create(input_data='{"account_id": "act_..."}') # string not dict
✅ RIGHT: meta_ads_create(input_data={"account_id": "act_...", "adset_id": "123", ...})object_story_spec / link_data fields (applies to every objective)
When building an inline creative (creative.object_story_spec):
page_idgoes at the top level ofobject_story_spec— never insidelink_data. Omitting it → "Facebook Page is Missing."
Valid link_data fields: message, link, description, picture, image_hash, call_to_action, name, caption.
Common footguns:
headlineis not a validlink_datafield → "field headline is not supported." Put the headline text inname(ordescription).captionmust 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"} |
---
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
{
"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.
# After meta_ads_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_campaigns_update | campaign_id, status, daily_budget |
| Ad set status / budget / targeting | meta_ads_ad_sets_update | ad_set_id, status, daily_budget, targeting |
| Ad status / creative | meta_ads_update | ad_id, status |
| Take everything live at once | meta_ads_campaigns_activate | campaign_id |
Before updating, fetch current state with meta_ads_campaigns_get or meta_ads_ad_sets_get — do not guess at current values.
To pause: meta_ads_campaigns_update(campaign_id="...", status="PAUSED") To increase budget: meta_ads_campaigns_update(campaign_id="...", 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 is a top-level parameter on meta_ads_ad_sets_create (and meta_ads_campaigns_create) — NOT a field inside input_data. Putting it inside input_data causes a Pydantic error. bid_amount (in cents) goes inside input_data.
When the user explicitly asks for a bid cap, set it and proceed — do not stop and ask (they already told you):
meta_ads_ad_sets_create(
mode="manual",
bid_strategy="LOWEST_COST_WITH_BID_CAP", # top-level param
input_data={
"account_id": "act_...",
"campaign_id": "...",
"name": "...",
"optimization_goal": "LINK_CLICKS",
"billing_event": "IMPRESSIONS",
"daily_budget": 7500, # $75/day, ad-set level for manual
"bid_amount": 250, # $2.50 cap, inside input_data
"targeting": { ... }
}
)LOWEST_COST_WITH_BID_CAPandCOST_CAPrequirebid_amount.- For Advantage+ / CBO,
bid_strategybelongs on the campaign (create_campaign) andbid_amounton 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:
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:
"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 IDexclusions.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 ← REMOVEDThis 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_create. It must be created as a standalone creative first.
Two-step pattern (required):
# Step 1 — create the dynamic creative with asset_feed_spec
creative = meta_ads_ad_creatives_create(
account_id="act_...",
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_create(input_data={
"account_id": "act_...",
"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_create's creative dict is not supported — the inline creative spec only accepts object_story_spec or creative_id.
Meta Ads: Dashboard Building
When the user asks for a Meta dashboard or performance report interface, follow the preset-first workflow below.
---
Workflow: preset first, custom second
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. Check for dashboard templates
Call hyper_data_list_dashboard_templates. If a suitable preset exists (e.g. meta_business_performance), use it:
hyper_data_build_dashboard(
name="Meta Performance Dashboard",
template_id="meta_business_performance"
)Preset templates cover the most common reporting views and require no custom SQL.
3. Custom dashboard only when needed
If no preset covers the user's requirements, build a custom dashboard using hyper_data_build_dashboard with tool_data_sources and sql_data_sources (see the custom pattern below). Inspect the live tool schema before calling — do not invent dashboard patterns.
4. Cache refresh policy
Data syncs automatically every 30 minutes. If data is stale or the user requests a refresh, call meta_business_sync with no parameters. This is a background refresh — do not wait for completion.
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.
hyper_data_build_dashboard(
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.
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_ad_accounts_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:
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_listreturns all audiences in the account, not just lookalikes. Filter bysubtype/ name yourself.
Validate any audience before using it. A discovered audience is not necessarily usable. Check before building the ad set:
operation_status.codemust be200(Normal). Code433means the audience is broken ("couldn't create, delete and retry").delivery_status.codemust be200(ready). Code300means too small to deliver.approximate_countshould 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_screenshot 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_extract_branding 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
- Leads → campaigns/leads.md
- Traffic → campaigns/traffic.md
- Awareness/Engagement → campaigns/awareness-engagement.md
- App Promotion → campaigns/app-promotion.md
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_ad_accounts_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_screenshot)
5. Extract branding (firecrawl_extract_branding)
6. Generate ad creative from screenshot
7. Upload image → get image_hash (meta_ads_ad_images_upload)
8. Create campaign → campaign_id (meta_ads_campaigns_create)
9. Get Pixel ID → pixel_id (meta_ads_ad_pixels_list)
10. Create ad set → adset_id (meta_ads_ad_sets_create)
11. Create ad → ad_id (meta_ads_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 uses mode + input_data
- create_ad uses single input_data dict
- Will use meta_ads_campaigns_activate(), not update(status="ACTIVE")---
Step 4: Execution
Discovery
meta_ads_ad_accounts_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 PASSResearch
firecrawl_screenshot({"url": "https://example.com/demo"})
# → Screenshot: dark navy, "Book a Demo" hero CTA, B2B product screenshots
firecrawl_extract_branding({"url": "https://example.com"})
# → Colors: #0D1B2A (navy), #00C2FF (electric blue)
# → Font: Inter | Tone: professional, direct, B2BCreative
meta_ads_ad_images_upload({"account_id": "act_111222333", "image_url": "<generated_image_url>"})
# → image_hash: xyz789abc012Campaign creation (step-by-step)
# Step 1: Campaign
meta_ads_campaigns_create(
account_id="act_111222333",
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_ad_sets_create(
mode="advantage_plus",
input_data={
"account_id": "act_111222333",
"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_create(
input_data={
"account_id": "act_111222333",
"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: 556677889Preview and summary
# 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 inlineCampaign 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)
# RIGHT
meta_ads_campaigns_activate(campaign_id="987654321")
# WRONG — do not use
# meta_ads_campaigns_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.
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_ad_accounts_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_screenshot)
5. Extract branding (firecrawl_extract_branding)
6. Generate ad creative from screenshot
7. Upload image → get image_hash (meta_ads_ad_images_upload)
8. Create campaign → campaign_id (meta_ads_campaigns_create)
9. Get Pixel ID → pixel_id (meta_ads_ad_pixels_list)
10. Create ad set → adset_id (meta_ads_ad_sets_create)
11. Create ad → ad_id (meta_ads_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 uses mode + input_data
- create_ad uses single input_data dict
- Will use meta_ads_campaigns_activate(), not update(status="ACTIVE")---
Step 4: Execution
Discovery
meta_ads_ad_accounts_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 PASSResearch
firecrawl_screenshot({"url": "https://skinfirst.com"})
# → Screenshot: clean minimal layout, soft pastel palette, hero product shot
firecrawl_extract_branding({"url": "https://skinfirst.com"})
# → Colors: #F5EDE4 (cream), #9B7D6E (warm brown)
# → Font: Playfair Display | Tone: elegant, naturalCreative
# Generate ad image using screenshot as visual reference
meta_ads_ad_images_upload({"account_id": "act_111222333", "image_url": "<generated_image_url>"})
# → image_hash: abc123def456Campaign creation (step-by-step)
# Step 1: Campaign
meta_ads_campaigns_create(
account_id="act_111222333",
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_ad_sets_create(
mode="advantage_plus",
input_data={
"account_id": "act_111222333",
"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_create(
input_data={
"account_id": "act_111222333",
"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: 112233445Preview and summary
# 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 inlineCampaign 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)
# RIGHT — activates campaign + ad sets + ads
meta_ads_campaigns_activate(campaign_id="987654321")
# WRONG — do not use this
# meta_ads_campaigns_update(campaign_id="987654321", status="ACTIVE")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
- Traffic tier → campaigns/traffic.md
- Sales tier → campaigns/sales.md
- Leads tier → 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 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 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_campaigns_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 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 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_campaigns_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 |