[ PAID MEDIA ]
/meta-lead-ads
Creates a complete Meta lead ads campaign from a Google Slides brief — reads copy, visuals, and lead form spec from the deck, uploads assets, builds the campaign in Meta (PAUSED), and reports to Slack and ClickUp.
ACME Agency, <id> and you@example.com mark values that are per-agency — your install fills them with YOUR clients and accounts. If a section references a helper script you don't have yet, it ships with that workflow's install.Meta Lead Ads — Campaign Creator
Triggers
/meta-lead-ads "ACME Agency" "https://docs.google.com/presentation/d/..."— manual, full run/meta-lead-ads— prompts for client and Slides URL if not provided- ClickUp auto-trigger when a task tagged
claude-codematches: meta lead,lead ads meta,lead ads facebook,facebook lead,instagram leadoglasi lead,lead oglas,lead ads- Slides URL must be present in the task description
What this skill does
Reads a Google Slides brief (one slide per ad format, one slide for the lead form), extracts all copy and visuals, uploads assets to Meta, creates a lead gen form, campaign, ad set, and ads — all in PAUSED status for ACME Agencyw. Posts a summary to the client's Slack channel and closes the ClickUp task.
Expected Google Slides Format
Each slide serves one purpose. Put an explicit type marker as a standalone line at the bottom of the left-column text box so the parser is unambiguous: CAROUSEL, SINGLE IMAGE, or VIDEO. Without the marker the parser falls back to image/video counting, which can guess wrong on edge cases.
| Slide | Type | Layout | Marker |
|---|---|---|---|
| Single image ad | SINGLE IMAGE | Left: body + Headline: + Description:. Right: 1 image | SINGLE IMAGE on its own line |
| Video ad | VIDEO | Left: body + Headline: + Description:. Right: embedded Drive MP4 | VIDEO on its own line (optional — video element auto-detects) |
| Carousel ad | CAROUSEL | Left: body + multiple Headline: lines (first is ad-level, rest are card names). Right: 1 image per card (extras are treated as logo / end card and dropped) | CAROUSEL on its own line |
| Lead form spec | auto | Text only — see format below | none needed |
Primary text box convention
The left-column text box is the source of truth for all copy. It's split into body lines and labeled fields. Labeled fields use Label: value on their own line, case-insensitive. Supported labels:
| Label | Meaning |
|---|---|
Headline: / Naslov: / Title: | Ad headline. For carousels, multiple lines — first is ad-level, rest are card names in order |
Description: / Opis: | Ad description (single image / video) or per-card description (carousel, rarely used) |
Link: / URL: | Destination URL. For carousels, pair each Headline: with its own Link: directly below it |
Anything without a label is body copy. The CAROUSEL / SINGLE IMAGE / VIDEO markers, any labeled field, and any bare URL are stripped from the body before it's sent to Meta — so you can edit the slide freely without worrying about marker text leaking into the ad.
Carousel card URL pairing
There are two ways to attach a destination URL to each carousel card. Pick the most convenient:
A. Inline (preferred — deterministic). Put Link: lines directly below each card's Headline: in the primary text:
Headline: [ad-level title]
Headline: Card 1 name
Link: https://example.com/card-1
Headline: Card 2 name
Link: https://example.com/card-2
B. Separate URL text boxes (legacy). Drop bare URLs in their own text boxes anywhere on the slide. The parser matches each URL to a card by keyword similarity (URL slug tokens vs. headline tokens, with prefix matching so Croatian declension like "kostrena/kostreni" still matches). This works well when URL slugs reflect property/product names. If two cards don't share distinctive keywords with any URL, the parser falls back to reading order (top-to-bottom, left-to-right) and logs the pairing — always verify in the dry-run output.
If you don't provide any URLs, each card falls back to the client website.
Body copy formatting
Body text is automatically reformatted through formatAdCopyForMeta() before hitting Meta's API — see ACME Agency/scripts/lib/copy_formatter.mjs. You don't need to add blank lines by hand; the formatter will split long paragraphs into 3–5 short blocks. If you do add manual blank lines, the formatter respects them.
Lead form slide text format:
[Form headline — largest text]
[Form description paragraph]
[Custom question label?]
Option 1
Option 2
Option 3
Ime i prezime
Broj telefona
Email
Grad
[Thank you title]
[Thank you body text]
Defaults when fields are missing from slides:
- Form description →
"Ispunite podatke ispod i naš tim će vas kontaktirati." - Thank you title →
"Hvala!" - Thank you body →
"Naš tim će Vas kontaktirati u roku od 24 sata." - Button text →
"Posjetite web stranicu"+ client website - Contact fields → FULL_NAME, PHONE, EMAIL
Also supported (since 2026-06-17 ACME Agency):
- Labeled form fields. The form slide may use the same labels as the ad slides —
Headline:(form title),Description:(form copy),Questions:(heading). The labels are stripped before parsing, so the title/description come through clean. - Thank-you screen on its OWN slide. Mark it with
META LEAD FORM(and aThank you screenheading). All slides carrying an explicitMETA LEAD FORM/INSTANT FORMmarker are collected in deck order and merged into one form, so the form spec and the thank-you screen can live on separate consecutive slides. - GHL autoresponder email on its own slide. Mark it
THANK YOU EMAIL(standalone line). That slide is classified UNKNOWN and ignored by Meta — it's the copy the operator pastes into GoHighLevel, not the Meta instant-form thank-you screen. Without the marker it used to score as a second LEAD_FORM slide (matches/email/+/zahvalj/) and silently overwrite the real form.
ClickUp Task Format
Task name: Pustiti meta lead oglase za [service]
Tag: claude-code
Description:
https://docs.google.com/presentation/d/PRESENTATION_ID/edit
Budget: 15€ dnevno
Targetujemo cijelu Hrvatsku
Godine: 30+
Placement: bez Audience Networka
The Slides URL must be in the task description. Budget, age, and geo are parsed automatically — if missing, defaults are used (€15/day, 30+, Croatia, no Audience Network).
Inject Mode (add ads to an existing campaign / ad set)
When the brief names an existing campaign + ad set + form, the script switches to INJECT mode — it does NOT create a campaign/ad set/form, just adds ads to what's there. Brief vocabulary:
u kampanju: ACME Agency - Leads / Meta ABO - maj 2026
u Ad Set: CRO MW 18-65 (Čiovo)
i poveži sa formom: ACME Agency - form 02
Copy (slide 10): https://docs.google.com/presentation/d/.../edit
Visuals (3.png): https://drive.google.com/drive/folders/<ID>
Copy (slide 10):/Copy (slides 3, 4, 5):— restrict to specific slides (parenthetical list, mirrors the targeting parser).Visuals: <folder>— pull creatives from a Drive folder, mapped to ad slides by sorted filename in deck order (default). Image slides consume images, video slides consume videos — a mixed folder (1.png+video.mp4) is routed by type automatically.Visuals (3.png): <folder>— pick the file named3.pngfor the (single) image slide instead of sorted order.Visuals (image_1, 4.png): <folder>— multiple named files in order: first name → first image slide, second name → second image slide, etc. Extension optional (image_1matchesimage_1.png). Missing name → clear error listing the folder's files.- Page is derived from the ad set, not
clients.json. The script reads the ad set'spromoted_object.page_idand uses it (with the matching Instagram from existing ads). This is required for multi-page clients — see ACME Agency below.verifyInjectAccess()then validates token access to that derived page before any upload.
Targeting Parsing Rules
| Field | What to write | Default |
|---|---|---|
| Budget | 15€ dnevno / 20e daily / €20/day | €15/day |
| Age | 30+ or 25-55 | 30–65 |
| Geo | cijela Hrvatska / Croatia / HR | Croatia |
| Audience Network | bez Audience Networka (excluded by default) | excluded |
Preflight (run BEFORE creating any Meta campaign)
Meta lead ads create real campaigns in production accounts. Even though they're created PAUSED, validate everything before submission. A failed campaign creation mid-pipeline often leaves orphaned ad sets and forms that need manual cleanup in Meta.
- Client exists in
clients.jsonAND hasad_account(required) ANDpage_id(required for lead forms). <id>set in.envand the page is in/me/accountsfor that token. The script auto-runsverifyInjectAccess()at the start of every run (Step 0) to catch missing Page ADVERTISE task / inactive ad accounts BEFORE any slide parse or asset upload — error message names the exact BM screen to fix.- Slides URL is a real Google Slides URL (
docs.google.com/presentation/d/...) — reject any other shape. - Slides accessible by the gws CLI / Slides API — read once before parsing to confirm permissions.
- Required slides present: copy slide, visual slide(s), targeting slide, lead form spec slide. If any missing → abort with which one.
- Lead form locale is
EN_US(Meta does not supporthr_HR). Force this — never accept other locales. facebook_positionsdoes NOT includereels(Meta API limitation). If reels was specified → strip it and warn.- Attribution windows are valid per CLAUDE.md
## AI Generation API Constraints: only1d_click,7d_click,28d_click,1d_view,7d_view. Reject14d_clickand30d_click. - Budget is in account currency AND ≥ Meta minimum daily (€1 EUR / $1 USD typically).
- Targeting is non-empty — at minimum: countries, age range, gender. Reject empty targeting (Meta will create but it won't deliver).
- All embedded slide images are extractable AND ≥ 600 px on the longest side (Meta rejects tiny images).
- Videos (if any) are Drive-hosted MP4 (NOT YouTube embeds — those silently fail).
- ClickUp task ID present if
--clickup-taskwas passed (for posting completion update).
If all checks pass, log "preflight: OK (n ads, m ad sets, lead form locale=EN_US, budget=...)" and proceed.
How to Run
Via ClickUp (automatic)
- Create a ClickUp task in the client's list
- Paste the Google Slides URL in the description
- Add budget, targeting instructions in plain text
- Tag the task
claude-code - Run:
node ACME Agency/scripts/clickup_executor.mjs --execute
Via slash command (manual)
/meta-lead-ads "ACME Agency" "https://docs.google.com/presentation/d/..."
Claude Code will detect the intent and run the skill with the provided arguments.
Direct CLI
node ACME Agency/scripts/meta_lead_ads.mjs --client "ACME Agency" --slides "https://docs.google.com/presentation/d/..."
Advanced CLI flags (optional)
All flags are opt-in — defaults preserve the historical CBO + narrow placements + Higher Volume + auto-generated names behavior. Use them when the brief asks for something the parser can't infer (most often: ABO budget, Higher Intent forms, SMS verification, narrow placement exclusion).
| Flag | Purpose | |
|---|---|---|
--abo | ABO budget mode — daily_budget moves from campaign to ad set | |
--budget-eur <n> | Override daily budget (EUR) | |
--campaign-name "<s>" | Override auto-generated campaign name | |
--adset-name "<s>" | Override auto-generated ad set name | |
--geo HR,SI | Override targeting.geo_locations.countries | |
--age-min <n> / --age-max <n> | Override targeting age range | |
--placements-exclude <list> | Switch to broad placements, exclude listed tokens. Valid: fb_notifications, fb_marketplace, fb_right_column, ig_explore_home, search | |
--higher-intent | Lead form: <id>=true (adds ACME Agencyw step before submit) | |
--sms-verification | Lead form: <id>=true (SMS OTP). Separate Graph API field — can combine with --higher-intent | |
--privacy-url <url> | Override privacy policy URL | |
--privacy-text "<s>" | Override privacy policy link text | |
--cta-action <action> | Thank-you button_type (default VIEW_WEBSITE) | |
--cta-url <url> | Thank-you website_url | |
--cta-text "<s>" | Thank-you button_text | |
| `--status PAUSED\ | ACTIVE` | Campaign/ad set/ad status (default PAUSED) |
Example — ACME Agency 2026-05-22 (ABO + Higher Intent + SMS + narrow placements):
node ACME Agency/scripts/meta_lead_ads.mjs --client "ACME Agency" \
--slides "https://docs.google.com/presentation/d/..." \
--campaign-name "ACME Agency - Lead / Forms ABO - maj 2026" \
--adset-name "CRO 30+" \
--abo --budget-eur 25 \
--geo HR --age-min 30 --age-max 65 \
--placements-exclude fb_notifications,fb_marketplace,fb_right_column,ig_explore_home,search \
--higher-intent --sms-verification \
--privacy-url "https://ACME Agency.com/o-nama/politika-privatnosti" \
--privacy-text "Visit ACME Agency's Privacy Policy." \
--cta-action VIEW_WEBSITE \
--cta-url "https://ACME Agency.com/" \
--cta-text "Posjetite naš website"
Two UI-only toggles (no Graph API field as of v20):
- "Background image: use image from your ad" → toggle in Forms Library after creation
- "Flexible form delivery: Optimized" → toggle in Forms Library after creation
The script's Slack report flags these reminders when --higher-intent or --sms-verification is set.
Key Files
| File | Purpose |
|---|---|
ACME Agency/scripts/meta_lead_ads.mjs | Main script — slides parser, targeting parser, Meta creation, reporting |
ACME Agency/scripts/lib/google_slides.mjs | Google Slides API auth + utilities |
ACME Agency/scripts/clickup_executor.mjs | Executor — meta_lead_ads handler at line ~145 |
ACME Agency/clients/clients.json | Client registry — ad_account, page_id, instagram_id, website |
Environment Requirements
| Env var | Used for |
|---|---|
META_ACCESS_TOKEN | All Meta API calls |
GOOGLE_ADS_CLIENT_ID | OAuth client ID (shared with Slides auth) |
<id> | OAuth client secret |
<id> | Slides API access (drive scope covers slides) |
CLICKUP_API_KEY | ClickUp task updates |
SLACK_BOT_TOKEN | Slack reporting |
Google Slides API must be enabled in Cloud Console project agency-os-mcp-490220. To enable: https://console.cloud.google.com/apis/library/slides.googleapis.com?project=agency-os-mcp-490220
What Gets Created in Meta
All objects are created in PAUSED status — nothing goes live automatically.
- Lead gen form — with parsed headline, description, custom questions, contact fields, thank you screen
- Campaign — OUTCOME_LEADS objective, CBO with parsed budget
- Ad set — ON_AD destination, LEAD_GENERATION optimization, parsed targeting (no Audience Network)
- Ads — one per slide: single image, video, and/or carousel (SIGN_UP CTA → lead form)
After creation:
clients.jsonis updated with the new form ID- Summary posted to the client's Slack channel
- ClickUp task marked complete,
claude-codetag removed
After the Skill Runs
- Activate in Ads Manager — ACME Agencyw creatives and targeting, then turn on
- Connect form to GHL — in GHL, go to Integrations → Facebook Lead Ads → select the form by ID
- GHL automation triggers on new lead submission
Verification (run AFTER campaign creation — confirm everything wired correctly)
Lead ads have many moving parts and silent failures leave orphaned objects in Meta. Check ALL of these before declaring done:
- [ ] Campaign created with status
PAUSED(NEVERACTIVEfrom this skill) - [ ] Campaign objective is
LEAD_GENERATION(orOUTCOME_LEADS) - [ ] Lead form created with a real
form_id, localeEN_US, and the form fetches successfully via GET - [ ] Every ad set is linked to the campaign AND has the form attached
- [ ] Every ad has creative attached (image OR video OR carousel — none missing)
- [ ] Targeting on each ad set is non-empty AND matches the targeting slide
- [ ] Daily budget set on each ad set, in correct currency
- [ ] No
reelsinfacebook_positions(would have been stripped in preflight, but verify the response confirms it) - [ ] No
14d_clickor30d_clickin attribution - [ ] Slack post via slack-reporter returned
tsnon-null with: campaign ID, form ID, ad count, link to Ads Manager - [ ] If
--clickup-task <id>passed: ClickUp task updated with completion comment + Ads Manager link - [ ] Manifest saved at
ACME Agency/clients/<Client>/meta_lead_ads_<YYYY-MM-DD>.jsonwith all created object IDs (for cleanup if needed)
If any check fails, name the gap explicitly AND list the orphaned Meta object IDs that need manual cleanup. Never claim success when verification fails — orphaned ad sets in Meta are real money waiting to drain.
Important Notes
- Videos must be Drive-hosted MP4 — YouTube embeds in slides are not supported
- Images are extracted directly from the slide's embedded content — quality depends on what was inserted into the slides
- If slide images are low-res, insert high-res versions into the slides before running
- Carousel card count is driven by the number of
Headline:lines after the first (ad-level) headline. Extra images beyond that count are treated as logos or end-cards and dropped - Explicit slide markers (
CAROUSEL/SINGLE IMAGE/VIDEO) always win over heuristic classification. When a slide has many images but should be single/video, add the marker to force it - The lead form locale is forced to
EN_US(Meta does not supporthr_HR) - Meta Lead Form briefs do NOT need a website/landing URL. The Graph API requires an external creative link for LEAD_GENERATION ads (the Ads Manager UI hides this), so the script supplies it itself:
clients.jsonwebsite→ else the Page's ownwebsite(GET /{page_id}?fields=website) → else it aborts asking you to set one. Never solicit a web link from the operator for a lead-form campaign. - Form resolution finds not-yet-attached forms. After scanning existing campaign ads,
findLeadFormByNamefalls back to listing the Page'sleadgen_formsvia a minted Page token — so a brand-new ad set + brand-new form launched together resolves without a freestyle script. Form resolution runs after page derivation so the fallback queries the correct (ad-set) page. facebook_positionsdoes not acceptreels(Meta API limitation)- Every ad's body copy is reformatted through
formatAdCopyForMeta()— seeACME Agency/CLAUDE.md→ "Meta Ad Creation Rules" - Dry-run the parser before creating ads:
``bash node -e "import('./ACME Agency/scripts/meta_lead_ads.mjs').then(async m => { const r = await m.parsePresentation('PRESENTATION_ID', { skipImageDownload: true }); console.log(JSON.stringify(r, null, 2)); })" ``
Inject-mode brief (adding ads to an existing campaign)
Inject mode adds new ads into an existing campaign + ad set + form — it does NOT create a campaign. Trigger lines (natural phrasing tolerated):
Dodaj Ads u kampanju: <Campaign Name> ← required (also "u kampanju:" / "Campaign:")
u Ad set: <Ad Set Name> ← required (also "Ad set name:" / "in Ad Set:")
Forma: <Form Name> ← OR "Form:" / "Lead Form name:" / "poveži sa formom:"
(or "Website URL: <url>" for a web-traffic ad)
Copy (slide 3) / Copy (slides 1, 2): <Slides URL> ← which deck slides to pull copy from
Visuals (1.png, Video_1.mp4): <Drive folder URL> ← see named-files below
Tip: single image ← optional ad-type override (see below)
- Named visual files —
Visuals (a.png, b.mp4): <folder>cherry-picks exact files from a folder that holds many. Each name resolves against whichever pool it belongs to (image or video), in listed order, so one list drives both image and video slides. Without the parenthetical, each type pool is consumed in sorted filename order (the default). Use this whenever the wanted file isn't alphabetically first in its pool — otherwise the wrong asset attaches silently. Tip:ad-type override —Tip: single image/Tip: video/Tip: carousel(HR:Tip: slika/karusel, orkao single image) forces the ad type for the chosen slide(s), overriding the deck's auto-classification. Use when a slide is a VIDEO in the deck but you want a single-image ad from its copy + a static image (Copy (slide 3)+Visuals (1.png)+Tip: single image).
Incidents
- 2026-06-19 — ACME Agency (parodontologija inject): a carousel slide with one ad-level
Headline:and no per-card headlines madebuildCarouselCardsfall back to literal card namesCard 1..N, whichcreateAdCreativesent aschild_attachments[].name→ "Card 1" printed under every card on the live ad. Fix: fallback cards now getheadline: '', and the CAROUSEL_AD branch omitsname/descriptionwhen blank (both optional onlink_data). Carousels with real per-card headlines unchanged. Injected 3 ads (single<id>, carousel<id>— 5 cards, allname:null, video<id>) into ad setZG MW 30+ broad, form<id>, all PAUSED. - 2026-05-22 — ACME Agency (task 86c9t0u0a): mixed image+video brief (2 image slides + 3 video slides) crashed twice. (a)
Visuals folder has 2 images but brief needs 5— the visuals-folder check counted ALL ad slots but listed only image files; now useslistDriveFolderAssets()with separate per-pool count validation. (b)Unexpected end of JSON inputon a 107MB video — single POST to/advideosreturns empty body above ~100MB; now auto-routes >50MB through Meta's chunked upload session (upload_phase=start|transfer|finish). - 2026-05-22 — ACME Agency: new client needed ABO + Higher Intent + SMS verify + narrow placements + custom CTA/privacy that the script could not express. Added 14 CLI flags (see "Advanced CLI flags" above) so future clients with the same shape can be served without one-off scripts. Two UI-only toggles ("Background image from ad", "Flexible delivery Optimized") have no Graph API field — flagged in Slack report.
- 2026-05-27 — ACME Agency: inject mode uploaded 3 videos (~90 MB total) before failing at
/adcreativeswith "You don't have required permission to access this profile". Root cause: ACME Agency's Page was aclient_pagein Paradox BM but the system user behindMETA_ACCESS_TOKENhad no Page-level task permission. Existing ads on the same campaign worked because they were created via Ads Manager UI (user token). Now caught byverifyInjectAccess()at Step 0 of bothrunMetaLeadAdsInjectandrunMetaLeadAds— the error message names the exact BM screen to fix and aborts before any upload. - 2026-05-28 — ACME Agency: SLO inject failed with "The page associated with this lead form is different with the page selected in Ad Set." ACME Agency runs two pages (German
<id>"Bioklimatische Pergolen" + SLO<id>"ACME Agency");clients.jsonrecorded only the German one. Inject now derives the page (and matching IG) from the ad set'spromoted_object.page_idrather thanclients.json, so multi-page clients work automatically. Theclients.jsonentry also gained apagesmap for reference. - 2026-05-28 — ACME Agency slide-10 inject: two gaps fixed. (1)
Visuals (3.png):wasn't parsed (regex requiredVisuals:with nothing between label and colon) — added the parenthetical form + named-file selection. (2) Per-type sequence naming (Single Image 01) existed only in uncommitted sandbox WIP and was lost on a git sync — restored<id>()+buildAdName()and wired into all three creation paths. - 2026-05-29 — ACME Agency (SLO inject):
findLeadFormByNameresolved forms only by scanning existing ads'object_story_spec, so a freshly-created form not yet on any ad ("ACME Agency - form 02 (Slovenija)") couldn't be found. Added a Page-token fallback (mintGET /{page_id}?fields=access_token→ listGET /{page_id}/leadgen_forms). Form resolution also moved to AFTER page derivation so the fallback queries the correct (ad-set) page.clients.jsonACME Agency gainedform_02_slovenija(<id>). - 2026-06-01 — ACME Agency Nekretnine (lead-form inject): brand-new client with
website: null→ the creative link defaulted tofacebook.com→ Meta hard-rejected the ads ("Lead Generation Ads should always link to external content") AFTER a full video upload. Lead-form flows (inject + full-create) now fall back to the Page's ownwebsitefor the creative link, and only abort if the Page has none either. Confirms the team norm: a "Meta Lead Form" brief never needs a web link from the operator. - 2026-06-03 — ACME Agency (named videos): the visuals matcher honored named files only for IMAGES; VIDEOS were always taken alphabetically, so
Visuals (…, Video_1.mp4)silently attached the wrong video (Blefaroplastika VIDEO.mp4sorted first). Now both pools are narrowed by the named-file list — a name resolves into whichever pool it exists in. Also backfilled ACME Agencywebsite: https://go.ACME Agency.hr/(its single-image ads need a real external link; see the ACME Agency fallback). - 2026-06-03 — ACME Agency (
Tip:override): a brief pairedCopy (slide 3)(a VIDEO slide in the deck) withVisuals (1.png)to build a single-image ad. The type-aware matcher demanded a video for slide 3, so it couldn't be served without a one-off script. Added theTip: single image|video|carouseldirective (parsed inparseInjectTargets, applied before the visuals block) to force the ad type. - 2026-06-17 — ACME Agency (new client, full create): the lead-form parse silently produced a near-empty form (only
EMAIL, zero qualifying questions, wrong thank-you) because the deck split the form across 3 slides in a labeled format: slide 5 = form spec withHeadline:/Description:/Questions:labels, slide 6 = thank-you screen, slide 7 = the GHL autoresponder email. Three root causes fixed: (1) the email slide (THANK YOU EMAIL) matched/email/+/zahvalj/→ scoredLEAD_FORM→ overwrote slide 5;classifySlidenow returns UNKNOWN for any slide with a standaloneTHANK YOU EMAILmarker. (2) the thank-you screen (slide 6) only hit 1 form-indicator → UNKNOWN → lost; an explicitMETA LEAD FORM/INSTANT FORMmarker now forcesLEAD_FORM, and the parse loop collects ALLLEAD_FORMslides in deck order and merges them into one form (so spec + thank-you screen on separate slides work). (3)parseLeadFormSlidenow strips leadingHeadline:/Description:/Questions:/Naslov:labels and drops marker-only lines so they don't leak into the title/description. Files:lib/google_slides.mjs(classifySlide),meta_lead_ads.mjs(parse loop +parseLeadFormSlide). The script also has no detailed-interest targeting — the owner/business-interest layer (flexible_spec) was applied to the ad set by a post-create GraphPOST(behaviorSmall business owners<id> + interests Entrepreneurship/Business networking/Business model). Campaign<id>, form<id>, all PAUSED.