agentleFS
bartlomiejborzuckibartlomiejborzucki/meta-ads-agent/skills/meta-ads-campaign/SKILL.md

meta-ads-campaign

Skill0 starsChanged 44 days agoMITView on GitHub

Build Meta (Facebook/Instagram) campaigns end to end: turn a brief into a validated campaign plan, then create the campaign, ad set, assets, creative, and ads - all PAUSED - verify the result, and persist every id so an interrupted build can resume. Use for "build a campaign", "create ads from this brief", "set up a campaign but don't launch it", "launch this offer", or resuming a campaign build that failed partway.

We split this skill into the rules it gives an agent

11 rules. About 0 are common, 6 are distinctive, and 5 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈6

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 5

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    1. Meta's official Ads MCP - the execution layer: Look for tools named ads_* in this session.

    line 23
  2. Distinctive:

    Never skip validation. Never activate because creation succeeded.

    line 60
  3. Distinctive:

    Ask only for what you cannot discover.

    line 64
  4. Very specific:

    If the offer is vague, write the plan with your assumptions stated in notes rather than interrogating the user.

    line 79
  5. Distinctive:

    Optimisation goal: - must be valid for the objective.

    line 96
  6. Distinctive:

    If a call fails, stop. Do not continue past a failure, and do not retry a write without querying Meta first - a timeout does not tell you …

    line 203
  7. Very specific:

    If the CLI is absent, say so and offer the two real alternatives: upload the file in Ads Manager and pass the resulting image_hash or …

    line 218
  8. Very specific:

    If the user already has an image_hash or video_id, use it and skip the upload entirely.

    line 226
  9. Distinctive:

    If they say no, or say nothing, the campaign stays paused.

    line 266
  10. Distinctive:

    If the plan changed after objects were created, the tool blocks the resume - correctly.

    line 282
  11. Very specific:

    Read the source through the MCP: the campaign, its ad sets with targeting, promoted_object, budgets and bids, and each ad's creative with …

    line 290

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Campaign builder

Everything you create is PAUSED. Activation is a separate, approved step - read meta-ads-core first if it is installed; this skill repeats the parts it cannot work without.

<!-- shared:preflight start - generated from packaging/shared/preflight.md by scripts/sync_skill_blocks.py - edit there -->

Preflight: two checks, kept separate

Independent questions; never let one answer stand in for the other.

1. Meta's official Ads MCP - the execution layer. Look for tools named ads_* in this session. With none, nothing here can reach an account: say so and help the user connect it - connect-meta-mcp.md under references/ in the meta-ads-core skill, or https://github.com/bartlomiejborzucki/meta-ads-agent/blob/master/skills/meta-ads-core/references/connect-meta-mcp.md. The local CLI does not replace it.

2. The local meta-ads-agent CLI - optional, usually absent. Assume it is missing until this succeeds:

meta-ads-agent --version

"command not found" is the normal answer, not a fault. Show no meta-ads-agent ... command before that probe has succeeded - a command that fails at the user's prompt costs more than the step it saves. Say "that step needs the optional CLI, which is not installed here" and carry on.

The MCP alone covers audits, reporting, Ad Library research, previews, tracking, creative, optimisation and the changes that follow; .meta-ads/ files can be written directly from the templates in meta-ads-core's assets/. Only these need the CLI: plan validation (so campaign builds), local image and video upload, video / existing-post / multi-variant / carousel creatives, deletion, and the report arithmetic (which has a by-hand route).

<!-- shared:preflight end -->

The pipeline

intent → account context → brand/offer context → PLAN → VALIDATE
       → [approval if consequential] → campaign PAUSED → ad sets PAUSED
       → assets → creatives → ads PAUSED → preview → QA
       → report → awaiting approval

Never skip validation. Never activate because creation succeeded.

1. Gather

Ask only for what you cannot discover. Everything below is discoverable from Meta or the workspace, so reading first makes the conversation shorter.

From Meta (ads_get_ad_accounts, ads_get_user_pages, ads_get_ig_accounts, ads_get_datasets): account id, currency, timezone, eligibility, Page, Instagram identity, datasets and their event volume.

From the workspace (.meta-ads/brand.yaml, offers/<slug>.yaml, voice.md): naming conventions, UTM templates, DSA beneficiary and payor, default countries and CTA, banned phrases, claims policy, thresholds.

