Create and publish ads to Meta (Facebook/Instagram) Ads Manager via the Marketing API — campaigns, ad sets, ads, image upload, carousel + video-placeholder support, all created PAUSED for human review. Runs a mandatory holistic pre-publish review and dual user confirmation. Use after /creative_director has prepared creatives, copy, and landing pages. Triggers (EN): "publish to Meta", "create Meta ads", "push ads to Ads Manager", "launch the campaign on Facebook/Instagram", "/meta_ads_publishe...
Pro shows the line behind each finding and how to fix it
Scanned 10/2/2026
npx -y skills add agentik-os/claude-code-skills --skill meta_ads_publisher --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Meta Ads Publisher?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/agentik-os-meta-ads-publisher)More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.
---
name: meta_ads_publisher
description: Create and publish ads to Meta (Facebook/Instagram) Ads Manager via the Marketing API — campaigns, ad sets, ads, image upload, carousel + video-placeholder support, all created PAUSED for human review. Runs a mandatory holistic pre-publish review and dual user confirmation. Use after /creative_director has prepared creatives, copy, and landing pages. Triggers (EN): "publish to Meta", "create Meta ads", "push ads to Ads Manager", "launch the campaign on Facebook/Instagram", "/meta_ads_publisher". Triggers (FR): "publie sur Meta", "crée les pubs Meta", "lance la campagne Facebook/Instagram", "envoie les pubs dans Ads Manager", "mets les pubs en ligne (en pause)".
---
# Meta Ads Publisher
Publish campaign assets to Meta Ads Manager via Marketing API.
## Single-execution craft (do NOT parallelize)
This skill performs ONE stateful, sequential publish operation — it is not a fan-out audit.
The chain is strictly ordered and each step's output is the next step's input:
**campaign → `campaign_id` → ad set → `adset_id` → upload image → `image_hash` → ad.**
- **NEVER run parallel agents against the same ad account.** Concurrent writers race on
the same campaign/ad-set state and create duplicate or orphaned objects (R-SCOPE: one writer
per ad account). If multiple ads are needed, create them serially within the single ad set.
- **Craft upgrades (apply every run):**
1. **Idempotency** — before creating a campaign, list existing campaigns
(`GET /act_{AD_ACCOUNT}/campaigns?fields=name,status`) and reuse/abort on an exact-name
match instead of silently creating a duplicate.
2. **Copy ↔ creative fidelity** — the ad copy, CTA, and landing URL must be carried verbatim
from the /creative_director output; never paraphrase, re-write, or "improve" approved copy
at publish time. This skill is a faithful courier, not an editor.
3. **Fail loud, fail paused** — if any API call errors mid-chain, STOP, report the exact Graph
API error + the IDs already created, and never proceed to the next step on a guess.
> **Portability note (VPS-only infra):** credentials below assume a local `.env` and the OmegaOS
> `TOOLS.md`. On any other host, export `META_AD_ACCOUNT_ID`, `META_PAGE_ID`, `META_ACCESS_TOKEN`
> in the environment (or your secret manager) — the curl logic is host-agnostic and needs no change.
## Prerequisites
- Meta Marketing API token (system user with ads_management permission)
- Ad Account ID
- Facebook Page ID
- Payment method on ad account (required by API)
- Approved creatives from /creative_director
## Credentials
Set these in your `.env` file:
```
META_AD_ACCOUNT_ID=act_your_account_id
META_PAGE_ID=your_facebook_page_id
META_ACCESS_TOKEN=your_access_token
```
See TOOLS.md for how to obtain these credentials.
## Workflow
### 1. Gather Assets
Before publishing, confirm you have:
- [ ] Campaign name and objective
- [ ] Ad set targeting (geo, age, interests)
- [ ] Daily/lifetime budget
- [ ] Image files (uploaded or local paths)
- [ ] Ad copy for each creative
- [ ] Landing page URLs
- [ ] CTA type (LEARN_MORE, SIGN_UP, etc.)
### 2. Pre-Publish Holistic Review ⚠️ MANDATORY
**Before creating ANY ads, review the full picture:**
```
┌─────────────────────────────────────────────────────────┐
│ HOLISTIC REVIEW CHECKLIST │
├─────────────────────────────────────────────────────────┤
│ □ Ad copy matches creative visuals │
│ □ Landing page URL is correct for each ad │
│ □ CTA matches the funnel stage (TOFU/MOFU/BOFU) │
│ □ Targeting makes sense for the offer │
│ □ Budget is appropriate for test/scale phase │
│ □ All ads in set are thematically consistent │
│ □ No typos or broken links │
│ □ DSA compliance fields filled (EU targeting) │
│ □ Image dimensions correct (1:1, 4:5, etc.) │
│ □ Video placeholders have complete copy │
└─────────────────────────────────────────────────────────┘
```
**Present summary to user:**
```markdown
## 📋 Pre-Publish Review
**Campaign:** [name]
**Objective:** [TRAFFIC/LEADS/etc]
**Budget:** €[X]/day
**Ad Set:** [name]
**Targeting:** [countries], ages [X-Y]
**Ads to create:**
| # | Name | Type | Landing Page | CTA |
|---|------|------|--------------|-----|
| 1 | ... | Image | /path | LEARN_MORE |
| 2 | ... | Video | /path | SIGN_UP |
**Copy preview:**
> [First 100 chars of each ad's copy...]
✅ All checks passed. Ready to publish?
```
**Wait for user confirmation before proceeding.**
### 3. Create Campaign
```bash
curl -X POST "https://graph.facebook.com/v21.0/act_{AD_ACCOUNT}/campaigns" \
-d "name=Campaign Name" \
-d "objective=OUTCOME_TRAFFIC" \
-d "status=PAUSED" \
-d "special_ad_categories=[]" \
-d "is_adset_budget_sharing_enabled=false" \
-d "access_token=$TOKEN"
```
**Objectives:**
- `OUTCOME_AWARENESS` — Brand awareness
- `OUTCOME_TRAFFIC` — Website traffic
- `OUTCOME_ENGAGEMENT` — Post engagement
- `OUTCOME_LEADS` — Lead generation
- `OUTCOME_SALES` — Conversions
### 4. Create Ad Set
```bash
curl -X POST "https://graph.facebook.com/v21.0/act_{AD_ACCOUNT}/adsets" \
-d "name=Ad Set Name" \
-d "campaign_id={CAMPAIGN_ID}" \
-d "status=PAUSED" \
-d "billing_event=IMPRESSIONS" \
-d "optimization_goal=LINK_CLICKS" \
-d "bid_strategy=LOWEST_COST_WITHOUT_CAP" \
-d "daily_budget=2000" \
-d 'targeting={"geo_locations":{"countries":["DE","AT","CH"]},"age_min":25,"age_max":55,"targeting_automation":{"advantage_audience":0}}' \
-d "dsa_beneficiary=Company Name" \
-d "dsa_payor=Company Name" \
-d "access_token=$TOKEN"
```
**Required for EU targeting:**
- `dsa_beneficiary` — Who benefits from the ads
- `dsa_payor` — Who pays for the ads
- `targeting_automation.advantage_audience` — 0 to disable, 1 to enable
### 5. Upload Images
```bash
curl -X POST "https://graph.facebook.com/v21.0/act_{AD_ACCOUNT}/adimages" \
-F "filename=@/path/to/image.png" \
-F "access_token=$TOKEN"
```
Returns `image_hash` for use in ad creative.
### 6. Create Ads
**Single Image Ad:**
```bash
curl -X POST "https://graph.facebook.com/v21.0/act_{AD_ACCOUNT}/ads" \
-d "name=Ad Name" \
-d "adset_id={ADSET_ID}" \
-d "status=PAUSED" \
-d 'creative={"object_story_spec":{"page_id":"{PAGE_ID}","link_data":{"image_hash":"{HASH}","link":"https://...","message":"Ad copy here","call_to_action":{"type":"LEARN_MORE"}}}}' \
-d "access_token=$TOKEN"
```
**Carousel Ad:**
```bash
curl -X POST "https://graph.facebook.com/v21.0/act_{AD_ACCOUNT}/ads" \
-d "name=Carousel Ad" \
-d "adset_id={ADSET_ID}" \
-d "status=PAUSED" \
-d 'creative={"object_story_spec":{"page_id":"{PAGE_ID}","link_data":{"message":"Main copy","link":"https://...","child_attachments":[{"link":"...","image_hash":"...","name":"Slide title","description":"Slide desc"}],"multi_share_optimized":true}}}' \
-d "access_token=$TOKEN"
```
**Video Ad (placeholder — copy only, no video):**
Create as link ad with copy. Video uploaded later:
```bash
curl -X POST "https://graph.facebook.com/v21.0/act_{AD_ACCOUNT}/ads" \
-d "name=VIDEO PLACEHOLDER - Name" \
-d "adset_id={ADSET_ID}" \
-d "status=PAUSED" \
-d 'creative={"object_story_spec":{"page_id":"{PAGE_ID}","link_data":{"link":"https://...","message":"Video script as ad copy","name":"Headline","description":"Subhead","call_to_action":{"type":"LEARN_MORE"}}}}' \
-d "access_token=$TOKEN"
```
### 7. Post-Publish Confirmation
After creating all ads, provide summary:
```markdown
## ✅ Ads Published to Meta
**Campaign:** [name] (ID: xxx) — PAUSED
**Ad Set:** [name] (ID: xxx) — PAUSED
**Ads Created:**
| # | Name | Type | ID | Status |
|---|------|------|-----|--------|
| 1 | ... | Image | xxx | PAUSED |
| 2 | ... | Carousel | xxx | PAUSED |
| 3 | ... | Video Placeholder | xxx | PAUSED |
**Review in Ads Manager:**
https://www.facebook.com/adsmanager/manage/ads?act={AD_ACCOUNT}
**Next steps:**
1. Review ads in Ads Manager
2. Upload videos for placeholder ads
3. When ready, set status to ACTIVE
```
## CTA Types
| Type | Use Case |
|------|----------|
| LEARN_MORE | General info, tutorials |
| SIGN_UP | Email capture, courses |
| DOWNLOAD | Lead magnets, PDFs |
| GET_OFFER | Promotions, discounts |
| BOOK_NOW | Consultations, calls |
| CONTACT_US | B2B inquiries |
| SHOP_NOW | E-commerce |
| WATCH_MORE | Video content |
## Common Errors
| Error | Solution |
|-------|----------|
| No Payment Method | Add credit card to ad account |
| App in Development Mode | Switch app to Live mode |
| DSA fields required | Add dsa_beneficiary and dsa_payor |
| Advantage Audience required | Add targeting_automation.advantage_audience |
| Invalid image dimensions | Use 1:1, 4:5, or 9:16 aspect ratios |
## Integration with Creative Team
```
/campaign_planner → Strategy approved
↓
/creative_director → Assets created
↓
/meta_ads_publisher → Published to Meta (PAUSED)
↓
User reviews in Ads Manager → ACTIVE
```
## Output contract & verification
**Produces (Done Criteria):**
- One campaign, ≥1 ad set, ≥1 ad — **all `status=PAUSED`** — in the target ad account.
- A post-publish summary table with the **real** campaign/ad-set/ad IDs returned by the API.
- A working Ads Manager review link for the account.
**VERIFY (run before claiming done — runtime is the only truth, L1):**
```bash
# Confirm every created object exists AND is paused. Fail if any row is ACTIVE.
curl -s "https://graph.facebook.com/v21.0/{CAMPAIGN_ID}?fields=name,status&access_token=$TOKEN"
curl -s "https://graph.facebook.com/v21.0/{ADSET_ID}?fields=name,status&access_token=$TOKEN"
curl -s "https://graph.facebook.com/v21.0/act_{AD_ACCOUNT}/ads?fields=name,status&access_token=$TOKEN"
```
Done only when each GET returns the object with `"status":"PAUSED"`. A 200 on POST is not proof —
read back the object.
**No-hallucination guardrail (R-CITE):**
- Every ID in the summary table MUST come from an actual API response body — **never invent,
guess, or pattern-fill an ID.** If a call failed, the ID cell reads `FAILED: <error>`, not a number.
- Quote the exact Graph API error string on any failure; do not summarize it as "something went wrong".
- The review checklist (§2) is graded against the real assets on disk/URL, not assumed.
## Safety Rules
1. **Always create as PAUSED** — Never auto-activate ads
2. **Always do holistic review** — Check copy ↔ creative match
3. **Always confirm with user** — Before creating, after creating
4. **Always provide Ads Manager link** — For manual review
5. **Never modify active ads** — Only paused/draft
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!