--- name: build-campaign description: Create a full campaign (campaign + ad sets + ads) from a plain-text description. Parses natural language into structured API calls. Prefers the draft workflow for safer creation with batch validation. argument-hint: "" allowed-tools: ["Read", "Bash"] --- # Spotify Ads API — Full Campaign Builder Given a plain-text description of an advertising campaign, parse it into structured API calls and create the full campaign hierarchy: Campaign → Ad Sets → Ads. ## Default Flow: Draft → Validate → Publish By default, use the **draft workflow** for all campaign hierarchy creation. This creates draft entities first, validates the entire hierarchy, and only publishes after confirmation. Route to the `spotify-ads-api:drafts build ` skill to execute the draft flow. The draft flow is preferred because: - **Batch validation** catches all errors across the hierarchy before anything goes live - **Safe iteration** — the user can review and edit drafts before publishing - **Easy undo** — delete the draft if something looks wrong; no live entities to clean up Publishing a draft always requires explicit user confirmation immediately before the `PUBLISH` request, even when `auto_execute` is enabled. Only use the direct creation flow below if the user explicitly asks to skip drafts or create live entities immediately. If a direct write is denied, do not infer that the credentials are read-only; offer the draft workflow instead. ## Direct Creation Flow (Legacy) ## Setup Set this skill's folder and define the request wrapper: ```bash SKILL_DIR="" api() { "$SKILL_DIR/scripts/api-request.sh" build-campaign "$@"; } ``` Before the first Ads API v3 call, read and follow `$SKILL_DIR/references/live-openapi.md`. To retrieve settings values (TOKEN, AD_ACCOUNT_ID, AUTO_EXECUTE, BASE_URL, SDK_HEADER, SKILL_HEADER, PLUGIN_VERSION) for use outside API calls, run `api --env`. The output is eval-safe, so `eval $(api --env)` assigns them all. ## Step 1: Parse the Campaign Description Extract the following from the user's plain-text input. If a field is missing or ambiguous, use the defaults noted below. If a required field cannot be inferred, ask the user. ### Campaign-level fields | Field | Required | Default | |-------|----------|---------| | name | yes | — | | objective | yes | REACH | | ad_product | no | UNSET (resolves to AUCTION) | Valid objectives: `REACH`, `CLICKS`, `VIDEO_VIEWS`, `CONVERSIONS`, `LEAD_GEN`, `EVEN_IMPRESSION_DELIVERY`, `PODCAST_STREAMS`, `APP_INSTALLS`, `WEBSITE_VISITS` ### Ad set-level fields (one or more) | Field | Required | Default | Notes | |-------|----------|---------|-------| | name | yes | — | 2-200 chars | | start_time | yes | — | ISO 8601 UTC | | end_time | required if LIFETIME | — | ISO 8601 UTC | | budget.micro_amount | yes | — | Amount (in ad account's billing currency) x 1,000,000 | | budget.type | yes | DAILY | `DAILY` or `LIFETIME` | | asset_format | yes | AUDIO | `AUDIO`, `VIDEO`, `IMAGE`, or `CATALOG` | | category | yes | — | Valid `ADV_X_Y` code (fetch from `GET /ad_categories` if needed) | | bid_strategy | yes | MAX_BID | Plain string: `MAX_BID`, `COST_PER_RESULT`, `AUTOBID`, or `UNSET` | | bid_micro_amount | yes with MAX_BID/COST_PER_RESULT | 15000000 | Bid cap in micro-units. Not required with AUTOBID. | | pacing | no | PACING_EVEN | `PACING_EVEN` or `PACING_ASAP` | | delivery | no | ON | `ON` or `OFF` | | targets.age_ranges | yes | [{"min":18,"max":54}] | Array of `{min, max}` objects | | targets.geo_targets | yes | {"country_code":"US"} | **Flat object** with `country_code` string | | targets.platforms | no | ["ANDROID","DESKTOP","IOS"] | Valid: `ANDROID`, `DESKTOP`, `IOS` | | targets.placements | yes | ["MUSIC"] | `MUSIC` or `PODCAST` | | targets.genders | no | [] | `MALE`, `FEMALE`, `NON_BINARY` | **Ad set validation guardrails:** - Reject or ask to correct zero/negative budgets and zero bids. `budget.micro_amount` and `bid_micro_amount` must be positive when present. - Do not include `currency` in ad set `budget`; currency is only required in `/estimates/audience` budget payloads. - Do not send `cost_model`, `skippable`, `is_skippable`, or `ad_platforms` in ad set create payloads. - Only use `ANDROID`, `DESKTOP`, and `IOS` in `targets.platforms`; never use `WEB`, `MOBILE`, or `CONNECTED_DEVICE`. - Use `min >= 18` for age ranges unless the user explicitly confirms a market/category that allows minors. - When geo refinements are present (`city_ids`, `postal_code_ids`, `region_ids`), include `country_code` in the same `geo_targets` object. - If `bid_strategy=UNSET`, omit `bid_micro_amount` unless the API response or user-provided source explicitly requires it. ### Ad-level fields (one or more per ad set) | Field | Required | Notes | |-------|----------|-------| | name | yes | 2-200 chars | | tagline | yes (optional for drafts) | 2-40 chars | | advertiser_name | yes | 2-25 chars | | assets.asset_id | yes (optional for drafts) | UUID — prompt user to select | | assets.logo_asset_id | yes | UUID — prompt user to select | | assets.companion_asset_id | yes (audio) | UUID — required for AUDIO format ads | | call_to_action.key | yes | e.g. `SHOP_NOW`, `LEARN_MORE`, `LISTEN_NOW`, `SIGN_UP` | | call_to_action.clickthrough_url | yes (optional for drafts) | Landing page URL | | delivery | no | `ON` (default) or `OFF` | ## Step 1.5: Load Ad Product Rules Read and follow `$SKILL_DIR/references/ad-product-validation.md`. Fetch the live catalog once for this workflow, resolve the planned campaign's product, and use the applicable rules while constructing the plan. Do not display a per-field checklist. ## Step 2: Confirm the Parsed Plan Before making any mutating API calls, present the full parsed plan as a visual tree: ``` Campaign: "My Campaign" (objective: REACH) ├── Ad Set 1: "Ad Set A" (AUDIO, $75/day, US, ages 25-54, Mar 1 start) │ └── Ad 1: "My Ad" → SHOP_NOW → example.com └── Ad Set 2: "Ad Set B" (VIDEO, $500 lifetime, US, ages 18-54, Mar 4–Apr 4) └── Ad 2: "My Video Ad" → LEARN_MORE → example.com ``` Also show a table with all field values for each entity. Ask the user to confirm or adjust. If the ad category was not specified, ask the user to select one. You can fetch valid categories from `GET /ad_categories` to present options. ## Step 2.5: Validate Audience Size After the user confirms the plan but before executing API calls, run an audience estimate for each ad set's targeting: ```bash api POST "estimates/audience" \ '{ "ad_account_id": "", "start_date": "", "asset_format": "", "objective": "", "bid_strategy": "", "bid_micro_amount": , "budget": {"micro_amount": , "type": "", "currency": "USD"}, "targets": { } }' ``` **Important:** This endpoint is NOT scoped under `/ad_accounts/{id}/` — it's at the top level: `POST /estimates/audience`. Use the base URL directly followed by `/estimates/audience`. Display the estimate results in a summary: ``` Audience Estimate for "Ad Set A": Projected unique users: ~142,000 Estimated daily reach: 8,500 – 12,000 Estimated daily impressions: 15,000 – 22,000 Estimated CPM: $12.50 – $18.00 Likely to deliver budget: Yes ``` Convert any CPM micro-amounts to the ad account's billing currency for display. **If the audience is too small** (very low `projected_unique_users` or the API returns a 400 error indicating audience too small), warn the user and suggest: - Broadening the age range - Adding more platforms - Removing restrictive targeting (artist/genre/interest) - Switching from VIDEO to AUDIO format (lower thresholds) - Expanding geo targeting Ask the user whether to: 1. Proceed anyway with current targeting 2. Adjust targeting (then re-estimate) 3. Cancel this ad set Run the estimate for each ad set in the plan before proceeding to Step 3. ## Step 3: Prompt for Assets For each ad, fetch available assets from the account: ```bash api GET "ad_accounts/{ad_account_id}/assets?limit=50&sort_direction=DESC" ``` Present audio/video assets and image assets separately in tables, and ask the user to pick: - **asset_id** — the creative (must match the ad set's `asset_format`: audio for AUDIO, video for VIDEO, etc.) - **logo_asset_id** — a logo image - **companion_asset_id** — a companion image (required for AUDIO format ads) ## Step 3.5: Validate the Final Hierarchy Using the catalog loaded in Step 1.5, validate the complete campaign, ad set, and ad request bodies now that assets and all dependent fields are known. Apply the canonical procedure's static and runtime checks, including asset lookups and the audience estimate above. Never send a known-invalid request. Do not add another confirmation or print per-field successes. If the existing plan summary is still visible, one compact validation status line is sufficient. Surface a failure only when an explicit user choice must change or no safe compliant value can be inferred. ## Step 4: Execute API Calls Sequentially Execute each step in order, passing IDs forward from each response. ### 4a. Create Campaign Include `ad_product` when the resolved destination product is CONTENT or FPMNG. Omit it for the default AUCTION flow. ```bash api POST "ad_accounts/{ad_account_id}/campaigns" \ '{"name":"...","objective":"..."}' ``` Extract the campaign `id` from the response. ### 4b. Create Ad Sets (using campaign_id from 4a) ```bash api POST "ad_accounts/{ad_account_id}/ad_sets" \ '{ "name": "...", "campaign_id": "", "start_time": "...", "end_time": "...", "budget": {"micro_amount": ..., "type": "..."}, "asset_format": "...", "category": "ADV_X_Y", "targets": { "age_ranges": [{"min": ..., "max": ...}], "geo_targets": {"country_code": "..."}, "platforms": ["ANDROID", "DESKTOP", "IOS"], "placements": ["MUSIC"] }, "bid_strategy": "MAX_BID", "bid_micro_amount": ..., "pacing": "PACING_EVEN", "delivery": "ON" }' ``` Extract each ad set `id` for use in ad creation. ### 4c. Create Ads (using ad_set_id from 4b) ```bash api POST "ad_accounts/{ad_account_id}/ads" \ '{ "name": "...", "ad_set_id": "", "tagline": "...", "advertiser_name": "...", "assets": { "asset_id": "...", "logo_asset_id": "...", "companion_asset_id": "..." }, "call_to_action": { "key": "SHOP_NOW", "clickthrough_url": "https://..." }, "delivery": "ON" }' ``` ## Step 5: Summary After all entities are created, display a final summary table: | Entity | ID | Name | Status | |--------|----|------|--------| | Campaign | `uuid` | ... | ... | | Ad Set 1 | `uuid` | ... | ... | | ↳ Ad 1 | `uuid` | ... | ... | | Ad Set 2 | `uuid` | ... | ... | | ↳ Ad 2 | `uuid` | ... | ... | ## Execution Behavior - If `auto_execute` is `true`, execute each API call directly after presenting the plan. - If `auto_execute` is `false`, present the full plan and ask for confirmation before executing. Then execute all calls in sequence without additional confirmation per call. - Always check the `HTTP_STATUS:` line from curl output to determine success or failure before interpreting the response body. - On error, show the error message and stop. Do not continue creating dependent entities if a parent fails. Never automatically retry a POST — if a campaign/ad set/ad creation fails with a 5xx, check if the entity was actually created (e.g., list campaigns) before suggesting a retry. ## Critical Schema Notes These are non-obvious API requirements that MUST be followed: 1. **`bid_strategy`** is a plain STRING enum, NOT an object. Valid: `MAX_BID`, `COST_PER_RESULT`, `AUTOBID`, `UNSET` 2. **`geo_targets`** is a flat object `{"country_code": "US"}`, NOT an array of objects 3. **`platforms`** valid values are `ANDROID`, `DESKTOP`, `IOS` — NOT "MOBILE" or "CONNECTED_DEVICE" 4. **`category`** is required on ad sets — must be a valid `ADV_X_Y` code from `GET /ad_categories` 5. **`end_time`** is required when budget type is `LIFETIME` 6. **`companion_asset_id`** is required when creating ads for AUDIO ad sets 7. **`call_to_action`** uses field name `key` (not `type`) and `clickthrough_url` (not `url`) 8. Budget amounts must be in **micro-units** (multiply amount by 1,000,000) 9. **Min audience thresholds** apply — VIDEO format may require broader targeting than AUDIO. If you get a "Min audience threshold was not met" error, suggest expanding the age range or switching format.