From the user, if still missing: the offer and its landing page, the budget and whether it is daily or lifetime, the market, the creative assets, and what counts as a result.

If the offer is vague, write the plan with your assumptions stated in notes rather than interrogating the user. They can correct a plan faster than they can answer eight questions.

2. Choose the structure

Discover current valid values rather than recalling them:

ads_get_field_context   → objectives, optimisation goals, CTA types for this
                          account and API version

Decisions to make explicitly, and record why in the plan's notes:

  • Objective - follows from what counts as a result. Use current ODAX names (OUTCOME_*), never a retired objective from an old example.
  • Optimisation goal - must be valid for the objective. If it optimises for conversions, a dataset and an event with recent volume are required.
  • Budget level - campaign (CBO) or ad set (ABO), never both. Meta rejects both, and so does the validator.
  • Audience - broad or defined. Both are defensible; this is a media-buying judgement, not a platform rule, so present it as a choice.
  • Placements - automatic unless specific creative demands otherwise.
  • Advantage settings - state them. Meta may alter a creative automatically, and the user should not discover that from a preview.

Background, with the uncertainty marked: references/objectives-and-optimization.md.

3. Write the plan

.meta-ads/campaigns/<slug>/plan.yaml. Schema and every field, annotated: assets/campaign-plan.yaml.

Budgets are display amounts with a currency - 70 and PLN, never 7000. Statuses are PAUSED; there is no field for anything else.

Names follow the brand's conventions: copy the naming templates from brand.yaml into the plan's names as written - {brand} | {objective} | {date}, {audience}, {variant} - and let the CLI expand them, together with the brand's UTM parameters, rather than expanding them yourself:

meta-ads-agent render-plan .meta-ads/campaigns/<slug>/plan.yaml          # show
meta-ads-agent render-plan .meta-ads/campaigns/<slug>/plan.yaml --write  # save

It never overwrites a UTM parameter already in a URL, and {date} is the plan's own creation date, so two builds agree on it. The validator refuses a name that still contains a token. With no CLI, write the names and URLs out in full instead, and say that you did.

Show the plan to the user before creating anything. A plan is cheap to change and a campaign is not.

4. Validate

meta-ads-agent validate-plan .meta-ads/campaigns/<slug>/plan.yaml

0 valid, 1 blocked, 2 unparseable. Fix every error before the first write. Warnings are for the user to see, not for you to suppress.

It checks the account exists and is active and payable, the currency matches, budgets convert cleanly and sit on one level only, the Page and Instagram identity exist on this account, the dataset and event exist, the destination is a reachable public URL, EU transparency fields are present when the plan targets the EU/EEA, special ad categories are consistent with brand config, and local assets exist and are the type they claim.

It also reports which layer will perform each step, so you can tell the user in advance where the fallback is involved.

Pass --account-file if you have cached account facts; references/campaign-plan-guide.md explains how to write them.

<!-- shared:no-cli-writes start - generated from packaging/shared/no-cli-writes.md by scripts/sync_skill_blocks.py - edit there -->

Building with no CLI: stop at the plan

Plan validation is the gate between a draft and spent money. It catches what a careful reader misses: minor-unit currency arithmetic, a budget on exactly one level, whether the Page, Instagram identity and dataset exist on this account, EU transparency fields, special-category consistency, and whether each local asset is the type it claims.

So when the probe says the CLI is absent: write the plan, show it, and stop before the first write. Do not create objects and validate afterwards - a wrong campaign that already exists, even paused, is much harder to argue with.

Then offer the routes forward, and say which you recommend:

  1. Validate without installing anything, if uv is on their PATH:
    uvx --from "git+https://github.com/bartlomiejborzucki/meta-ads-agent.git" \
      meta-ads-agent validate-plan <plan>
  2. Install it, if they will build campaigns again:
    uv tool install "git+https://github.com/bartlomiejborzucki/meta-ads-agent.git"
    meta-ads-agent doctor
  3. Build it by hand in Ads Manager from the plan - it is a complete specification.
  4. Keep the plan as the deliverable and continue with the read-only work.

Offering no route forward is not an answer, and neither is quietly proceeding.

<!-- shared:no-cli-writes end -->

5. Create, PAUSED, recording as you go

Record each id before starting the next call. This is what makes a failure resumable instead of duplicating.

