diff --git a/AGENTS.md b/AGENTS.md index ed2d51e..7e78470 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,6 +65,7 @@ These non-obvious API quirks were discovered through real testing and are critic - **Tracking headers**: The request wrapper (`scripts/api-request.sh`) injects `X-Spotify-Ads-Sdk: /` and `X-Spotify-Ads-Skill: ` on every API call. Skills never construct these headers manually — the wrapper reads the plugin version from the platform manifest and takes the skill name as its first argument. This eliminates malformed-header errors seen in production logs (e.g., doubled `X-Spotify-Ads-Sdk: X-Spotify-Ads-Sdk: ...`). - **`entity_status_type` must match `entity_type`** in `aggregate_reports` queries. For example, use `entity_status_type=AD_SET` when `entity_type=AD_SET` — using `entity_status_type=CAMPAIGN` with `entity_type=AD_SET` causes a filter validation error. - **Audience estimates**: The build-campaign and ads skills run `POST /estimates/audience` before creating ad sets to validate targeting. This catches "min audience threshold" errors before they happen. +- **Ad product catalog validation**: Before every campaign, ad set, or ad POST/PATCH, follow `skills/api-reference/references/ad-product-validation.md`. Fetch `GET /ad_product_catalog` once per workflow; do not maintain a timed cross-workflow cache because the endpoint returns `Cache-Control: no-cache, no-store`. Validate final creates and deep-merged effective updates, including required parent/runtime context. Never send a known violation, but do not add per-field success checklists or validation-only confirmation gates. Campaign responses do not consistently expose `ad_product`, so use known workflow context and do not silently infer a reserved CONTENT/FPMNG product. ## OpenAPI Spec diff --git a/agents/spotify-ads-request-builder.md b/agents/spotify-ads-request-builder.md index 2cb25f4..5024dff 100644 --- a/agents/spotify-ads-request-builder.md +++ b/agents/spotify-ads-request-builder.md @@ -97,9 +97,9 @@ You are a Spotify Ads API specialist that translates natural language advertisin - Date descriptions ("last month", "next week") → ISO 8601 datetimes - Status changes ("pause", "stop", "archive") → status field values 4. Identify any missing required fields and ask the user via AskUserQuestion -5. Construct the `api()` helper call(s) with the correct method, path, and JSON body - -6. Before creating any ad set, run a pre-flight audience estimate using `POST /estimates/audience` (top-level endpoint, NOT under `/ad_accounts/{id}/`) with the proposed targeting parameters. Display the estimated reach and impressions. If the audience is too small or the estimate indicates delivery issues, warn the user and suggest targeting adjustments before proceeding. +5. Construct the `api()` helper call(s) with the correct method, path, and JSON body. +6. Before any campaign, ad set, or ad POST/PATCH, read and follow `$PLUGIN_ROOT/skills/api-reference/references/ad-product-validation.md`. Fetch `GET /ad_product_catalog` once for the current workflow, validate final creates or deep-merged effective updates, and never send a known catalog violation. Do not print per-field success checklists or add a validation-only confirmation; surface only incompatible explicit choices or unresolved material issues. +7. Before creating any ad set, run a pre-flight audience estimate using `POST /estimates/audience` (top-level endpoint, NOT under `/ad_accounts/{id}/`) with the proposed targeting parameters. Display the estimated reach and impressions. If the audience is too small or the estimate indicates delivery issues, warn the user and suggest targeting adjustments before proceeding. **Dashboard Routing:** When the user asks about campaign performance, summaries, or dashboard-like views (e.g., "How are my campaigns doing?", "Show me a summary of my ad performance", "What's my spend today?", "Campaign dashboard", "Quick overview of all campaigns"), route them to the `/spotify-ads-api:dashboard` skill. diff --git a/skills/ads/SKILL.md b/skills/ads/SKILL.md index 97182d3..b121570 100644 --- a/skills/ads/SKILL.md +++ b/skills/ads/SKILL.md @@ -38,6 +38,12 @@ api GET "ad_accounts/{ad_account_id}/ad_sets?limit=50&sort_direction=DESC" Format as table: ID | Name | Campaign ID | Status | Format | Budget | Start ### `ad-sets create` +Collect the required fields below first. Before the POST, read and follow +`$PLUGIN_ROOT/skills/api-reference/references/ad-product-validation.md`. Fetch the live +catalog, parent campaign, and any runtime inputs required by its rules, then validate +the final ad set body against `ad_set.create` plus `ad_set.both`. Do not add a separate +validation confirmation. + Prompt for required fields: - **name** (2-200 chars) - **campaign_id** (uuid — suggest listing campaigns first) @@ -189,6 +195,14 @@ api GET "ad_accounts/{ad_account_id}/ad_sets/$AD_SET_ID" ``` ### `ad-sets update ` + +Before the PATCH, read and follow +`$PLUGIN_ROOT/skills/api-reference/references/ad-product-validation.md`. Fetch the live +catalog, current ad set, parent campaign, and runtime state required by applicable +rules. Deep-merge the proposed PATCH into the current ad set and validate the effective +entity against `ad_set.update` plus `ad_set.both`. Do not add a separate validation +confirmation. + Prompt for fields to update (min 1). Same fields as create, all optional. Read the published ad set, then check `GET /drafts/ad_sets/{id}`. If it returns 404, create a draft with `POST /ad_sets/{id}/drafts`. If a draft already exists, disclose its pending state before combining changes. PATCH `/drafts/ad_sets/{id}`, resolve its draft `campaign_id`, fetch the draft campaign's current hierarchy version, and validate it. Keep the result staged. @@ -202,6 +216,12 @@ api GET "ad_accounts/{ad_account_id}/ads?limit=50&sort_direction=DESC" Format as table: ID | Name | Ad Set ID | Status | Delivery ### `ads create` +Collect the required fields and asset selections below first. Before the POST, read and +follow `$PLUGIN_ROOT/skills/api-reference/references/ad-product-validation.md`. Fetch +the live catalog, current parent ad set and campaign, and referenced assets, then +validate the final ad body against `ad.create` plus `ad.both`. Do not add a separate +validation confirmation. + Prompt for required fields: - **name** (2-200 chars) - **ad_set_id** (uuid — suggest listing ad sets first) @@ -240,6 +260,13 @@ api GET "ad_accounts/{ad_account_id}/ads/$AD_ID" ``` ### `ads update ` +Before the update, read and follow +`$PLUGIN_ROOT/skills/api-reference/references/ad-product-validation.md`. Fetch the live +catalog, current ad, parent ad set and campaign, and any referenced assets. Deep-merge +the proposed changes into the current ad and validate the effective entity against +`ad.update` (when present) plus `ad.both`. Do not add a separate validation +confirmation. + Read the published ad, then check `GET /drafts/ads/{id}`. If it returns 404, create a draft with `POST /ads/{id}/drafts`. If a draft already exists, disclose its pending state before combining changes. PATCH `/drafts/ads/{id}`, fetch its parent draft ad set to resolve the draft campaign, fetch the campaign's current hierarchy version, and validate it. Keep the result staged. Draft ad updates support `name`, `advertiser_name`, `tagline`, `assets`, `asset_format`, `call_to_action`, `third_party_tracking`, `placements`, `weight`, and `status`. Always preserve third-party tracking entries the user did not explicitly remove or replace, and set `measurement_event` explicitly on every entry. diff --git a/skills/api-reference/SKILL.md b/skills/api-reference/SKILL.md index fc1c44c..1a0ec87 100644 --- a/skills/api-reference/SKILL.md +++ b/skills/api-reference/SKILL.md @@ -172,6 +172,7 @@ For changes to published campaigns, ad sets, or ads, first check whether a same- - `GET /businesses/{id}` — Get business by ID - `GET /targets/artists` — Search artist targets - `GET /ad_categories` — List ad categories +- `GET /ad_product_catalog` — Get live product-specific validation rules for AUCTION, CONTENT, and FPMNG. Fetch once per create or update workflow and do not retain a timed cross-operation cache. Follow `references/ad-product-validation.md` before mutating campaigns, ad sets, or ads. - `POST /estimates/audience` — Estimate audience size for targeting parameters (recommended before creating ad sets to validate reach) - `POST /estimates/bid` — Get bid recommendations - `POST /ad_accounts/{id}/reserved_prices` — Get pricing for reserved ad products (fCPM) @@ -222,6 +223,7 @@ For detailed request/response schemas and field definitions, consult: - **`references/endpoints.md`** — Complete endpoint details with all parameters and response schemas - **`references/schemas.md`** — Request/response body schemas with field types, constraints, and required fields - **`references/enums.md`** — All enum values for status fields, asset formats, targeting options, report dimensions/metrics +- **`references/ad-product-validation.md`** — Mandatory catalog-validation procedure for campaign, ad set, and ad mutations ### Example Files diff --git a/skills/api-reference/references/ad-product-validation.md b/skills/api-reference/references/ad-product-validation.md new file mode 100644 index 0000000..27775b3 --- /dev/null +++ b/skills/api-reference/references/ad-product-validation.md @@ -0,0 +1,113 @@ +# Ad Product Catalog Validation + +Use this procedure before every campaign, ad set, or ad `POST` or `PATCH`, including +draft creation and edits, clone operations, bulk changes, and replacement ads. + +The hard invariant is: **never send a request with a known, unresolved ad product +catalog violation.** Printing a checklist is not validation and is not required. + +## 1. Fetch Rules Once Per Workflow + +Fetch the catalog at the start of the current create or update workflow: + +```bash +api GET "ad_product_catalog" +``` + +Reuse that response while assembling and executing the current workflow. Do not reuse +a response retained from an earlier operation or attempt to maintain a timed session +cache. If the catalog request fails, do not execute a mutation that depends on it. + +The catalog layers product-specific restrictions on top of the OpenAPI schema: + +- OpenAPI defines request shapes and field types. +- The catalog defines product-specific allowed values, required or forbidden fields, + constraints, restrictions, frequency caps, and cross-field rules. +- For creates, apply the matching entity's `create` and `both` sections. +- For updates, apply the matching entity's `update` and `both` sections. A missing + operation section means there are no additional rules in that section; `both` still + applies. + +When the catalog explicitly lists product-specific allowed values, use that live list +for product validation; some deprecated enums in the committed OpenAPI may lag the +catalog. OpenAPI still governs whether the field exists and what shape and type it has. +If the catalog requires a field or shape that OpenAPI cannot represent, do not invent a +payload or claim that validation passed. Stop before the mutation and explain the +conflict. + +## 2. Resolve the Ad Product + +- For campaign creation, use the request's `ad_product`. Treat an omitted value, + `UNSET`, or `UNKNOWN` as `AUCTION`. +- For a hierarchy created in the current workflow, carry that resolved product forward + to its ad sets and ads; do not refetch the campaign merely to rediscover it. +- For existing ad sets and ads, fetch the entity chain needed to reach the parent + campaign. Use `ad_product` when the campaign response provides it. +- When an existing campaign response omits `ad_product`, use `AUCTION` only when the + request context and entity data do not indicate a reserved `CONTENT` or `FPMNG` + campaign. If the user, source operation, pricing fields, or entity configuration + indicates a reserved product, obtain an authoritative product choice instead of + silently defaulting to `AUCTION`. +- Do not infer a reserved product solely from a campaign objective. + +For bulk operations, resolve each distinct campaign once and group entities by resolved +product. For clones, validate against the product that the new campaign request will +actually create, not merely the source campaign's product. + +## 3. Validate the Final Effective Entity + +Assemble all fields before validation. + +- **Create:** validate the final request body plus any parent or asset data referenced + by cross-entity rules. +- **Update:** fetch the current entity, deep-merge the proposed PATCH into it, and + validate the resulting effective entity. Do not validate only the changed fields. +- On update, apply rules conditioned on a value being new or changed only when the + PATCH actually changes that value. Do not reject a valid historical entity by + reapplying a creation-time future-date check to an unchanged start time. +- Fetch enough parent context to evaluate catalog rules. Ad validation can require the + parent ad set's format, platforms, and dates as well as the campaign objective or + delivery goal group. +- Fetch referenced assets when rules depend on asset type, status, duration, audio + tracks, or archive state. + +Do not manufacture a pass result for fields or conditions that the catalog does not +address. + +## 4. Handle Static and Runtime Rules Honestly + +Classify applicable rules while validating: + +1. **Static rules** can be evaluated from the final entity and fetched parent context. + Enforce these before the mutation. +2. **Resolvable runtime rules** require a read-only API check. Use the appropriate + endpoint when the inputs are available, for example audience or bid estimates, + reserved pricing, asset lookup, or reporting data needed for a budget decrease. +3. **Server-only rules** depend on state the public API does not expose, such as an + internal exemption or cooldown. Do not label these as passed. Apply every known + prerequisite, allow the mutation endpoint to perform the authoritative check, and + report any rejection normally. + +If a known value violates a rule, do not send it. If a runtime condition cannot be +proved locally, do not turn that uncertainty into a user confirmation gate or claim +that the condition passed. + +## 5. Minimize User Interruptions + +- If an assistant-inferred value or default violates the catalog, replace it with a + compliant value and disclose the adjustment in the existing plan or change summary. +- If a value explicitly chosen by the user violates the catalog, explain the exact + rule, recommend compliant alternatives, and ask one focused question. Revalidate the + revised value. +- Ask only when there is no safe compliant choice or the alternatives materially change + the user's intent. +- Do not print per-field pass checklists or add a separate confirmation for validation. + When the workflow already presents a plan or change summary, add one compact line such + as `Ad product validation: static AUCTION rules passed` and mention only material + adjustments or unresolved server-only checks. +- Validation never replaces an existing confirmation required by `auto_execute`, bulk + changes, or draft publishing. + +For draft hierarchies, perform this catalog preflight before creating or editing drafts, +then run the draft campaign `VALIDATE` action as the authoritative hierarchy check. +Publishing remains the only mandatory extra confirmation. diff --git a/skills/api-reference/references/endpoints.md b/skills/api-reference/references/endpoints.md index 3c84b4d..e96f226 100644 --- a/skills/api-reference/references/endpoints.md +++ b/skills/api-reference/references/endpoints.md @@ -692,6 +692,100 @@ Create a new ad account under a business. --- +## Ad Product Catalog + +### GET /ad_product_catalog +Returns the live validation rules for externally available ad products: AUCTION, +CONTENT, and FPMNG. These product-specific rules layer on top of the OpenAPI request +shape and field types. + +Fetch the catalog once for each create or update workflow and reuse it only during that +workflow. Do not maintain a timed session cache; the endpoint response is delivered +with `Cache-Control: no-cache, no-store, max-age=0, must-revalidate`. + +Resolve the product from the new campaign request or known hierarchy context. An +omitted, `UNSET`, or `UNKNOWN` campaign product maps to AUCTION. Campaign responses do +not consistently expose `ad_product`, so do not silently classify an existing hierarchy +as AUCTION when its configuration or request context indicates CONTENT or FPMNG. See +`ad-product-validation.md` for the complete resolution and interaction procedure. + +**Response:** 200 — Ad product catalog with validation rules per product type. + +**Response structure:** + +Each product has `campaign`, `ad_set`, and `ad` sections. Rules are separated by +operation: use `create` plus `both` for POST requests and `update` plus `both` for PATCH +requests. Some products omit an operation section when they have no additional rules +for it. + +The following is an abbreviated structural example. Always use values from the live +response rather than treating this sample as an exhaustive catalog. + +```json +{ + "description": "Ad product validation rules that layer on top of the OpenAPI spec...", + "ad_products": { + "AUCTION": { + "display_name": "...", + "description": "...", + "campaign": { + "create": { + "allowed_values": { "objective": [""] } + }, + "update": { + "allowed_values": { "status": ["ACTIVE", "PAUSED"] }, + "restrictions": ["field: cannot change after creation"] + }, + "both": { + "constraints": ["end_time must be within 365 days of start_time"], + "cross_field_rules": ["When X: Y"] + } + }, + "ad_set": { + "create": { + "allowed_values": { "...": "..." }, + "required_fields": ["..."], + "forbidden_fields": ["..."], + "cross_field_rules": ["..."] + }, + "update": { + "allowed_values": { "...": "..." }, + "restrictions": ["..."], + "cross_field_rules": ["..."] + }, + "both": { + "frequency_caps": { "max_impressions": { "DAY": 5, "WEEK": 35, "MONTH": 50 } }, + "constraints": ["..."], + "cross_field_rules": ["..."] + } + }, + "ad": { + "create": { + "required_fields": ["assets.asset_id: primary asset required; must not be archived"] + }, + "update": { "restrictions": ["Cannot edit an archived creative"] }, + "both": { + "constraints": ["Audio creative duration: max 31,000 ms"] + } + } + } + } +} +``` + +**How to apply rules:** +- For a **POST**: apply every rule category present in `create` and `both`. +- For a **PATCH**: fetch the current entity, deep-merge the patch, then apply every rule category present in `update` and `both` to the effective entity. +- Match the entity type to the correct key: `campaign`, `ad_set`, or `ad`. +- Treat `required_fields`, `forbidden_fields`, `constraints`, `restrictions`, and + `cross_field_rules` as semantic rules, not merely field-name arrays. +- Retrieve parent entities, assets, estimates, prices, or reporting state when a rule + depends on them. +- Do not report runtime or server-only conditions as passed unless the required state + was actually checked. + +--- + ## Estimates **Important:** These are top-level endpoints — they are NOT nested under `/ad_accounts/{ad_account_id}/`. The `ad_account_id` is passed in the request body instead. diff --git a/skills/build-campaign/SKILL.md b/skills/build-campaign/SKILL.md index 41d5600..f15d954 100644 --- a/skills/build-campaign/SKILL.md +++ b/skills/build-campaign/SKILL.md @@ -47,6 +47,7 @@ use the defaults noted below. If a required field cannot be inferred, ask the us |-------|----------|---------| | 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` @@ -94,9 +95,16 @@ Valid objectives: `REACH`, `CLICKS`, `VIDEO_VIEWS`, `CONVERSIONS`, `LEAD_GEN`, ` | 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 +`$PLUGIN_ROOT/skills/api-reference/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 API calls, present the full parsed plan as a visual tree: +Before making any mutating API calls, present the full parsed plan as a visual tree: ``` Campaign: "My Campaign" (objective: REACH) @@ -171,12 +179,27 @@ Present audio/video assets and image assets separately in tables, and ask the us - **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":"..."}' @@ -186,8 +209,6 @@ Extract the campaign `id` from the response. ### 4b. Create Ad Sets (using campaign_id from 4a) -For each ad set: - ```bash api POST "ad_accounts/{ad_account_id}/ad_sets" \ '{ @@ -215,8 +236,6 @@ Extract each ad set `id` for use in ad creation. ### 4c. Create Ads (using ad_set_id from 4b) -For each ad: - ```bash api POST "ad_accounts/{ad_account_id}/ads" \ '{ diff --git a/skills/bulk/SKILL.md b/skills/bulk/SKILL.md index 8978b10..25a738a 100644 --- a/skills/bulk/SKILL.md +++ b/skills/bulk/SKILL.md @@ -63,9 +63,24 @@ Ask the user to select entities. Support these selection formats: - All: `all` - Mixed: `1-3, 5` -### 3. Confirm changes +### 3. Validate against ad product rules + +Before showing the final change summary, read and follow +`$PLUGIN_ROOT/skills/api-reference/references/ad-product-validation.md`. Fetch the live +catalog once for the batch, fetch each current entity, resolve each distinct parent +campaign once, and validate the final effective entities. Group validation by campaign +and product instead of repeating catalog or campaign requests for every entity. + +For budget decreases, retrieve any available spend and pacing state needed to apply the +catalog's delivery floor. Treat cooldowns or account exemptions that the public API +does not expose as server-only checks; do not claim they passed and do not add a user +confirmation for them. + +### 4. Confirm changes Show a summary of what will change. For budget operations, show before/after values. For status changes, show entity names and the target state. +Include one compact catalog-validation status in this existing summary. Do not print +per-field checklists or add a separate confirmation. ### 4. Stage changes @@ -73,7 +88,7 @@ Group selected entities by parent campaign. For each target, check for an existi After staging all selected changes, fetch each affected draft campaign's current `draft_hierarchy_version` and validate once per campaign. Continue on per-entity staging failures, but do not validate a campaign until all successful edits for that campaign are staged. -### 5. Show results +### 6. Show results Display a final summary table: @@ -318,6 +333,9 @@ Ask the user to select which ads to update and which new asset to use. The new a api GET "ad_accounts/{ad_account_id}/ads/$AD_ID" ``` +The batch validation step must include the current ad, its parent ad set and campaign, +the replacement asset, and the final replacement-ad body. + **Check for or create the draft:** ```bash diff --git a/skills/campaign-strategy/SKILL.md b/skills/campaign-strategy/SKILL.md index 12550a8..185f2ef 100644 --- a/skills/campaign-strategy/SKILL.md +++ b/skills/campaign-strategy/SKILL.md @@ -5,7 +5,7 @@ description: Generate Spotify Ads campaign strategy from a landing page, product # Spotify Ads API - Campaign Strategy -Plan campaign structure and targeting before creating entities. This skill researches the offer, checks current Spotify Advertising guidance, validates targetability through the Ads API, and returns an API-ready plan. Do not create campaigns, ad sets, ads, assets, or audiences unless the user explicitly asks to execute after reviewing the plan. +Plan campaign structure and targeting before creating entities. This skill researches the offer, checks current Spotify Advertising guidance, validates targetability through the Ads API, and returns an API-ready plan when credentials permit live validation. Otherwise, it clearly marks validation as deferred. Do not create campaigns, ad sets, ads, assets, or audiences unless the user explicitly asks to execute after reviewing the plan. For detailed planning heuristics, read `references/planning-framework.md`. @@ -39,6 +39,8 @@ If budget, dates, or market are missing, make a conservative recommendation and - Choose CTA and landing URL based on the page and asset. Use `LEARN_MORE` when conversion intent is informational or regulated. 4. Validate API targetability. + - Read and follow `skills/api-reference/references/ad-product-validation.md` from the plugin root. When credentials are available, fetch `GET /ad_product_catalog` once for this strategy workflow and use it to validate the final plan. Do not print a per-field checklist. + - When credentials are unavailable, still provide the strategy, label it **catalog validation deferred**, and do not call it API-validated or ready to execute. The execution skill must fetch the live catalog before any mutation. - Fetch valid ad categories from `GET /ad_categories`; use the closest exact category code. - Look up every requested geo with `GET /targets/geos?country_code=&q=&limit=20`; never fall back to country-only without saying so. - Use only targeting dimensions available in the Ads API. If recommending interests, genres, artists, playlists, or languages, validate them with the matching target endpoint before presenting IDs. **Only `/targets/geos` accepts `limit`/`offset` parameters.** All other target endpoints (`/targets/genres`, `/targets/interests`, `/targets/artists`, `/targets/playlists`, `/targets/languages`) accept only `q` and/or `ids` — passing `limit` will cause a 400 error. @@ -64,7 +66,7 @@ Return a compact strategy package: - **Recommended structure:** campaign objective, ad sets, budget split, asset format, placements, frequency cap, pacing, bid strategy, CTA, and ad rotation. - **Validated targeting:** category code, geo IDs, and any other target IDs. Mark unvalidated ideas clearly. - **Forecasts:** audience estimate, likely-to-deliver flag, reach/impression/CPM ranges, and bid estimate when available. -- **API-ready plan:** campaign tree plus JSON skeletons for campaign and ad sets. +- **API-ready plan:** campaign tree plus JSON skeletons for campaign and ad sets when live catalog validation completed; otherwise label this section as a proposed plan with validation deferred. - **Next step:** what to confirm before handing off to `/spotify-ads-api:drafts build` (preferred) or `/spotify-ads-api:build-campaign`. Recommend the draft flow so the user can review and validate the full hierarchy before going live. ## Guardrails diff --git a/skills/campaigns/SKILL.md b/skills/campaigns/SKILL.md index 4c50998..7f0ac78 100644 --- a/skills/campaigns/SKILL.md +++ b/skills/campaigns/SKILL.md @@ -38,6 +38,12 @@ Prompt the user for a name and campaign goal. Map the goal to `delivery_goal_gro - **name** (string, 2-200 chars) - **delivery_goal_group** (`AWARENESS`, `WEBSITE_TRAFFIC`, `APP_PROMOTION`, `ENGAGEMENT_ON_SPOTIFY`, or `LEAD_GEN`) +Then read and follow +`$PLUGIN_ROOT/skills/api-reference/references/ad-product-validation.md`. Fetch the live +catalog once for this operation. Validate the final campaign body against `campaign.create` +plus `campaign.both`. An omitted, `UNSET`, or `UNKNOWN` `ad_product` resolves to +`AUCTION`. Do not add a separate validation confirmation. + ```bash api POST "ad_accounts/{ad_account_id}/drafts/campaigns" \ '{"name":"...","delivery_goal_group":"..."}' @@ -59,6 +65,12 @@ Prompt the user for fields to update (at least 1 required): - **name** (string, optional) - **status** (ACTIVE, PAUSED, ARCHIVED, optional) +Before the PATCH, read and follow +`$PLUGIN_ROOT/skills/api-reference/references/ad-product-validation.md`. Fetch the live +catalog and current campaign once for this operation, deep-merge the proposed changes, +and validate the effective campaign against `campaign.update` plus `campaign.both`. +This applies to status-only updates too. Do not add a separate validation confirmation. + ```bash api GET "ad_accounts/{ad_account_id}/campaigns/$CAMPAIGN_ID" api GET "ad_accounts/{ad_account_id}/drafts/campaigns/$CAMPAIGN_ID" diff --git a/skills/clone/SKILL.md b/skills/clone/SKILL.md index 13a095b..2e4339f 100644 --- a/skills/clone/SKILL.md +++ b/skills/clone/SKILL.md @@ -128,6 +128,21 @@ api POST "estimates/audience" \ If the API returns a min-audience-threshold error, pause before creating that ad set and suggest broader targeting or a lower-threshold format. +### Step 4.5: Validate the Clone Against Ad Product Rules + +Read and follow +`$PLUGIN_ROOT/skills/api-reference/references/ad-product-validation.md`. Fetch the live +catalog once for the clone workflow and validate the complete destination hierarchy, +including user modifications, runtime estimates, and referenced assets. + +Resolve the product from the **new campaign request**. Preserve a known source +`CONTENT` or `FPMNG` product only when the destination request explicitly includes that +product; omit `ad_product` for the default `AUCTION` flow. Do not validate against the +source product while creating a different destination product. + +Apply assistant-inferred compliant adjustments before showing the clone plan. Surface +only incompatible explicit choices; do not print a per-field checklist. + ### Step 5: Present Clone Plan Show the full plan with changes highlighted: @@ -153,6 +168,9 @@ Create entities in dependency order, passing IDs forward. #### 6a. Create campaign +For a destination CONTENT or FPMNG campaign, include that explicit `ad_product` in the +request. For the default AUCTION flow, omit `ad_product`. + ```bash api POST "ad_accounts/{ad_account_id}/drafts/campaigns" \ '{"name":"Summer Promo (Copy)","delivery_goal_group":"AWARENESS"}' @@ -164,7 +182,7 @@ Extract the new campaign `id` from the response. If this fails, stop — no depe #### 6b. Create ad sets (using new campaign_id) -For each source ad set (excluding any the user filtered out): +For each source ad set, excluding any the user filtered out: ```bash api POST "ad_accounts/{ad_account_id}/drafts/ad_sets" \ @@ -190,7 +208,7 @@ If an ad set creation fails, log the error and skip its ads. Continue with remai #### 6c. Create ads (using new ad_set_ids) -For each source ad (excluding ARCHIVED/REJECTED), mapped to the correct new ad set: +For each source ad, excluding ARCHIVED/REJECTED, mapped to the correct new ad set: ```bash api POST "ad_accounts/{ad_account_id}/drafts/ads" \ @@ -269,7 +287,11 @@ Same modification options as campaign clone (name, dates, budget, targeting) but ### Step 4: Validate and Present Plan -Same validation as campaign clone (dates, assets, budget type). +Apply the same date, asset, budget, and audience checks as the campaign clone. Read and +follow `$PLUGIN_ROOT/skills/api-reference/references/ad-product-validation.md`, fetch +the catalog once for this clone workflow, and resolve the **target campaign's** product. +Validate the final new ad set and ads against that destination product before presenting +the existing confirmation. Do not print a per-field checklist or add another gate. ### Step 5: Execute diff --git a/skills/drafts/SKILL.md b/skills/drafts/SKILL.md index 4a19f6d..1e5233b 100644 --- a/skills/drafts/SKILL.md +++ b/skills/drafts/SKILL.md @@ -103,6 +103,14 @@ Given a plain-text campaign description, create the full draft hierarchy: draft 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 +`$PLUGIN_ROOT/skills/api-reference/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**: @@ -125,6 +133,17 @@ Fetch available assets from the account and present them for selection, just lik 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:** @@ -299,6 +318,14 @@ Display all fields in a readable format. Note that `draft_hierarchy_version` is 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 +`$PLUGIN_ROOT/skills/api-reference/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" \ diff --git a/tests/README.md b/tests/README.md index 76330e1..40cdd35 100644 --- a/tests/README.md +++ b/tests/README.md @@ -35,6 +35,8 @@ Run scenarios 22-32 to exercise campaign strategy, monitoring, export, bulk oper Run scenarios 33-34 to verify that implicit tracking edits use drafts and that an explicitly requested direct write handles permission denial without overstating the credentials' restrictions. Scenario 33 requires existing published ads with tracking entries; Scenario 34 may use a mocked 403 response. +Run scenario 35 to verify ad product catalog validation behavior across creates, updates, and draft workflows. + ## Validation checklist For every scenario, verify: @@ -49,6 +51,7 @@ For every scenario, verify: - [ ] Destructive or externally consequential actions receive explicit confirmation at the required boundary. - [ ] POST and PATCH requests are not automatically retried after ambiguous failures. - [ ] Results are summarized clearly, including partial failures in batch workflows. +- [ ] Mutating campaign/ad set/ad workflows fetch the live ad product catalog once per workflow, block known violations, and do not add per-field success checklists or redundant confirmations. ## Test fixtures diff --git a/tests/test-scenarios.md b/tests/test-scenarios.md index f56073d..e9ad21c 100644 --- a/tests/test-scenarios.md +++ b/tests/test-scenarios.md @@ -1,6 +1,6 @@ # Test Scenarios -34 structured test scenarios for validating the Spotify Ads API plugin. Each scenario covers specific API quirks and plugin behaviors. For a concise prompt-per-capability view, see [`prompt-catalog.md`](prompt-catalog.md). +35 structured test scenarios for validating the Spotify Ads API plugin. Each scenario covers specific API quirks and plugin behaviors. For a concise prompt-per-capability view, see [`prompt-catalog.md`](prompt-catalog.md). **Important:** All entity names (campaigns, ad sets, ads) must be prefixed with `[Test reject]` so they are automatically rejected by ad review and never serve live impressions. @@ -804,28 +804,31 @@ curl -s -w "\nHTTP_STATUS:%{http_code}" -X POST -H "Authorization: Bearer ` (not creating a new draft ad) - Re-validation uses fresh `draft_hierarchy_version` from the draft campaign (not the version from before the edit; `draft_hierarchy_version` is `null` on ad drafts) -- Full cycle: create → validate (fail @ 400) → edit → validate (pass @ 200) → offer publish +- Full cycle: load existing draft → validate (fail @ 400) → edit → validate (pass @ 200) → offer publish --- @@ -1131,3 +1134,60 @@ The create-from-published POST is omitted when the initial draft GET succeeds. - No inference about specific organizational roles, user types, tools, or permission systems. - No recommendation to use a proprietary UI or ask a specially privileged user. - Draft staging is presented as the compatible alternative in generic, public-facing language. + +--- + +## Scenario 35: Ad Product Catalog Validation Behavior + +**Prompts:** Run these as separate subcases: + +1. Create a valid AUCTION campaign and ad set using inferred defaults. +2. Request `MOBILE` as the platform for an ad set. +3. Update one field on an existing ad set while leaving related fields unchanged. +4. Decrease a started lifetime ad set's budget. +5. Build a draft hierarchy and publish it. + +**Quirks tested:** Live catalog freshness, product resolution, static versus runtime +rules, effective PATCH validation, minimal user interruption, and draft validation. + +**Expected behavior:** + +1. Each independent mutation workflow calls the catalog once: + ```bash + api GET "ad_product_catalog" + ``` + It may reuse that response within the same multi-entity workflow, but does not reuse + a response from an earlier prompt or maintain a 15-minute cache. +2. An omitted, `UNSET`, or `UNKNOWN` campaign product resolves to AUCTION. A known + CONTENT or FPMNG destination is validated against that product instead. +3. For the inferred valid plan, validation adds no separate confirmation and no + per-field pass checklist. The existing plan or change summary may contain one compact + line such as `Ad product validation: static AUCTION rules passed`. +4. `MOBILE` is never sent. If it was assistant-inferred, replace it with IOS and ANDROID + and disclose the adjustment in the existing summary. If the user explicitly insists + on the literal API value `MOBILE`, explain the catalog rule and ask one focused + question offering IOS, ANDROID, or both. +5. For PATCH, fetch the current entity and required parents, deep-merge the proposed + changes, and validate the effective result. Do not validate only the outgoing patch + fields. +6. For the lifetime budget decrease, retrieve available spend/pacing state before + applying the catalog floor. Do not claim that an unobservable cooldown or account + exemption passed; leave the authoritative check to the mutation endpoint. +7. Draft creation still runs the draft campaign `VALIDATE` action after catalog + preflight. Catalog validation does not add another publish gate; explicit publish + confirmation remains required. +8. If the catalog GET fails, no dependent campaign/ad set/ad mutation is executed. A + strategy-only request may continue but must be labeled `catalog validation deferred` + rather than API-ready. + +**Success criteria:** + +- Exactly one catalog GET per independent workflow, not one per entity and not a timed + cross-workflow cache +- No mutation containing a known catalog violation +- No fabricated success for runtime or server-only conditions +- No `✅/❌` field dump and no validation-only confirmation +- Explicit incompatible user choices cause at most one focused remediation question +- PATCH validation uses current entity + patch + required parent context +- Clone validation follows the destination campaign product +- Draft hierarchy validation and explicit publish confirmation remain intact