| name | uni1-image-ad |
| description | Use when the user wants to generate a Meta/Facebook image ad with Luma uni-1 and attach it to an existing Meta ad set. Triggers on phrases like "uni-1 ad", "make a Luma ad creative", "new image ad in <ad set name>", "uni-1 image ad for Meta", "generate uni-1 ad creative", "upload uni-1 image as Meta ad". Anchors on uni-1 + Meta together — does NOT trigger on a generic "make an ad" without the uni-1 / Luma cue, and does NOT trigger for non-Meta destinations (TikTok, Google, etc.). |
uni1-image-ad
Generate one or more uni-1 images and create paused Meta ads from them, attached to an existing ad set in the user's Meta ad account.
Hard rules — never relax
These come from the user's Luma brand contract and from "don't spend money by accident":
-
Model is uni-1. Never substitute photon-1, photon-flash-1, or any other model. The brand contract requires every promotional image be generated by uni-1. The helper script enforces this; you must not work around it.
-
Always clone from an existing ad. The user must supply an existing ad ID to clone — page, ad set, link URL, and CTA are read from that ad. Never hand-pick a page or ad set independently. If the user does not provide an ad ID, ask for one before any other input.
-
Claude writes the new ad copy. Body and headline are generated fresh per variant, tailored to the new image. The cloned ad's copy is reference for tone/voice only — never copy it verbatim.
-
Ad status is PAUSED. Never pass --status ACTIVE or otherwise unpause an ad. The user reviews and launches manually in Ads Manager.
-
No campaign or ad-set mutations. New ads are attached to the cloned ad's existing ad set. The skill never creates campaigns or ad sets.
-
Confirmation gate before every Meta CLI mutation. Show the full command, wait for explicit approval, then run.
-
Audit log first, then mutate. Append the JSONL row when a creative is created, before attempting the ad. Orphaned creative IDs must be recoverable from ./generated/runs.jsonl.
-
No screenshot/platform chrome in generated images. The output of generate_image.py must be the standalone ad creative — the static image the advertiser uploads. Never render iOS device chrome (status bar, home indicator), platform brand-row headers ("Sponsored"/"Saved"), post text/captions surrounding the ad, link-card footers ("URL.COM | headline | Learn more"), engagement rows (likes/comments/shares), or platform tab bars. The script auto-appends a no-chrome guard suffix to every prompt by default. If a use case genuinely requires screenshot-style chrome (rare — usually a UGC-aesthetic ad), pass --allow-chrome explicitly and explain why.
Generation modes
The helper script supports two modes; pick based on what the user asks for.
| Mode | When to use | Required | Optional |
|---|
image (default) | Generate a brand-new ad image. | --prompt, --aspect-ratio | --image-ref (up to 8) for product/style grounding |
image_edit | Modify an existing image (swap colors, change background, add an element). | --prompt, --source | --image-ref (up to 8) for additional guidance |
Reference images are how you keep brand fidelity. When the user wants the ad to feature a specific product (jar, bottle, package), pass that product image via --image-ref. The model holds packaging, wordmarks, and label typography much more faithfully when it has the asset to ground on. Without a reference, output is "inspired by" — labels and wordmarks will be approximate.
Refs and source can be local file paths (PNG/JPG/JPEG/WEBP/GIF). The script base64-encodes them; no need to upload anywhere.
Inputs the user must provide (or you must elicit)
The user-facing surface is small — most ad fields come from the cloned ad. Cache last_clone_ad_id per account in ~/.claude/skills/uni1-image-ad/state.json.
| Input | Source | Notes |
|---|
| Existing ad ID to clone | User (required) | Ad must live in the configured AD_ACCOUNT_ID. Cache as default for next run. |
| Seed prompt | User | The creative direction in their words. You will rewrite it (see Phase 3). |
| Mode | User or inferred | image (default) or image_edit. Use image_edit only if the user is editing a specific source image. |
Source image (--source) | User | Required only for image_edit. |
Reference image(s) (--image-ref) | User | Optional but strongly recommended when the ad features a specific product. Up to 8. |
Variant count N | User, default 1 | Cap at 5. |
| Aspect ratio | User | One of 1:1, 1.91:1, 4:5, 9:16. Reject anything else. Ignored in image_edit mode. |
| Ad name (base) | User, optional | Defaults to <cloned-ad-name> · uni-1 vN. You suffix per variant. |
| CTA override | User, optional | Defaults to the cloned ad's CTA. Override only if user explicitly asks. |
Derived from the cloned ad (do NOT ask the user — read these via meta ads ad get + meta ads creative get):
| Field | Comes from |
|---|
| Ad set ID | ad.adset_id of the cloned ad |
| Page ID | creative.object_story_spec.page_id of the cloned creative |
| Link URL | creative.object_story_spec.link_data.link |
| CTA | creative.object_story_spec.link_data.call_to_action.type |
| Cloned body / title | Reference for tone only — used in Phase 3.5 to write new copy |
Workflow
Phase 1: Preflight
Verify in this exact order; bail on the first failure with a fix-it message.
- The current working directory has a
.env file containing LUMA_API_KEY, ACCESS_TOKEN, and AD_ACCOUNT_ID.
- Run
meta auth status. It must print Authenticated. On non-zero, instruct the user to refresh their token (see references/meta-cli-flags.md § Token refresh quick steps).
- Read
~/.claude/skills/uni1-image-ad/state.json. If accounts[<AD_ACCOUNT_ID>].last_clone_ad_id exists, offer it as the default for "ad ID to clone".
Phase 2: Gather inputs and clone the source ad
-
Ask for the ad ID to clone if not already supplied. Show cached last_clone_ad_id as the default. Refuse to proceed without one.
-
Resolve the ad via meta -o json ads ad get <AD_ID>. From the response, capture:
adset_id — the destination for the new ad
creative.id — to fetch the cloned creative
-
Resolve the creative via meta -o json ads creative get <CREATIVE_ID>. From the response, capture:
object_story_spec.page_id (or object_story_spec.link_data.page_id) → page_id
object_story_spec.link_data.link → link_url
object_story_spec.link_data.call_to_action.type → CTA
object_story_spec.link_data.message → cloned body (tone reference)
object_story_spec.link_data.name → cloned title (tone reference)
-
Show the cloned values to the user as a compact summary so they can sanity-check. Example:
Cloning from ad 120246xxxxxxxxxxxx ("Old Headline · v3")
Ad set: 120243594343360512 (Luma AI Testimonial Ads)
Page: 1234567890 (Mr. Paid Social)
Link: https://lumalabs.ai/api
CTA: LEARN_MORE
Body (cloned, for tone reference):
"<first 80 chars>…"
-
Then gather the new-image inputs: seed prompt, mode (default image), --source if image_edit, --image-ref paths, variant count N, aspect ratio, optional ad-name base, optional CTA override.
-
Validate aspect ratio strictly. Reject anything not in {"1:1", "1.91:1", "4:5", "9:16"}.
Phase 3: Prompt rewrite
This is where you add value. First check the prompt library, then either fill a template or write fresh.
3a — Check the prompt library
Load references/prompt-library.md. It has 7 validated parameterizable templates:
| Tag | Format | When |
|---|
| T1 | Apple Notes listicle | "Why I switched to X" / sentimental list of reasons |
| T2 | Editorial article hero | Publication-co-signed credibility ad (FORBES, WIRED, Vogue, etc.) |
| T3 | Story tweet+UGC composite | Authority quote + UGC photo for Stories/Reels |
| T4 | Fake Google search mosaic | "Best X for Y" with publication logos as social proof |
| T5 | Comparison table (light) | Brand vs. category competitor with feature checklist |
| T6 | Comparison table (dark, hooky) | Stop-the-scroll dark-mode "this RUINS X" hook |
| T7 | Sticky-note + product flatlay | Tactile UGC-style, 30-day-test reviewer voice |
If the user's seed prompt or brief maps onto one of these, use it: read the matching section in prompt-library.md, fill in the {placeholder} variables for the user's brand, and use that as the rewritten prompt. Tell the user which template you matched and why.
If nothing fits, write fresh — then propose adding the new pattern to the library after generation succeeds.
3b — Fleshing out a fresh prompt (or filling a template)
When writing or completing a prompt, anchor on:
- Subject and pose
- Lighting and time of day
- Lens / framing
- Color palette / mood (pull from the brand's identity)
- Composition (rule of thirds, leading lines)
- Negative space for text overlay if the ad has body/headline copy
- Reference roles — if
--image-ref is being used, name each reference's role explicitly in the prompt (e.g. "the product in image_ref[0]", "the lighting from image_ref[1]"). Luma's docs note that multi-reference quality improves when each reference is labeled in the prompt.
- Standalone-creative scope — never describe iOS chrome, Sponsored badges, engagement counts, or platform UI. The script's no-chrome guard catches violations, but write the prompt as if the rule is on you.
Show the rewritten prompt to the user as one block. Tell them which template (if any) it's based on. Ask: "Use this, edit it, or start over?" Loop until approved.
Phase 4: Generate
Run the helper script. Always pass --env-file pointing at the project .env. Add --image-ref <path> once per reference (max 8). Switch to --mode image_edit --source <path> if the user is editing rather than creating from scratch.
~/.claude/skills/uni1-image-ad/scripts/generate_image.py \
--prompt "<rewritten>" \
--aspect-ratio <ratio> \
--n <N> \
--image-ref <product.png> \
[--image-ref <style-board.png>] \
--out ./generated \
--env-file .env
~/.claude/skills/uni1-image-ad/scripts/generate_image.py \
--prompt "<edit-instruction>" \
--mode image_edit \
--source <existing.png> \
[--image-ref <guidance.png>] \
--n <N> \
--out ./generated \
--env-file .env
Reference-grounded generations typically take ~40–60s (vs ~6s text-only). Don't bail early — the script's poll timeout is 240s.
Each line on stdout is JSON for one variant (variant, path, generation_id, width, height). Display the paths to the user.
If any variant comes back below 1080×1080, regenerate it (the script logs a warning to stderr but still emits the JSON line — you decide whether to keep it). If all variants failed, stop and report the error from stderr.
Phase 5: Confirm variants
Show all paths and ask: "Use all / use these specific ones / regenerate / cancel." One confirmation covers all selected variants.
Phase 5.5: Write new ad copy
Two sub-steps: first read the data, then write copy informed by it. Don't skip the data read — generic copy is the #1 failure mode of this skill.
5.5a — Pull what's actually winning spend in this account
~/.claude/skills/uni1-image-ad/scripts/top_spending_ads.py \
--limit 10 --days 30
That prints one JSON-per-line for the top-10 spending ads in the configured account, with body, title, spend, impressions, ctr, and creative_id. Read all of it, then derive a short pattern profile:
- Voice — first-person? founder? testimonial? brand-distant?
- Length distribution — what's the median body length of the top performers?
- Bullet style —
→, ✓, •, no bullets, emoji?
- Hook structures — counter-narrative, statistic, personal anecdote, threat?
- Credibility anchors — recurring numbers, names, claims?
- CTA closers —
Join us →, See the demo, Comment X, no closer?
State the pattern profile to the user as a compact table before drafting copy. This is the diff between "I made up some copy" and "I matched what's working."
5.5b — Write copy that matches the pattern profile
For each selected variant, you (Claude) write a new body and headline. Anchor on:
- The new image — what it actually shows. Lead with the visual hook.
- The pattern profile from 5.5a — match length distribution, voice, bullet style, CTA closer. The data is the spec.
- The cloned ad's voice — secondary signal; never copy phrasing verbatim.
- A copy framework from
references/ad-copy-frameworks.md — load that file before writing. Pick a framework that fits the image + the dominant pattern in 5.5a.
- For multi-variant runs, use different frameworks per variant so the user A/B tests structure, not just wording.
No hard character cap. Meta truncates around the read-more fold, but if the top spenders run 400-700 char bodies and that's clearly working in this account, write to that length. The data decides, not a rule of thumb. (Exception: titles still fit under ~40 chars to avoid placement-level truncation.)
Show all variants' copy as a numbered table for review, including which framework each used and a one-line note on which top-spending ad it pattern-matches:
v1 [framework: First Person] matches: 120245165245740512 ("Refresh. Duplicate. Pause. Pray.")
title: …
body: …
v2 [framework: Statistic] matches: 120243595999050512 ("The math doesn't lie.")
title: …
body: …
Ask: "Use this copy / edit specific lines / regenerate." Loop until approved before any Meta CLI calls.
Phase 6: Upload loop
For each selected variant i:
-
Compose the creative-create command using the cloned page_id, link_url, and CTA (or override) and the new copy from Phase 5.5. Use references/meta-cli-flags.md for the exact flag spelling. Quote all string values with embedded spaces.
meta -o json ads creative create \
--name "<base> v<i>" \
--image <path-to-new-uni1-image> \
--page-id <CLONED_PAGE_ID> \
--body "<new-body-from-phase-5.5>" \
--title "<new-title-from-phase-5.5>" \
--link-url "<CLONED_LINK_URL>" \
--call-to-action <CLONED_OR_OVERRIDE_CTA>
-
Show the command, ask "run this?". Run on yes. Capture the creative ID.
-
Append to ./generated/runs.jsonl immediately (one JSON per line):
{"ts": "<iso8601>", "prompt": "<rewritten>", "variant": <i>, "generation_id": "<gid>", "image_path": "<path>", "creative_id": "<cid>", "creative_id_status": "created", "ad_id": null, "ad_id_status": "pending"}
-
Compose the ad-create command using the cloned adset_id:
meta -o json ads ad create <CLONED_ADSET_ID> \
--name "<base> v<i>" \
--creative-id <CREATIVE_ID>
-
Show, confirm, run. Capture the ad ID. Update the JSONL row in place (read all lines, replace this one, rewrite) with ad_id and ad_id_status: "created".
-
If ad-create fails: leave ad_id_status: "failed" with an error field. Tell the user the creative was created (give the ID) and offer to retry just the ad-create step.
Phase 7: Persist cache, report
- Update
~/.claude/skills/uni1-image-ad/state.json:
{
"accounts": {
"<AD_ACCOUNT_ID>": {
"last_clone_ad_id": "<CLONED_AD_ID>",
"last_run_ts": "<iso8601>"
}
}
}
- For each created ad, print:
- The ad ID
- Deep link:
https://business.facebook.com/adsmanager/manage/ads?act=<numeric>&selected_ad_ids=<AD_ID> (strip the act_ prefix from AD_ACCOUNT_ID)
- Remind the user: all ads are PAUSED — review and launch manually.
Retry mode (partial-failure recovery)
If runs.jsonl already exists when the user re-runs the skill, scan it for rows where creative_id_status == "created" but ad_id_status != "created". Offer:
"I see orphan creative(s) from a previous run. Do you want to retry the ad create step against those existing creatives, or generate a new image?"
If retry: skip Phases 3–5, jump to Phase 6 step 4 with the existing creative IDs.
Dry-run mode
If the user says "dry run", "preview only", or "show me the commands", run Phases 1–4, then in Phase 6 just print every meta command without executing, and exit. Do not write to runs.jsonl in dry-run mode.
Out of scope — fail clearly
If the user asks for any of these, stop and explain it's not supported:
- Creating ads without cloning from an existing one. The skill always clones. If the user has no existing ad to clone, ask them to create one manually in Ads Manager first (page, ad set, link, CTA), then re-run with that ad's ID.
- Creating a new campaign or ad set. Same answer — create the parent structure manually, then clone an ad inside it.
- Auto-launching ads (
--status ACTIVE). Refuse.
- Video, carousel, or DCO ads. Refuse — image only.
- Multi-account batch in one run. The skill operates on the account in
AD_ACCOUNT_ID only.
- Insights / reporting. Different skill.
Files this skill owns
~/.claude/skills/uni1-image-ad/SKILL.md — this file
~/.claude/skills/uni1-image-ad/scripts/generate_image.py — Luma uni-1 caller
~/.claude/skills/uni1-image-ad/scripts/top_spending_ads.py — pulls top-N spending ads + their copy for Phase 5.5a
~/.claude/skills/uni1-image-ad/references/meta-cli-flags.md — load this before composing each meta command
~/.claude/skills/uni1-image-ad/references/ad-copy-frameworks.md — load this in Phase 5.5 before writing body/title
~/.claude/skills/uni1-image-ad/references/prompt-library.md — load this in Phase 3 before rewriting the prompt; 7 validated parameterizable image templates
~/.claude/skills/uni1-image-ad/state.json — per-account page/adset cache
Files written into the user's project directory (<cwd>):
<cwd>/generated/<ts>-<slug>-v<i>.png — generated images
<cwd>/generated/runs.jsonl — append-only audit log