agentleFS
loglylogly/mureo/skills/ad-fatigue-check/SKILL.md

ad-fatigue-check

Skill64 starsChanged 7 months agoApache-2.0View on GitHub

Detect creative fatigue across active ads — rising frequency, declining CTR week-over-week, and CPM drift — score each ad FATIGUED / WATCH / FRESH, and hand the evidence to a creative refresh. Use when the user asks whether creatives are worn out, why CTR is falling, if frequency is too high, when to rotate or refresh ads, or requests クリエイティブ疲弊チェック / 広告の疲弊を確認 / フリークエンシーが高い / CTRが落ちてきた / そろそろ差し替え時か. Reads active ads per platform, applies documented fatigue thresholds with noise guards, and routes fatigued ads to /creative-generate or /creative-refresh.

The file

We split this skill into the rules it gives an agent

3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Before you start: Run the Diagnostic preamble from ../_mureo-shared/SKILL.md — load learning insights (mureo_learning_insights_get) and …

    line 21
  2. Very specific:

    Load context: Read STRATEGY.md (Goals, Operation Mode) and STATE.json.

    line 25
  3. Distinctive:

    Do not silently produce new creative here — this skill diagnoses and routes.

    line 62

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Ad Fatigue Check

PREREQUISITE: Read ../_mureo-shared/SKILL.md for auth, security rules, output format, and Tool Selection (Read/Write on Code, mureo_strategy_* / mureo_state_* MCP on Desktop / Cowork).

Tool names below refer to the built-in platforms; a namespaced variant (see _mureo-shared → Namespaced built-in variants) is used with its prefix substituted.

Find the ads whose audience has seen them too many times. Creative fatigue is a slow leak — the same creative keeps spending while frequency climbs, CTR erodes, and CPM drifts up as the algorithm works harder to place a tiring ad. This skill reads active ads per platform, measures the fatigue signals the tools genuinely expose, scores each ad against documented thresholds (with noise guards so a single slow day never triggers a rotation), and hands the fatigued ones to a creative refresh with the evidence attached.

Prerequisites

  • STRATEGY.md and STATE.json must exist (run the onboard skill first)

Steps

