skills/creative-fatigue-rotation-planner/SKILL.md
Read-only TikTok creative fatigue detector + rotation plan — compares each running ad's recent click-rate (and, for conversion ads, conversion-rate) against its own 30-day baseline to catch decaying creatives, then returns a ranked Retire / Scale / Watch plan with the size of the drop, the spend at risk, and an optional shortlist of fresh library assets to swap in. Triggers: creative fatigue, are my ads fatiguing, which creatives to refresh, ad fatigue, creatives getting tired, CTR dropping, rotate my creatives, which ads to retire, refresh ads, declining ad performance, creative rotation plan, spend at risk on old creatives. ❌ Do NOT use this skill for: actually swapping, uploading, pausing, or appealing a creative (use manage-creative); a full multi-dimensional performance report (use get-performance-report); whole-campaign delivery diagnosis like rejections / budget / bids (use diagnose-campaign-health); cross-campaign budget reallocation (use optimize-budget). This skill never writes — it plans and routes.
npx skillsauth add chatandbuild/skills-repo creative-fatigue-rotation-plannerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
A read-only skill. It reads ad-level performance over two time windows, measures how far each
creative has decayed from its own baseline, and prints a ranked rotation plan. It never swaps,
uploads, pauses, or appeals anything — the actual rotation is handed to manage-creative.
Tool chain (verified against the live MCP):
Stage 0: resolve advertiser (paste advertiser_id/bc_id, or auth_advertiser_get / bc_get) → advertiser_info_get
Stage 1: confirm windows (recent 3d vs baseline 30d) + ask if a library scan is wanted
Stage 2 (read-only):
① report_integrated_get (ad level, recent 3 days: ctr / conversion_rate / spend / impressions) → sync
② report_task_create → report_task_check → report_task_download (ad level, last 30 days) → async
③ smart_plus_ad_get / ad_get → ad ↔ creative names
④ (optional) file_video_ad_search / file_image_ad_search → fresh candidates
Stage 3: compute decay per creative → classify Retire / Scale / Watch → spend at risk
Stage 4: render the rotation plan (+ optional swap-in shortlist) → route the swap out
This skill must NEVER call any write tool — no ad_update, smart_plus_ad_update,
*_status_update, *_material_status_update, file_*_upload, adgroup_appeal, *_create
(except report_task_create, a read-only report task), or *_delete. Only the read tools in the
dispatcher table below plus tool_execute / tool_get. If asked to plan AND swap, produce the
plan, then refuse the swap and route to manage-creative. A silent write = catastrophic failure.
| Out-of-scope intent | Route to |
|---|---|
| Actually swap / upload / pause / appeal a creative | manage-creative |
| A full multi-dimensional performance report | get-performance-report |
| Whole-campaign delivery diagnosis (rejected / budget / bid / no-impressions) | diagnose-campaign-health |
| Move budget across campaigns and execute it | optimize-budget |
It sounds like what you really want is [to execute a swap / a full report / a delivery diagnosis] — there's a dedicated skill for that. Want me to switch to
<skill>?
Runs through the TikTok Ads MCP. layer (tt-ads-mcp-layer: ~40 tools + tool_execute/tool_get
dispatcher) or flat (tt-ads-mcp-flat: all ~400 callable directly). Mapping below names each
real tool; under flat, call it directly.
| Mechanism | When | Call shape |
|---|---|---|
| L0 direct tool | on the layer L0 allowlist (or flat) | call by name directly |
| tool_execute dispatch | other grouped tools on layer | tool_execute(tool_name="<tool>", params={...}); unsure → tool_get(["<tool>"]) first |
Complete tool mapping (single source of truth). All READ-ONLY:
| Purpose | Real tool | Mechanism (layer) |
|---|---|---|
| Stage 0 authorized ad accounts | auth_advertiser_get | L0 direct |
| Stage 0 Business Centers | bc_get | tool_execute |
| Stage 0 advertiser details (currency/timezone) | advertiser_info_get | L0 direct |
| Stage 2 ① recent ad report (sync) | report_integrated_get | L0 direct |
| Stage 2 ② baseline ad report (async) | report_task_create / report_task_check / report_task_download | tool_execute |
| Stage 2 ③ Smart+ ad list | smart_plus_ad_get | L0 direct |
| Stage 2 ③ legacy ad list | ad_get | tool_execute |
| Stage 2 ④ library videos (optional) | file_video_ad_search | tool_execute |
| Stage 2 ④ library images (optional) | file_image_ad_search | tool_execute |
| (optional, allowlist-only) dedicated fatigue signal | creative_fatigue_get | tool_execute |
Metric values come back as strings — parse to numbers. The conversion metric is
conversion(singular); conversion rate isconversion_rate. There is a dedicatedcreative_fatigue_gettool, but it is allowlist-only — if it errors (not allowlisted), fall back to the report-metric method below, which is the primary path. The optional library scan (④) is off the critical path: if it errors or is empty, say "No unused library assets found" and continue — never block. ⚠️ Paywalled (n/a) metrics → shown/a, never guess.
| Code | Trigger | What this skill does |
|---|---|---|
| E101_NO_ACCOUNT | Can't list or resolve any advertiser and no id given | Ask the user to paste an advertiser_id/bc_id; note auth_advertiser_get/bc_get were empty |
| E102_NO_PERMISSION | API returns 40001 No permission to operate advertiser | Tell the user this token isn't authorized on that advertiser; ask them to authorize it or give another id |
| E103_NO_ADS | No ad clears the data floor | Honestly report "not enough creative data to assess fatigue yet," stop — don't fabricate |
| E104_ASYNC_FAILED | The baseline async report came back FAILED | Surface the raw error, suggest narrowing the range, ask to retry |
| E105_API_4XX | 4xx (e.g. 40002 invalid field) | Surface raw code + message; fix the param or skip that slice |
| E106_API_5XX | 5xx | Surface the error, ask whether to retry (max 1) |
Honest layering:
E101 / E103are skill states;E102 / E104 / E105 / E106wrap real platform errors (40001, FAILED,40002, 5xx) and MUST surface the rawcode+message.
Fast path (preferred): if the user gives an advertiser_id (or bc_id), use it directly — some
tokens can't enumerate accounts:
Paste the advertiser ID whose creatives I should check (a long numeric string).
Else try to list: auth_advertiser_get; if empty, bc_get; if both empty and no id →
E101_NO_ACCOUNT, ask for an id. Confirm: advertiser_info_get([advertiser_id]) →
keep currency, timezone; 40001 → E102_NO_PERMISSION.
Creative fatigue settings:
Recent window: last 3 days (default)
Baseline window: the 30 days BEFORE the recent window (default; NON-overlapping)
Library scan: suggest fresh videos/images to swap in? (yes / no, default yes)
Default: 3-day recent vs prior-30-day baseline, library scan on. Tell me if you want to change it.
Store recent_days (3), baseline_days (30), library_scan. Date sanity: baseline end_date
≤ today; reject future dates before any call.
Checking your creatives for fatigue, this usually takes 20–40 seconds...
① Recent report (sync) — report_integrated_get (L0): advertiser_id, report_type = BASIC,
data_level = AUCTION_AD, dimensions = ["ad_id"],
metrics = ["spend","impressions","clicks","ctr","conversion","conversion_rate","cpm","video_play_actions","ad_name","campaign_name"],
recent date range. Parse string metrics to numbers. cpm + video_play_actions are required — many
accounts are video-views/reach ads whose only valid engagement metric is cost-per-view or CPM (verified
live: most ads on 7415418166391832593, 7595534274158903297, 7631551715865657362 are video-views).
② Baseline report (async) — ad level over the prior non-overlapping baseline window (default
the 30 days ending the day before the recent window), same metrics as ① → report_task_create → poll
report_task_check → on success report_task_download. On FAILED → E104.
③ Ad ↔ creative names — smart_plus_ad_get (L0) and/or ad_get (tool_execute) to map ad_id
→ ad/creative name + material id.
④ (optional) Library scan — only if library_scan = yes: file_video_ad_search /
file_image_ad_search. If it errors or is empty → "No unused library assets found," continue.
If no ad clears the data floor (Stage 3) → E103_NO_ADS, stop honestly.
📎 Exact thresholds, formulas, exclusions, and the creative-churn fallback live in fatigue-criteria.md. Summary contract below.
0. Data floor + engagement metric by objective: an ad needs volume to judge (Signal A:
baseline impressions ≥ 1000; Signal B: recent impressions ≥ 1000). Below both → "thin data," skipped.
Pick each ad's engagement metric by objective: conversion ad (conversion objective or records
conversions) → CVR / CPA; TRAFFIC / BRAND_CONSIDERATION / ENGAGEMENT → CTR; VIDEO_VIEWS →
cost-per-view (spend / video_play_actions) or view-rate; REACH → CPM. Never judge a
VIDEO_VIEWS / REACH ad on CTR (they have ~0 clicks by design — verified live: NUXE / vivo / CP video ads),
and never a CVR an awareness ad lacks. One advertiser often mixes objectives (traffic + video-views +
live-conversion ads together — verified live: 7119328366494220289, 7591834723639656465), so pick the
metric per ad, not per account — but anchor on the campaign (group by campaign_name, already in
the pull): all ads in one campaign share an objective. If you didn't fetch the campaign objective, infer it
from the metric signature: conversion > 0 → conversion ad (CVR/CPA); video_play_actions ≫ clicks and
ctr ≈ 0 → video-views/reach (cost-per-view / CPM, never CTR); video_play_actions = 0 with real clicks →
click objective (CTR / CPC). ⚠️ A near-zero-CTR ad inside a click/consideration campaign (where other
ads earn real CTR) is a bottom-tier-CTR Retire candidate, NOT a "cheap view" — only treat an ad as
video-views/reach when its whole campaign optimizes for views/reach. Verified live: Keyshu consideration
campaign spans CTR 0.05%→4.10% across same-objective ads (the 0.05% ad is a Retire, not a cheap view).
1. Signal A — time-decay (preferred, precise): for an ad present in BOTH windows —
engagement-metric fatigue = the ad's engagement metric (step 0) worsened ≥ 40% vs its baseline
(CTR or view-rate ↓ ≥ 40%, or cost-per-view / CPM ↑ ≥ 40%); CVR fatigue (conversion ads) =
recent_cvr < baseline_cvr × 0.7.
2. Signal B — vs current peers (always available; handles creative churn): many advertisers rotate
creatives fast (e.g. flash-sale bursts), so few ads persist across windows and Signal A finds little.
For each CURRENT ad (recent impressions ≥ 1000), rank it against the account's current
same-objective ads on its engagement metric (CTR / view-rate / CPM per step 0) plus recent_cvr /
recent CPA for conversion ads.
3. Coverage + mode (report honestly): count ads assessable by Signal A (cross-window history) vs only by Signal B (fresh). If most ads are fresh → say "this account rotates creatives quickly — showing current winners/losers rather than time-decay."
4. Classify each live ad into ONE bucket (tag the WHY):
conversion = 0 (clicks that don't convert) OR bottom-third vs current same-objective peers
(B: its engagement metric — and CVR/CPA for conversion ads — well below the median); AND recent_spend ≥ floor.5. Spend at risk = Σ recent_spend over Retire ads. Zero Retire → "No creatives to rotate right now."
Tag each Retire with its reason: decayed / underperforms current peers / clicks-don't-convert.
🎬 Creative Fatigue & Rotation Plan — {advertiser_name} ({advertiser_id}) · {currency}
Recent {recent_days}d vs baseline {baseline_days}d · Ads analyzed: {N} · Skipped (thin data <1000 impr): {M}
Mode: {time-decay ({A} ads have cross-window history) / current winners-losers (this account rotates creatives fast)}
★ Spend at risk (recent {recent_days}d on Retire creatives): {spend_at_risk}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🔴 Retire — fatiguing, rotate out soon
# Ad / creative Why (decayed / underperforms peers / no-conversions) Recent CTR/CVR vs baseline-or-peer {recent}d spend → manage-creative
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🟢 Scale — still winning, put more behind these → optimize-budget
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🟡 Watch — early decline / keep an eye on
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ♻️ Fresh candidates to rotate in (optional, from your library)
{if off/empty → "No unused library assets found."}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Assumptions & limits
· Fatigue = recent CTR < 60% of the 30-day baseline; for conversion ads also recent CVR < 70% of baseline.
Awareness ads (no conversions) are judged on CTR only.
· Excluded: ads with < 1000 baseline impressions ({M} skipped). "Spend at risk" is recent-window
spend on Retire ads, not a guaranteed loss. Candidate list is suggestion-only and may be empty.
Paywalled metrics show n/a.
🔍 Next steps (this skill changes nothing itself):
· Swap / retire / upload creatives → manage-creative · Budget behind winners → optimize-budget
· Full creative numbers → get-performance-report · Delivery issues → diagnose-campaign-health
Sort Retire by recent_spend desc (★ on the biggest). If every bucket is empty, say so and still
show analyzed/skipped counts + limits.
tools
Use only when the user explicitly asks to stage, commit, push, and open a GitHub pull request in one flow using the GitHub CLI (`gh`).
development
Use this skill any time a spreadsheet file is the primary input or output. This means any task where the user wants to: open, read, edit, or fix an existing .xlsx, .xlsm, .csv, or .tsv file (e.g., adding columns, computing formulas, formatting, charting, cleaning messy data); create a new spreadsheet from scratch or from other data sources; or convert between tabular file formats. Trigger especially when the user references a spreadsheet file by name or path — even casually (like "the xlsx in my downloads") — and wants something done to it or produced from it. Also trigger for cleaning or restructuring messy tabular data files (malformed rows, misplaced headers, junk data) into proper spreadsheets. The deliverable must be a spreadsheet file. Do NOT trigger when the primary deliverable is a Word document, HTML report, standalone Python script, database pipeline, or Google Sheets API integration, even if tabular data is involved.
development
Use this skill when turning messy workout information into clear logs, comparing user-provided sessions, surfacing trends or likely PRs, and suggesting realistic next-session steps.
tools
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.