--- name: drafts description: "Default write workflow for Spotify Ads API campaigns, ad sets, and ads. Stage new entities or changes to published entities as drafts, validate them, and publish only after explicit confirmation. Use for create, change, update, adjust, fix, pause, resume, archive, creative, tracking, or other campaign-hierarchy writes even when the user does not say draft." argument-hint: "build | stage-edit | list|get|edit|validate|publish|delete|draft-from" allowed-tools: ["Read", "Bash", "AskUserQuestion"] --- # Spotify Ads API — Draft Campaign Management Build, review, and publish campaign hierarchies using the draft workflow. Drafts are staging versions of campaigns, ad sets, and ads that can be created, edited, validated, and published as a batch — nothing goes live until you explicitly publish. ## Why Drafts The draft flow is the **preferred** way to create campaigns because: - **Review before going live** — build the entire hierarchy, then validate and publish in one step - **Batch validation** — the validate action checks the entire campaign hierarchy (campaign + ad sets + ads) at once, surfacing all errors before anything is created - **Safe iteration** — edit any part of the draft hierarchy without affecting live entities - **Undo-friendly** — delete drafts at any time before publishing; no cleanup of live entities needed - **Incomplete data allowed** — drafts accept partial entities (e.g., an AUDIO ad without `companion_asset_id`). Fields like `asset_id`, `clickthrough_url`, and `tagline` are optional for draft ads and only required when publishing or creating live ads. Required fields are only enforced during VALIDATE or PUBLISH, so you can build the hierarchy incrementally The alternative (direct entity creation or updates via `spotify-ads-api:campaigns`, `/ad_sets`, `spotify-ads-api:ads`) writes to published entities immediately and may require permissions unavailable to some accounts or credentials. Prefer the draft flow for every campaign hierarchy create or modify request, not only complete campaign builds. ## Setup Set this skill's folder and define the request wrapper: ```bash SKILL_DIR="" api() { "$SKILL_DIR/scripts/api-request.sh" drafts "$@"; } ``` 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. ## Operations Parse the user's argument to determine the operation. For `stage-edit`, `get`, `edit`, `delete`, and `draft-from`, require an entity type (`campaign`, `ad-set`, or `ad`) with the ID. If the user provides only a bare UUID, infer the type only when the current conversation makes it unambiguous; otherwise ask which entity type they mean. Use `stage-edit` automatically for ordinary requests to change a published campaign, ad set, or ad. The user does not need to mention drafts. --- ### `stage-edit ` — Safely Stage Changes to a Published Entity This is the default operation for natural-language changes to published campaign hierarchy entities. #### Step 1: Read the published entity Use the corresponding published GET endpoint to verify the ID, capture the current values, and resolve parent IDs when needed. #### Step 2: Check for an existing draft Drafts created from published entities reuse the same entity ID. Check the corresponding draft GET endpoint before creating anything: ```bash api GET "ad_accounts/{ad_account_id}/drafts//$ENTITY_ID" ``` - HTTP 200: a draft already exists. Display its current pending state and explain that the requested changes will be combined with it. Do not overwrite or discard fields that the user did not request to change. - HTTP 404: no draft exists; continue to Step 3. - Any other response: show the error and stop. #### Step 3: Create a draft when needed Only after a 404 from Step 2, create the draft from the published entity: ```bash api POST "ad_accounts/{ad_account_id}/campaigns/$CAMPAIGN_ID/drafts" api POST "ad_accounts/{ad_account_id}/ad_sets/$AD_SET_ID/drafts" api POST "ad_accounts/{ad_account_id}/ads/$AD_ID/drafts" ``` Use only the endpoint matching the selected entity type. Do not send a request body. #### Step 4: Patch the draft Build a minimal patch containing only requested changes and send it to the corresponding draft endpoint: ```bash api PATCH "ad_accounts/{ad_account_id}/drafts/campaigns/$CAMPAIGN_ID" '{...}' api PATCH "ad_accounts/{ad_account_id}/drafts/ad_sets/$AD_SET_ID" '{...}' api PATCH "ad_accounts/{ad_account_id}/drafts/ads/$AD_ID" '{...}' ``` For `third_party_tracking`, read and preserve every existing entry the user did not explicitly remove or replace. Send the complete intended tracking array and set `measurement_event` explicitly for every entry, especially `CLICKED` versus `IMPRESSION`. #### Step 5: Resolve and validate the parent draft campaign - Campaign draft: its ID is the draft campaign ID. - Ad set draft: use its returned `campaign_id`. - Ad draft: fetch its draft ad set by `ad_set_id`, then use the ad set's `campaign_id`. Fetch that draft campaign immediately before validation, use its current `draft_hierarchy_version`, and run `VALIDATE`. Report the change as **staged**, including the affected entity ID and validation result. Do not publish as part of `stage-edit`. Publishing is a separate operation that always requires explicit confirmation immediately before the request. --- ### `build` — Create a Full Draft Campaign Hierarchy Given a plain-text campaign description, create the full draft hierarchy: draft campaign → draft ad sets → draft ads. This mirrors `spotify-ads-api:build-campaign` but creates drafts instead of live entities. #### Step 1: Parse the Campaign Description Extract fields exactly as documented in the `build-campaign` skill. The same field requirements, defaults, and validation guardrails apply (micro-amounts, bid_strategy as plain string, geo_targets as flat object, platform enums, etc.). #### Step 1.5: Load Ad Product Rules Read and follow `$SKILL_DIR/references/ad-product-validation.md`. Fetch the live catalog once for this draft-build workflow, resolve the planned campaign product, and use the applicable rules while constructing the draft plan. Do not display a per-field checklist. #### Step 2: Confirm the Parsed Plan Present the plan as a visual tree, clearly labeled as **DRAFT**: ``` DRAFT Campaign: "My Campaign" (AWARENESS) ├── DRAFT Ad Set 1: "Ad Set A" (AUDIO, $75/day, US, ages 25-54, Mar 1 start) │ └── DRAFT Ad 1: "My Ad" → SHOP_NOW → example.com └── DRAFT Ad Set 2: "Ad Set B" (VIDEO, $500 lifetime, US, ages 18-54, Mar 4–Apr 4) └── DRAFT Ad 2: "My Video Ad" → LEARN_MORE → example.com ``` Ask the user to confirm or adjust before creating drafts. #### Step 3: Prompt for Assets Fetch available assets from the account and present them for selection, just like the `build-campaign` skill: ```bash api GET "ad_accounts/{ad_account_id}/assets?limit=50&sort_direction=DESC" ``` #### Step 3.5: Validate Against Ad Product Rules Using the catalog loaded in Step 1.5, validate the complete draft hierarchy now that assets and dependent fields are known. Apply resolvable runtime checks as well as static rules. Do not print per-field successes or add another confirmation. Add a compact validation status to the existing draft plan, and interrupt only for an explicit incompatible user choice or when no safe compliant value can be inferred. Catalog preflight does not replace the draft hierarchy `VALIDATE` action in Step 5. #### Step 4: Create Draft Entities Sequentially **4a. Create Draft Campaign:** ```bash api POST "ad_accounts/{ad_account_id}/drafts/campaigns" \ '{"name":"...","delivery_goal_group":"..."}' ``` Map the user's campaign goal to `delivery_goal_group`: | User goal | `delivery_goal_group` | |---|---| | Awareness, reach, brand recall, even impression delivery | `AWARENESS` | | Website traffic, clicks, website visits | `WEBSITE_TRAFFIC` | | App installs or mobile app promotion | `APP_PROMOTION` | | Video views, podcast streams, or on-platform engagement | `ENGAGEMENT_ON_SPOTIFY` | | Lead generation | `LEAD_GEN` | Do not set the deprecated `objective` field on draft campaigns. Extract the draft campaign `id` from the response. The response includes an initial `draft_hierarchy_version`, but do not rely on that value after creating child draft ad sets or ads because any hierarchy edit can increment the version. **4b. Create Draft Ad Sets** (using `campaign_id` = draft campaign ID from 4a): ```bash api POST "ad_accounts/{ad_account_id}/drafts/ad_sets" \ '{ "campaign_id": "", "name": "...", "start_time": "...", "end_time": "...", "budget": {"micro_amount": ..., "type": "..."}, "asset_format": "...", "category": "ADV_X_Y", "targets": { ... }, "bid_strategy": "MAX_BID", "bid_micro_amount": ... }' ``` Extract each draft ad set `id`. **4c. Create Draft Ads** (using `ad_set_id` = draft ad set ID from 4b): ```bash api POST "ad_accounts/{ad_account_id}/drafts/ads" \ '{ "ad_set_id": "", "name": "...", "tagline": "...", "advertiser_name": "...", "assets": { "asset_id": "...", "logo_asset_id": "...", "companion_asset_id": "..." }, "call_to_action": { "key": "SHOP_NOW", "clickthrough_url": "https://..." }, "third_party_tracking": [ {"measurement_event": "IMPRESSION", "measurement_partner": "DCM", "url": "https://...trackimp/..."}, {"measurement_event": "CLICKED", "measurement_partner": "DCM", "url": "https://...trackclk/..."} ] }' ``` #### Step 5: Validate the Draft After all draft entities are created, fetch the draft campaign again to get the current `draft_hierarchy_version`, then automatically run validation. Do not reuse a version captured before child draft ad sets or ads were created. ```bash api GET "ad_accounts/{ad_account_id}/drafts/campaigns/$DRAFT_CAMPAIGN_ID" ``` ```bash api POST "ad_accounts/{ad_account_id}/drafts/campaigns/$DRAFT_CAMPAIGN_ID" \ '{"action":"VALIDATE","draft_hierarchy_version":}' ``` The `draft_hierarchy_version` must match the current value from the draft campaign response. **If validation succeeds** (HTTP 200 with `validation_errors: null`): - Display a success summary with the full draft hierarchy - Ask the user: **Publish now** or **Keep as draft for later** **If validation returns errors** (HTTP 400 with `validation_errors` array): - Display each error with its entity type, entity ID, and message: ``` Validation Errors: ✗ AD_SET (id: abc-123): Ad set targeting is required ✗ AD (id: def-456): Missing companion_asset_id for AUDIO format ``` - Suggest fixes for each error - Ask the user if they want to fix the issues (for example, `edit ad-set ` or `edit ad `) or delete the draft #### Step 6: Summary Display a final summary: | Entity | Draft ID | Name | Status | |--------|----------|------|--------| | Draft Campaign | `uuid` | ... | DRAFT | | Draft Ad Set 1 | `uuid` | ... | DRAFT | | ↳ Draft Ad 1 | `uuid` | ... | DRAFT | Include the `draft_hierarchy_version` and remind the user they can: - **Validate**: `spotify-ads-api:drafts validate ` - **Publish**: `spotify-ads-api:drafts publish ` - **Edit**: `spotify-ads-api:drafts edit ` --- ### `list` — List Drafts List draft entities. Argument specifies which type: `campaigns`, `ad-sets`, or `ads`. Default to `campaigns`. **List draft campaigns:** ```bash api GET "ad_accounts/{ad_account_id}/drafts/campaigns?limit=50&sort_direction=DESC" ``` Format as table: Draft ID | Name | Status | Delivery Goal Group | Version | Created Optional filters use the actual query parameter names: `campaign_ids` (repeated param), `channel`, `statuses` (repeated param), `sort_field`, `sort_direction`. **List draft ad sets:** ```bash api GET "ad_accounts/{ad_account_id}/drafts/ad_sets?limit=50" ``` Format as table: Draft ID | Name | Campaign ID | Status | Format | Budget Optional filters: `campaign_ids` (repeated param), `statuses` (repeated param). **List draft ads:** ```bash api GET "ad_accounts/{ad_account_id}/drafts/ads?limit=50" ``` Format as table: Draft ID | Name | Ad Set ID | Status Optional filters: `ad_set_ids` (repeated param), `statuses` (repeated param). --- ### `get ` — Get a Draft by ID Use the entity type from the command to select the endpoint. If the user supplies only a bare ID, infer the type only when prior context is unambiguous; otherwise ask the user for `campaign`, `ad-set`, or `ad`. **Draft campaign:** ```bash api GET "ad_accounts/{ad_account_id}/drafts/campaigns/$DRAFT_CAMPAIGN_ID" ``` **Draft ad set:** ```bash api GET "ad_accounts/{ad_account_id}/drafts/ad_sets/$DRAFT_AD_SET_ID" ``` **Draft ad:** ```bash api GET "ad_accounts/{ad_account_id}/drafts/ads/$DRAFT_AD_ID" ``` Display all fields in a readable format. Note that `draft_hierarchy_version` is only populated on campaign drafts; ad set and ad drafts return `null` for this field. --- ### `edit ` — Update a Draft Use the entity type from the command to select the endpoint, then prompt the user for fields to update. The same field validations as create apply. Before the PATCH, read and follow `$SKILL_DIR/references/ad-product-validation.md`. Fetch the live catalog and current draft entity, then traverse its actual parent chain: draft ad → draft ad set → draft campaign, or draft ad set → draft campaign. Deep-merge the PATCH into the current entity and validate the effective result against the matching entity's `update` (when present) plus `both` rules. Do not print a checklist or add another confirmation. **Update draft campaign:** ```bash api PATCH "ad_accounts/{ad_account_id}/drafts/campaigns/$DRAFT_CAMPAIGN_ID" \ '{"name":"...","delivery_goal_group":"..."}' ``` Updatable campaign fields: `name`, `purchase_order`, `delivery_goal_group`, `status`. Do not introduce the deprecated `objective` field when editing drafts. **Update draft ad set:** ```bash api PATCH "ad_accounts/{ad_account_id}/drafts/ad_sets/$DRAFT_AD_SET_ID" \ '{...}' ``` Updatable ad set fields: `name`, `start_time`, `end_time`, `budget`, `bid_micro_amount`, `bid_strategy`, `targets`, `pacing`, `asset_format`, `category`, `frequency_caps`, `cost_model`, `delivery_goal`, `promotion`, `video_delivery_formats`, `status`. **Update draft ad:** ```bash api PATCH "ad_accounts/{ad_account_id}/drafts/ads/$DRAFT_AD_ID" \ '{...}' ``` Updatable ad fields: `name`, `advertiser_name`, `tagline`, `assets`, `asset_format`, `call_to_action`, `third_party_tracking`, `placements`, `weight`, `status`. After updating, display the updated draft. For campaign drafts, note the new `draft_hierarchy_version`. For ad set and ad drafts, `draft_hierarchy_version` is `null` in the response — fetch the parent draft campaign to get the updated version. --- ### `validate ` — Validate a Draft Campaign Dry-run the publish to check for errors across the entire hierarchy (campaign + all ad sets + all ads): ```bash api POST "ad_accounts/{ad_account_id}/drafts/campaigns/$DRAFT_CAMPAIGN_ID" \ '{"action":"VALIDATE","draft_hierarchy_version":}' ``` First, fetch the draft campaign to get the current `draft_hierarchy_version`: ```bash api GET "ad_accounts/{ad_account_id}/drafts/campaigns/$DRAFT_CAMPAIGN_ID" ``` **Handling the response:** - **HTTP 200** — validation passed. The response is a `PublishCampaignResult` with `validation_errors: null` and optionally `campaign` data. - **HTTP 400** — validation failed. The response contains a `validation_errors` array of `HierarchyValidationError` objects. Each `HierarchyValidationError` has: - `validation_entity_type`: `CAMPAIGN`, `AD_SET`, or `AD` - `validation_entity_id`: UUID of the failing entity - `message`: human-readable error description **If no validation errors (HTTP 200):** ``` ✓ Draft campaign "My Campaign" passed validation. Campaign + 2 ad sets + 3 ads are ready to publish. Publish now? spotify-ads-api:drafts publish ``` **If validation errors exist (HTTP 400):** ``` ✗ Draft campaign "My Campaign" has validation errors: Entity | ID | Error AD_SET | abc-123 | Budget micro_amount is required AD | def-456 | Missing companion_asset_id for AUDIO format AD | ghi-789 | call_to_action.clickthrough_url is required Fix these issues with: spotify-ads-api:drafts edit Then re-validate with: spotify-ads-api:drafts validate ``` --- ### `publish ` — Publish a Draft Campaign Publish the entire draft hierarchy (campaign + ad sets + ads) as live entities. Published entities retain the same IDs they had as drafts. **Always validate first** before publishing. #### Step 1: Fetch the draft campaign to get `draft_hierarchy_version` ```bash api GET "ad_accounts/{ad_account_id}/drafts/campaigns/$DRAFT_CAMPAIGN_ID" ``` #### Step 2: Run validation first ```bash api POST "ad_accounts/{ad_account_id}/drafts/campaigns/$DRAFT_CAMPAIGN_ID" \ '{"action":"VALIDATE","draft_hierarchy_version":}' ``` If validation errors exist, display them and stop. Do not publish with validation errors. #### Step 3: Confirm with the user Show the full draft hierarchy that will be published and ask for confirmation: ``` Ready to publish draft campaign "My Campaign": ├── Ad Set 1: "Ad Set A" (AUDIO, $75/day) │ └── Ad 1: "My Ad" └── Ad Set 2: "Ad Set B" (VIDEO, $500 lifetime) └── Ad 2: "My Video Ad" This will create live entities. Proceed? ``` #### Step 4: Publish Fetch the draft campaign again immediately before publishing. If the `draft_hierarchy_version` differs from the version that just passed validation, re-run validation before publishing. ```bash api GET "ad_accounts/{ad_account_id}/drafts/campaigns/$DRAFT_CAMPAIGN_ID" ``` ```bash api POST "ad_accounts/{ad_account_id}/drafts/campaigns/$DRAFT_CAMPAIGN_ID" \ '{"action":"PUBLISH","draft_hierarchy_version":}' ``` Display the published campaign details from the response. Published entities retain the same IDs they had as drafts. --- ### `delete ` — Delete a Draft Use the entity type from the command to select the endpoint. If the user supplies only a bare ID, infer the type only when prior context is unambiguous; otherwise ask. **Delete draft campaign** (also removes associated draft ad sets and ads): ```bash api DELETE "ad_accounts/{ad_account_id}/drafts/campaigns/$DRAFT_CAMPAIGN_ID" ``` **Delete draft ad set:** ```bash api DELETE "ad_accounts/{ad_account_id}/drafts/ad_sets/$DRAFT_AD_SET_ID" ``` **Delete draft ad:** ```bash api DELETE "ad_accounts/{ad_account_id}/drafts/ads/$DRAFT_AD_ID" ``` Expect a `204 No Content` response on success. --- ### `draft-from ` — Create a Draft from a Published Entity Create a draft copy of an existing live campaign, ad set, or ad for editing. Use the entity type from the command to select the endpoint. This is useful for making changes to published entities via the draft → edit → validate → publish workflow. **Draft from published campaign:** ```bash api POST "ad_accounts/{ad_account_id}/campaigns/$CAMPAIGN_ID/drafts" ``` **Draft from published ad set:** ```bash api POST "ad_accounts/{ad_account_id}/ad_sets/$AD_SET_ID/drafts" ``` **Draft from published ad:** ```bash api POST "ad_accounts/{ad_account_id}/ads/$AD_ID/drafts" ``` The draft-from-published response reuses the **same ID** as the live entity (not a new UUID). The status becomes `ACTIVE_RESTRICTED`. For a campaign draft, the returned entity is the draft campaign used for validate/publish. For an ad set draft, use its `campaign_id` as the draft campaign ID for validate/publish. For an ad draft, fetch the draft ad set referenced by its `ad_set_id`, then use that draft ad set's `campaign_id` as the draft campaign ID. Display the created draft and suggest next steps: - Edit: `spotify-ads-api:drafts edit ` - Validate: `spotify-ads-api:drafts validate ` - Publish: `spotify-ads-api:drafts publish ` --- ## Critical Schema Notes Unlike direct entity creation, draft creation accepts incomplete data — required fields are only enforced during VALIDATE or PUBLISH actions. This is a key benefit of drafts: save work in progress and complete it later. 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 for AUDIO format ads at publish/validation time, but can be omitted when creating draft ads 7. **`call_to_action`** uses field name `key` (not `type`) and `clickthrough_url` (not `url`) 8. **`third_party_tracking`** uses field `measurement_event` (NOT `type`) to distinguish tracker categories. Valid values: `IMPRESSION`, `CLICKED`, `START`, `FIRST_QUARTILE`, `MIDPOINT`, `THIRD_QUARTILE`, `COMPLETE`, `VIEWABLE_IMPRESSION`. **If `measurement_event` is omitted, it defaults to IMPRESSION** — always set it explicitly. Use `CLICKED` for click trackers and `IMPRESSION` for impression trackers. 9. Budget amounts must be in **micro-units** (multiply amount by 1,000,000) 10. **`draft_hierarchy_version`** is required when publishing or validating — always fetch the current version from the **draft campaign** immediately before calling publish/validate; never reuse a version captured before child drafts or edits. This field is only populated on draft campaign responses; ad set and ad draft responses return `null`. The version on the campaign increments when any entity in the hierarchy is created or edited 11. **Draft ad set `campaign_id`** must reference the **draft campaign ID**, not a published campaign ID 12. **Draft ad `ad_set_id`** must reference a **draft ad set ID**, not a published ad set ID ## Execution Behavior - If `auto_execute` is `true`, execute read, create, edit, delete, and validate calls directly after any required planning or asset-selection confirmation. - If `auto_execute` is `false`, present the curl command to the user and ask for confirmation before executing. - Never auto-execute `PUBLISH`. Publishing creates live entities and always requires explicit user confirmation immediately before the publish request. - 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 from the response body. Never automatically retry POST or PATCH requests. - **Draft DELETE is safe to retry** — unlike POST/PATCH, DELETE on drafts is idempotent. - If an explicitly requested direct/live campaign hierarchy write fails with HTTP 403 or an edit-permission error, describe it as a denial of direct editing only. Do not infer that the credentials are entirely read-only or identify a specific organizational role. Offer `stage-edit` as the compatible alternative. ## Draft vs Direct Entity Creation | Aspect | Draft Flow (Preferred) | Direct Flow | |--------|----------------------|-------------| | Validation | Batch — validate entire hierarchy at once | Per-entity — errors discovered one at a time | | Going live | Explicit publish step | Immediate on creation | | Editing | Free — edit any draft entity before publish | Requires PATCH on live entities | | Undo | Delete the draft — no cleanup needed | Must archive/pause live entities | | Use case | Default for new entities and changes to published entities | Only explicit immediate/direct live writes |