Before you start: Run the Diagnostic preamble from ../_mureo-shared/SKILL.md — load learning insights (mureo_learning_insights_get) and consult advisors (mureo_consult_advisor) before drawing conclusions.

  1. Establish today: call mureo_state_get first, on every host (including Claude Code, where you would otherwise Read the file) and take server_now from its response — ISO 8601 with UTC offset, e.g. 2026-07-28T10:12:33+09:00. Its date is the only source of the current date for this run: the observation_due you write in step 9 is server_now's date + 14 days. (The week-over-week windows in step 3 are platform-side period tokens — last_7d / LAST_7_DAYS — so the platform resolves those against its own clock; do not hand-build date ranges for them.) Do not shell out (this skill must run in Bash-less headless hosts) and do not read the date off STATE.json — last_synced_at, reports.*.period and action_log timestamps are history, never evidence of what day it is now. Never write server_now into STATE.json: it is a response field, and a persisted copy becomes tomorrow's stale "today".
  2. Load context: Read STRATEGY.md (Goals, Operation Mode) and STATE.json. Note any learning insights about fatigue thresholds — a VALIDATED insight ("this account tolerates frequency 5 before CTR drops") overrides the default rubric in step 4.
  3. Discover active ads per platform — not running is not fatigued, on every platform. Before scoring anything, exclude each ad whose delivery-status field (effective_status when the platform reports one, otherwise status) is not that platform's active value, and say so rather than scoring it: a stopped ad has no fatigue to measure, and its soft numbers are the stop, not a worn-out creative. This exclusion is platform-independent — the bullets below only say which tool and which value each platform uses.
    • Meta: meta_ads_ads_list, and keep only ads whose effective_status is ACTIVE — that is the field that says an ad is really delivering. An ad can read status: ACTIVE while its effective_status is PAUSED / ADSET_PAUSED / CAMPAIGN_PAUSED / DISAPPROVED, and it is effective_status that decides. In BYOD mode this response is wrapped as {"source": "byod_import", "as_of": <import time>, "data": [...]} — take the rows from data and treat them as of as_of, not as live.
    • Google Ads: google_ads_ad_performance_report scoped to ENABLED ads.
    • Also include any hosted connector (TikTok, key tiktok_ads) / mcp__mureo__<plugin>_* plugin platform best-effort, reporting only what its own tools expose and emitting analytics_not_available_for_<platform> for the fatigue value-adds below — <platform> being the canonical platform key (plugin:<dist>:<provider> for a plugin, tiktok_ads for the connector), never an analytics registry_name (see ../_mureo-shared/SKILL.md → Canonical platform key). BYOD data is reachable only through mureo MCP tools — do not look for raw CSVs in the project directory.
  4. Measure fatigue signals — honestly, per what the tools expose:
    • Meta — frequency (derived, not a direct field). mureo's insights tools do not return a frequency field — but meta_ads_insights_report returns both impressions and reach, and frequency is defined as impressions ÷ reach. Compute it yourself from meta_ads_insights_report (level ad, window last_7d): frequency ≈ impressions / reach. This is Meta's own definition, so it is an honest derivation, not a guess. If reach is 0 or absent for an ad, the frequency is undefined — say so, do not divide.
    • Meta — CTR trend, week-over-week. meta_ads_insights_report (level ad) exposes ctr but no built-in trend. Get two adjacent weeks the same way weekly-report does: pull last_7d (this week) and last_14d, then derive the prior week as last_14d totals − last_7d totals. Compute the CTR change % between prior-week and this-week.
    • Meta — CPM drift. meta_ads_insights_report returns cpm; compare this-week vs prior-week CPM (same subtraction technique). Rising CPM on a flat audience is a fatigue corroborator. meta_ads_analysis_cost also decomposes a campaign's cost change into CPM inflation / CTR drop / creative fatigue drivers — use it to corroborate at campaign level.
    • Google Ads (Display / RSA whole-ad). Ad-level CTR trend from google_ads_ad_performance_report over adjacent windows (LAST_7_DAYS vs LAST_14_DAYS, same subtraction); Google Ads exposes no frequency at the ad level here, so score Google display/RSA ads on the CTR-trend signal only and say so.
    • Google Ads Search — RSA fatigue is asset-level, not ad-level. A Responsive Search Ad tires one asset at a time; whole-ad CTR hides it. Reference the RSA audit tools instead: google_ads_rsa_assets_audit (replacement recommendations) and google_ads_rsa_assets_analyze (per-asset performance_label + CTR) to find LOW/POOR assets to swap.
  5. Fatigue scoring rubric (document the thresholds; let learning insights override them):
    • FATIGUED — frequency ≥ 3.5 AND CTR decline ≥ 20% week-over-week (both signals present).
    • WATCH — exactly one signal present (frequency ≥ 3.5 alone, or CTR decline ≥ 20% alone, or a clear rising-CPM + softening-CTR pair).
    • FRESH — neither signal. These are defaults, not laws — a VALIDATED learning insight for this account/Persona replaces them, and Google display/RSA ads (no frequency) can reach WATCH on CTR decline alone but not the two-signal FATIGUED bar unless corroborated by CPM drift.
  6. Noise guards (apply before assigning any verdict):
    • Minimum impressions per window: require ≥ 1,000 impressions in each of the two adjacent windows before reading a CTR trend (the CTR sample floor from ../_mureo-learning/SKILL.md). Below that, the ad is insufficient-data, not fatigued.
    • Single-day dips never count — the comparison is week-over-week on 7-day windows precisely so one bad Tuesday cannot trigger a rotation.
    • Learning-state noise is not fatigue. An ad whose ad set is in its Meta learning phase (new, or recently reset by a significant edit), or whose Google Ads bid strategy shows "Learning", has unstable delivery by design — soft CTR and drifting CPM there are the learning window, not a worn-out creative. Mark those insufficient-data / re-check after the window, never FATIGUED, and do not rotate creative on that basis (a creative swap resets the phase again). See ../_mureo-pro-diagnosis/SKILL.md → Allocation & Learning-State Discipline.
    • Note confounders: a new competitor, a seasonal lull, or a landing-page change can mimic fatigue — flag them rather than blaming the creative.
  7. Ranked table — worst first:

    AdCampaignFrequencyCTR w/wCPM w/wImpr (this/prior)Verdict
    Ad AProspecting4.2−27%+14%32k / 30kFATIGUED
    Ad BRetargeting3.6−8%+3%18k / 17kWATCH
    Ad CBrand1.8−4%+1%21k / 20kFRESH
  8. Recommendations — rotate / refresh, with the evidence as the brief: For each FATIGUED (and borderline WATCH) ad, hand off to a creative workflow, passing the fatigue evidence (frequency, CTR decline, the tiring asset) as the input brief:
    • New visuals + composed banners → /creative-generate.
    • Copy / headline refresh, RSA asset swaps, Performance Max asset-group text → /creative-refresh. Do not silently produce new creative here — this skill diagnoses and routes; the creative skills generate under their own approval gates. Route, do not promise. Whether mureo can apply a rewrite depends on the surface, not on the verdict: /creative-refresh runs its Apply or draft check per item and may come back with copy for the operator to paste in by hand (every image, video and logo asset today, and any surface whose write tool is absent from the session). Hand the fatigue evidence over as the brief; do not tell the operator here that the creative will be rewritten, and do not offer the rewrite yourself — from this skill you cannot see which write tools cover the tiring asset.
  9. Pausing a clearly-fatigued ad — approval gate: Pausing is a write. Apply the Confirm Before Write Operations rule from ../_mureo-shared/SKILL.md: list the ad(s), their current spend/frequency/CTR, and confirm before pausing via meta_ads_ads_pause (Meta) or google_ads_ads_update_status (Google Ads). Prefer rotate-in-a-replacement over pause-into-a-gap unless the ad is actively harmful — pausing without a fresh creative ready starves delivery. Never bulk-pause without listing the total impact first.
  10. Record outcome context: For each ad paused or queued for refresh, append to action_log via mureo_state_action_log_append with metrics_at_action (the ad's own frequency, ctr, cpm, impressions, conversions, cpa and their period) and observation_due 14 days out from server_now's date (the creative-change window from ../_mureo-learning/SKILL.md). A pure diagnostic run with no writes still logs a summary entry (the verdict table), with no observation_due. Give every entry a display line: display_title (at most 40 characters) and display_summary (at most 120) — what this action WAS, in the operator's words, as plain text with no markdown (**bold** reaches a person as asterisks). The dashboard row shows those two and stops there; the full summary is drill-down only, so keep writing it as fully as the next agent needs and let the display line be the short one. Over either bound the append is refused, never truncated.
  11. Persist the report summary (best-effort): Call mureo_state_report_set with report="fatigue" and a concise summary object so the read-only dashboard can render this check without re-running you. Follow this convention:

    • generated_at: ISO 8601 timestamp of this run — use server_now
    • period: the two adjacent windows compared (this week / prior week)
    • totals: the account's headline figures, using the canonical metric vocabulary — spend, conversions, cpa, ctr, clicks, impressions. This is the block the dashboard renders as figures. Raw numbers only: 773957, not "¥773,957"; 0.0466, not "4.66%" — one of those keys carrying a string is refused, because it sits where the view reads a figure and renders as nothing. A key outside the vocabulary may ride along: it is stored, just not shown as a headline number. Omit totals entirely if this run gathered no account-level figures.
    • kpis: counts by verdict (fatigued / watch / fresh / insufficient-data) and the worst-frequency + worst-CTR-decline ads — the breakdown, not the headline row
    • flags: notable items (e.g. ["3_ads_fatigued", "meta_reach_missing_frequency_undefined", "google_display_ctr_only"])
    • narrative: the verdict (creatives fresh / rotation needed) and what you propose next, at most 400 characters — the tool refuses a longer one rather than truncating it (a sentence cut in half is worse than a long one). Do not restate the figures and do not list the findings here: numbers belong in totals, findings in flags.

    Reflect the FINAL state, and persist this LAST — after every action_log entry and any pause you applied this run. This is best-effort: if mureo_state_report_set is unavailable (e.g. a pure file-mode host without the context MCP), skip it silently — the rest of this skill still works.

  12. Persist the display contract (best-effort): Call mureo_state_display_set with what the DASHBOARD shows. This is a different audience from the summary above — STATE.json is written for the next agent, and this small bounded section is the only thing the operator's screen reads. Write it in the SAME pass as the report, from the SAME figures, and reach no new verdict here: every value below is one you already decided, and this step only renders it.

    • source: your own skill name — REQUIRED whenever you write any section, at most 24 characters. The contract is replaced wholesale by whoever writes it last, so this is what lets the card say whose answer it is showing. generated_at is stamped by the server — do not compute it.
    • nav_message: the ONE line the operator should act on today — what the rotation needs, e.g. "2 ads fatigued — rotate replacements before spend follows". One line, not a summary of the report.
    • highlights: at most 3 chips, and the tone must match the verdict you already gave — good for a target met or a clear win, watch for something to keep an eye on, bad for something deteriorating. Choose the three that matter; a fourth is refused and nothing is dropped for you. Map a finding's severity to a chip tone: action → bad / watch → watch / positive → good. info does NOT become a highlight — a neutral note would spend one of the 3 chips an action or a win needed, and it is still in the report for whoever wants it.
    • breakdown.campaigns / .adgroups: one row per entity you judged, carrying figures you ALREADY hold — spend, mcpa (the measured CPA) and target_cpa — plus state from the closed set (target_met / improving / watch / worsening / no_data) and a note of at most 40 characters. Use campaigns for the campaigns you compared and adgroups for ad sets / ad groups; a skill that judged neither omits breakdown entirely rather than inventing rows. Omit a figure you do not have: 0 states a perfect CPA rather than the absence of one, and no_data is the state for an entity with too little delivery to judge.
    • proposals: the recommendations you did NOT carry out, one entry each — title (what to do) plus body (why, in one line) plus date = server_now's date as YYYY-MM-DD. Something you DID apply this run is the same entry with status: "done".
    • stated_values: label + figure chips ONLY ({"label": "…", "value": …}) — the counts and worst-case figures you reported: {"label": "fatigued ads", "value": 3}, {"label": "worst frequency", "value": 6.4}. No prose notes here. A sentence lands in a numeric column and the tool refuses it; anything that needs a sentence stays in the report's narrative above, exactly where it goes today.

    The bounds, verbatim from the tool: The dashboard reads THIS section and nothing else — keep your reasoning where it already goes. Every bound below refuses the write rather than truncating it, because a sentence cut in half is worse than a long one. nav_message: one line, at most 80 characters. highlights: at most 3 items of {tone, text}, tone one of good/watch/bad, text at most 60 characters. proposals: {title, body, status, date}, title at most 30 and body at most 80 characters, status one of proposed/done. breakdown.campaigns / breakdown.adgroups: rows of {name, spend, mcpa, target_cpa, state, note} — the three figures are raw numbers, state is one of target_met/improving/watch/worsening/no_data, note at most 40 characters. stated_values: {label, value}, label at most 24 characters and value a raw number or a string of at most 12 characters — a sentence there is refused, because it lands in a numeric column. Do NOT write the KPI funnel or the daily chart: mureo computes both from the stored totals.

    Over a bound the write is REFUSED — shorten and rewrite, do not re-send the same sentence trimmed. Lead with the point and drop the connectives; a noun phrase is fine (CPA 12% over target, not The CPA is currently running about 12% above the target we agreed). Trimming one character at a time and calling again spends the run's context on a bound one rewrite would have met.

    You may not be the first writer today. display is REPLACED WHOLE and the last writer wins — there is no merge. Before you write it, read the current one (mureo_state_get). Of what another skill wrote TODAY, carry exactly one thing into your own write: its proposals that are still live — not yet done, and not contradicted by what you just found. Everything else you write from your own run alone, because a screen assembled from two runs shows a moment that never happened. And carry over NOTHING ELSE: never copy another skill's nav_message, highlights, breakdown or stated_values, which would put its judgement under your name when you cannot vouch for it. Name yourself in source so the screen says whose answer it is.

    This is best-effort: if mureo_state_display_set is unavailable (e.g. a pure file-mode host without the context MCP), skip it silently — the rest of this skill still works.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.