Role
You help the media buyer lead batch-create creative sets in AppLovin. All API encapsulation and credential reuse are handled by the scripts/creative_tuner.py script in the same directory (ct.set_account() / ct.APL_MGMT_KEY / ct.APL_ACCOUNT_ID / ct.APL_MGMT_URL). All tool scripts are located in scripts/ and are named _cs_*.py.
Installation / Configuration (Read First)
- Place the entire
apl-creative-set/directory into~/.claude/skills/(or%USERPROFILE%\.claude\skills\on Windows). - In
scripts/, runcp config.example.json config.json, then fill in your own AppLovin credentials:accounts.<accountID>.report_key= AppLovin Report Key;.mgmt_key= Management API Key.account_aliasesallows you to give accounts nicknames (e.g.,"myapp": "your-account-id"), so scripts can usect.set_account("myapp").- The
feishusection is optional and only used by the tuning daily report increative_tuner.py; you don't need it just to create creative sets—leave it empty.
config.jsoncontains real secrets, never commit it to git / share it (add it to.gitignore).- In the scripts, anywhere you see
ct.set_account("YOUR_ACCOUNT")or uppercase constants likeCAMP=""/PA_IDS/VIDS/PREFIX/PP/SUFFIXat the top are TODO placeholders; modify them according to your current goal.
This skill is driven by Claude reading SKILL.md: you tell Claude your goal (campaign, videos to build, naming, CPP), and Claude modifies the constants at the top of the scripts and runs them following the flow below.
Hard Rules
- ★ Before each session, must confirm these three items with the user, item by item (iOS/Android differ, never reuse previous answers):
① Set naming rule; ② Whether to add CPP suffix; ③ What value CPP
product_pageshould take. Even if it's the same product line as last time, ask again; do not assume defaults. - Credential Security: Never output/print/echo any key/secret/token/open_id from
config.json. Scripts must never print credentials. - Default PAUSED: All newly created sets must have
status:"PAUSED". Only enable them after user confirms everything is correct (user says "enable" → then LIVE). - Only the requested asset scope: Users often limit to "only certain prefixes" or "only recently uploaded" — strictly follow the filter; don't create extra.
- Re-read to verify every step: After create/mount/CPP/enable, must re-read via
creative_set/listto confirm, not rely on response codes. - Numbers should state source, precise to 2 decimal places. If unsure, ask first. Before write operations, confirm then execute.
★ API Key Mechanics (Pitfalls encountered, must follow)
- Always attach campaigns via add-to-campaigns; do not rely on create to attach them reliably.
The
POST /creative_set/createbody can use either singularcampaign_id:"<internal_id>"or pluralcampaign_ids:["<internal_id>"], but neither guarantees attachment (the plural is often silently ignored). So the fixed flow is: create → immediatelyadd-to-campaignsto backfill → re-readcampaign_idsto verify. create body (production version):{"account_id":AID,"type":"APP","name":..,"status":"PAUSED","campaign_ids":["<internal_id>"],"product_page":"","languages":[],"countries":[],"assets":[{"id":".."},..]}→ returns{"id":..,"version":"V2"} - Backfill campaign:
POST /creative_set/add-to-campaigns, uses integer array:{"campaign_ids":[<internal_id>],"creative_set_ids":[..]}(max 20 campaigns / 50 sets per batch). Most reliable. Success = target campaign appears in the re-readcampaign_ids. - Modify set (rename/CPP/toggle/assets add/remove):
POST /creative_set/update{"id":..,"type":"APP","status":"LIVE|PAUSED","name":..,"product_page":..,"assets":[..]}- update generates a new version. Passing
assetsis a full replacement (not append): to remove, don't include that id; to add, include the id in the full list; any omitted id will be deleted (ensure you keep the video + playables you want to retain). - Fields not passed (e.g., name/product_page/campaign association) are observed to be preserved in practice; to be safe, when toggling or changing assets, include name + product_page + languages + countries to avoid loss.
- Passing
campaign_idsin update does not work (returns 200 but doesn't attach) — use only add-to-campaigns.
- update generates a new version. Passing
- Custom Product Page: Field
product_page.- Use default store page (no CPP) =
product_page:""(empty string, no suffix in name). - When setting CPP, fill the corresponding product_page value (string slug, not UUID). There is no listing endpoint; to confirm the value for a given name, scan all sets in the account and correlate
product_pagevs name suffix (see_cpp_map.py).
- Use default store page (no CPP) =
- Asset type field differs between two endpoints (easy to trip):
- In
asset/list(media library) the field isasset_type; - In
creative_set'sassets[]elements the field istype. - Values: Video =
VID_LONG_P/VID_SHORT_P(or starts with"VID"); Playable =HOSTED_HTML.
- In
- ★ Max assets per creative set = 11: Once assets reach 11, any update to add more is silently rejected (returns 200 but doesn't add; you can only detect it by re-reading). Before adding a playable, check
len(assets); for a full set of 11, you must first delete one to make room (replace), otherwise the addition fails. creative_set/listandasset/listpagination size ≤100/200, and some pages return bare list (not{results:[]}); parsing must useisinstanceto be compatible.asset/listpage limit = 100: for pagination, break whenlen(res)<100(using 200 would stop at first page, returning only the first 100 media).
Before Starting, Ask (unless user has already provided)
- Target campaign (name/ID) → locate across configured accounts, get internal id.
- Asset scope: Which videos? Prefix filter (e.g., a product line prefix), upload time boundary (upload_time ≥ date), status (usually ACTIVE and not already used by another set).
- Fixed playables: Which HTML playables to attach (user will give exact filenames).
- Naming rule: Default:
EN_videotype(AI/3D/JJ…takenfromassetname_slot)_videoname_topplayablegroupPA; if multiple videos from a series go into one set →EN_type_basenamegroup_topplayablegroupPA(basename = remove trailing index). - Series detection: Same concept + trailing number (iteration 1/4) → group together. If ambiguous, ask first (e.g., "basic/advanced" — should they be split?).
- CPP: Whether to set product_page, what value, and whether to add a suffix to the name.
Standard Flow (Script Pipeline, all in scripts/)
_apl_active_assets.py→ Pull ACTIVE assets, deduplicate →_apl_active_assets.json._cs_plan_scan.py→ Find fixed playable ids + candidate videos (prefix + is_video + not already used by existing sets + time filter) →_cs_scan.json. First modifyPREFIX/CAMP/targetsat the top._cs_group.py→ Group candidates by series/single asset, generate names →_cs_build_plan.json. Can use_cs_plan_show.pyto print for user confirmation._cs_build_all.py(Idempotent: check by name, skip duplicates) → Create each (PAUSED, with campaign_id) + backfill add-to-campaigns + re-read verify →_cs_create_result.json.- (Optional)
_cpp_apply.py all→ Setproduct_page+ add name suffix (first modifyPP/SUFFIXat top). - After user confirms,
_cs_enable.py→ status→LIVE (include name+product_page to avoid loss) + re-read. - Spot check:
_cs_spotcheck.py <id..>to inspect asset composition (video count + playable count).
Lightweight Path: Create Sets for a Few Specified Videos Directly (known video names, skip full scan)
Use when user just uploaded a few videos and wants sets built from them.
_cs_find_new.py <keyword..>→ Search media library by name for videos (full name+id+status+upload time; read-only). Verify it's the right one and status is ACTIVE.- Modify
_cs_en_new_create.pytop:VIDS=[(core_name,video_id),..]/PA_IDS(fixed playables) /CAMP; first confirm naming+CPP per hard rule 0.python _cs_en_new_create.py(dry-run: check duplicates + list plan) →go(create PAUSED + default store page""+ no language settings (follows campaign) + add-to-campaigns + re-read verify: campaign attachment/assets count/pp/status). - User says enable →
_cs_en_new_enable.py(reads_cs_en_new_ids.json, explicitly includes name/product_page/languages/countries to prevent loss, status→LIVE + re-read).
Add / Remove / Replace Playables in a Set
Batch add/remove a specific playable by set name keyword (via update with full asset replacement, preserve name/product_page/languages/countries/status, re-read after execution):
- Add:
_cs_add_pa.py <playable_id> <set_name_keyword> [enonly](dry-run) → thengo.enonly= only apply to sets starting withEN_(excludes localization-targeted sets). Idempotent (skips if already present); sets with 11 assets are warned and skipped automatically (you must first delete one to make room). - Remove:
_cs_rm_pa.py <playable_id>(dry-run) →go. Safety guard: after removal, the set must still have ≥1 video and ≥1 playable; otherwise it's flagged red and skipped. - Replace = swap old id for new id in the full asset list, then update.
- To find playable id, use
_cs_find_new.py <keyword>to search media library by name (HOSTED_HTML).
Delivery Script
Report: How many sets created (series/singles), how many videos covered, each set = corresponding video + N playables, all PAUSED + campaign-attached + CPP, plus a "set name → video" mapping; flag any assets that are rejected/not uploaded (can use full-status scan like _b8_leftover.py to classify REJECTED/not uploaded). Wait for user to say "enable" before running step 6.