1. ads_create_campaign   → record id, stage campaign_created
2. ads_create_ad_set     → record each id, stage ad_sets_created
3. assets                → see below, stage assets_uploaded
4. creative              → record id, stage creatives_created
5. ads_create_ad         → record each id, stage ads_created

If a call fails, stop. Do not continue past a failure, and do not retry a write without querying Meta first - a timeout does not tell you whether the object was created.

Assets

A local file needs the fallback; Meta's MCP lists uploaded media but cannot ingest a file. So this step needs the CLI, and there is no MCP route around it:

meta-ads-agent api upload-image ./creatives/a.jpg --account act_...
meta-ads-agent api upload-video ./creatives/a.mp4 --account act_...

If the CLI is absent, say so and offer the two real alternatives: upload the file in Ads Manager and pass the resulting image_hash or video_id, or install the CLI with its [api] extra. Do not describe the upload as done.

Both are deduplicated by content fingerprint, so a rerun costs nothing. Video upload waits for Meta to finish transcoding; a creative built against an unprocessed video is rejected.

If the user already has an image_hash or video_id, use it and skip the upload entirely.

Creative

ModeWho does it
single imageMCP - ads_create_creative
single videofallback - api create-creative --video
existing Instagram postfallback - api create-creative --post --instagram-media-id (see below)
existing Facebook Page postfallback - api create-creative --post
several copy variantsfallback - api create-creative --variants
an asset for one placement onlyfallback - api create-creative --variants --placement
carousel, 2 to 10 cardsfallback - api create-creative --carousel

An Instagram post is not boosted. ads_boost_ig_post exists, but Meta does not document whether it can create a paused boost, and everything here is created paused. So the post becomes an inert creative that a paused ad uses; it keeps the post's engagement and spends nothing until activation. If the user explicitly wants a boost, that is an activation - the same approval, after previews, as any other.

Say which one you used and why. Copy and angles come from meta-ads-creative.

6. Preview and QA

Hand off to meta-ads-preview. Render the placements the campaign actually targets, not just Feed, and check identity, copy, destination, crops, safe zones, and truncation before anyone is asked to approve spending.

7. Report, then ask

Tell the user what exists: every object with its id and status, which layer created it, the budget in account currency, where the previews are, and what QA found.

Then ask whether to activate. Explicitly. Once. Activation is top-down - campaign, then ad set, then ads - and an active ad under a paused ad set does not deliver.

If they say no, or say nothing, the campaign stays paused. That is a complete, successful outcome.

Resuming a failed build

meta-ads-agent state <slug>

Without the CLI, read state.json in the campaign directory yourself; it is plain JSON and records the stage, every id, and the plan fingerprint.

It reports what exists, which stage failed, whether the failure was retry-safe, and what comes next. Then re-read those objects from Meta, confirm they match, and continue from the first incomplete stage.

If the plan changed after objects were created, the tool blocks the resume - correctly. Restore the plan that produced them, or start a new slug.

Duplicating a campaign

"Same as last quarter's, with a new offer" is a new plan, not a copy call: the copy goes through the same review, validation and PAUSED build.

  1. Read the source through the MCP: the campaign, its ad sets with targeting, promoted_object, budgets and bids, and each ad's creative with its full fields, not only its id. Save them as one JSON file in the Marketing API's field names - the shape is in references/duplicate-input.md.
  2. With the CLI:

    meta-ads-agent duplicate-plan source.json --slug acme-webinar-q4 \
      --currency PLN --write

    Budgets and bids come back as display amounts in the account currency, every status is PAUSED, and schedules are left out. Read its notes out to the user. Without the CLI, write the plan by hand from the same fields, following the same rules.

  3. Make the change the user asked for in the new plan, then continue at step 4 above. The source campaign is never touched.

A multi-variant creative is not duplicated: say so, and plan that ad by hand.

Regional and regulatory

EU/EEA delivery requires DSA transparency fields - beneficiary and payor. They identify who paid and who benefits, and they must never be invented. Take them from brand.yaml, from the account's defaults, or from the user.

Special ad categories - credit, employment, housing, social issues, elections, politics - must be declared. An empty list is a claim the user is making, not a safe default, and misdeclaring risks account suspension. Never try to work around a category restriction.

Detail: references/special-categories-and-dsa.md.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.