Creative Fatigue Read
Shared quality bar: ../references/output-standard.md. All numbers cited here
live in ../references/thresholds.md.
Fatigue is the most over-used word in creative reporting. Most ads called tired
are being served to a smaller audience, or into a more expensive auction, and
pausing them fixes nothing.
Use this skill when
One named creative that previously performed is declining across consecutive
weeks and the proposed action is to pause or refresh it. Also use it before
approving a refresh budget, so the money goes to a creative whose decline was
actually caused by the creative.
Required input
- A daily export for that one ad ID: date, impressions, reach, link clicks,
spend. Reach is required for both windows and impressions cannot stand in for
it. Daily reach summed across each window runs the frequency signals; where
the platform reports deduplicated reach for the window, use that and say which
of the two you used, since only the deduplicated figure supports the new-reach
reading below.
- The reach the creative had already accumulated before the prior window opens,
where the export can give it, since new reach cannot be computed without it.
- Placement breakdown for the same dates, or a statement that placements did
not change.
- The date of the last creative edit on that ad.
- Which Advantage+ creative enhancements were on during each window and the date
any of them changed (
test.creative_enhancements_rule). Text variation, image
expansion, music and 3D animation change the asset per impression, so an
enhancement switched on between the two windows means the second window is a
different asset and not a tired one.
- Any budget, bid or audience change inside the comparison period, with dates.
Analysis workflow
- Confirm the ad has been live longer than
fatigue.refresh_floor since its
last creative edit. Below that, stop and report that delivery has not
settled; anything read now is the learning period.
- Establish that both windows ran the same asset, using
test.creative_enhancements_rule. The rule is about the CHANGE between the
windows, not the enabled state: an enhancement on for both is a constant and
costs nothing here, while one switched on between them makes the CTR decline
a comparison of two different assets. This is the single most likely
manufactured cause of a false fatigue verdict on a current account, because
these features get enabled account-wide by default and nobody logs the date.
Where the answer is unknown, say so, cap the read at low, and do not write
the word fatigue.
- Set the comparison period from
fatigue.window and state it as two date
ranges, not as a duration. Apply the recalibration rule in that entry only
if it fires, and say so if you moved it.
- Gate on volume before reading any rate. Both windows must clear
fatigue.volume_floor. Below it in either window no signal may fire at all,
whatever the percentages look like, and the honest output is that this
creative has not been served enough to be read.
- Run
../scripts/fatigue_curve.py over the daily export for the signals it
implements, which its own docstring names. This arithmetic is not eyeballed.
A rate compared by eye across two uneven windows is the most reliable way to
produce a confident wrong answer.
- Read the CTR change against
fatigue.ctr_decline_flag, confirming the
figure is link CTR as defined in the denominators table, and confirming the
change is in the rate rather than the click count. Without this one there is
no fatigue verdict to reach, whatever else moved.
- Read all three candidate second signals from the same export and the same
two windows, since which one discriminates depends on the audience.
fatigue.frequency_rise_flag is the relative move and is the one that works
on retargeting and any small pool. fatigue.saturation_flag is the new
reach decline and is the one that works on broad prospecting; compute new
reach as window reach minus the reach the creative already had, and where the
export cannot give that figure, say so in the output and fall back to
fatigue.frequency_ceiling. Read fatigue.frequency_ceiling windowed, for
the same window as the CTR you just used, and reject any lifetime frequency
figure outright.
- Apply
fatigue.verdict_rule as revised: the CTR decline plus at least one of
those three, inside the same window, both windows above the volume floor.
Then apply its void condition. Where the only second signal is
fatigue.frequency_ceiling and frequency was already above the ceiling in
the prior window, the verdict is void, because a flag that was lit before the
decline began did not fire. Say void, not fatigue, and name what the CTR fall
points at instead.
- Read CPM against
fatigue.cpm_rise_flag last, and only as context on the
auction. It is not one of the second signals in fatigue.verdict_rule and it
never carries a verdict. Report it in the same breath as the CTR result or
leave it out of the output entirely.
- Check whether spend, placements or audience moved inside the window. If they
did, the finding drops to
low confidence under the confidence scale, and
the change becomes the leading hypothesis.
Decision rules
fatigue.ctr_decline_flag crossed and at least one of
fatigue.frequency_rise_flag, fatigue.saturation_flag or
fatigue.frequency_ceiling crossed in the same window, both windows above
fatigue.volume_floor: this is fatigue. cut the creative or test a
refresh of the same angle, and say which, on whether the angle has ever
produced a winner. Name in the row which second signal fired.
- CTR crossed and the only second signal is
fatigue.frequency_ceiling, which
the creative was already above in the prior window: the verdict is void under
fatigue.verdict_rule, not fatigue. investigate the audience, and say
plainly that the ceiling was already lit and therefore did not fire.
- CTR crossed and no second signal moved:
investigate the audience and the
auction. Label the creative reading hypothesis, not export.
- A second signal moved and CTR did not:
investigate audience size. The
finding is the audience, and the creative stays live.
- Either window under
fatigue.volume_floor: no verdict of any kind.
monitor, and state how much longer at current delivery puts both windows
over the floor.
- New reach cannot be computed from the export: say so, read
fatigue.frequency_ceiling in its place as fatigue.saturation_flag
instructs, and mark the row needs_data on the saturation question rather
than treating a missing signal as a negative one.
- CPM moved while CTR held: report no CPM row and issue no fatigue verdict, per
fatigue.cpm_rise_flag, which is reported alongside
fatigue.ctr_decline_flag or not at all. Decision investigate, because the
auction is outside the creative.
- An Advantage+ enhancement switched on between the two windows, or the change
dates are unknown: no fatigue verdict.
investigate under
test.creative_enhancements_rule and say that the current window served a
different asset from the prior one, so the decline is a comparison of two
creatives rather than a decay of one.
- Ad live under
fatigue.refresh_floor: monitor, no verdict, state the date
it becomes readable.
Output format
Open with one line: is this creative tired, yes, no or void, naming the CTR
result and which second signal fired or failed to.
| Signal |
Current window |
Prior window |
Change |
Threshold key |
Crossed |
Evidence |
Confidence |
This is the pack's time-series skill, so it labels confidence on the
time-series conditions in ../references/output-standard.md rather than on the
comparison ones, and it can reach high. Those conditions, stated here so a row
can be checked rather than trusted: the signals came from an export, both
windows were set by fatigue.window and are stated as dates, both windows clear
fatigue.volume_floor, and no creative edit and no Advantage+ creative
enhancement change fell inside either window
(test.creative_enhancements_rule). All four. Exactly one missing, and not the
volume one, is medium with the missing condition named. Either window under
fatigue.volume_floor is low on its own, as is any window carrying a creative,
budget, placement or enhancement change. test.volume_floor is not read here at
all: it counts conversions per variant in a test, and this skill is not reading a
test.
Then the decision, then What this could not see, Missing data, Approval gates.
Practical example
Illustrative made-up account data, not a real advertiser.
Input, one ad ID, two consecutive windows set by fatigue.window. Current
window: 412,000 impressions, 158,000 reach, 1,030 link clicks, 4,120 spend.
Prior window: 380,000 impressions, 152,000 reach, 1,330 link clicks, 3,420
spend. Last creative edit 61 days ago, so fatigue.refresh_floor is cleared.
Enhancements: text variation was on across both windows and image expansion was
switched on four days into the current one. Under
test.creative_enhancements_rule the first is a constant and the second is not,
so before any signal is read the output records that part of the current window
served an asset the prior window never carried.
Both windows sit well above fatigue.volume_floor on impressions, so the
signals are allowed to fire at all, which is the first thing checked.
../scripts/fatigue_curve.py output: current link CTR 1,030 divided by 412,000
is 0.250 per cent. Prior link CTR 1,330 divided by 380,000 is 0.350 per cent.
The relative change is 0.100 over 0.350, a fall of 28.6 per cent, which crosses
fatigue.ctr_decline_flag.
Second signals, all three read on the same two windows. Windowed frequency is
412,000 over 158,000, which is 2.61 impressions per person, and sits below
fatigue.frequency_ceiling. Prior windowed frequency is 380,000 over 152,000,
which is 2.50, so the relative rise is 2.61 against 2.50, about 4 per cent, far
under fatigue.frequency_rise_flag. New reach cannot be computed: the export
supplied no cumulative reach before the prior window, so
fatigue.saturation_flag is needs_data and, as that entry instructs, the
fallback is the frequency ceiling, which is not crossed either. CPM is 10.00
against 9.00, a rise of 11.1 per cent, under fatigue.cpm_rise_flag, and it is
reported here only because the CTR result is being reported beside it.
Verdict: no. Two things stop it independently. The CTR decline crossed and not
one of the three second signals did, so fatigue.verdict_rule is not satisfied;
and the image expansion switched on mid-window means the two windows are not the
same asset, so under test.creative_enhancements_rule the decline has a
manufactured explanation before fatigue is even reached. This skill cannot
conclude fatigue. Falling CTR against flat frequency and unknown new reach points at the
audience or the auction, and reach grew alongside impressions, so the leading
hypothesis is a placement or audience shift. Decision investigate, not
cut. Three things would settle it: the placement breakdown, the creative's
cumulative reach, which would let fatigue.saturation_flag be read rather than
assumed absent, and a re-read on two windows that both sit entirely after the
enhancement change.
Guardrails
- Never declare fatigue on one signal, whatever the client expects.
- Never use lifetime frequency.
- Never compare click counts where the rule is written on rates.
- Never read fatigue on account impressions instead of creative impressions.
- Do not pause anything. Recommend, and let a human approve.
1---2name: creative-fatigue-read-ad-creative3description: Creative Fatigue Read4---56# Creative Fatigue Read78Shared quality bar: `../references/output-standard.md`. All numbers cited here9live in `../references/thresholds.md`.1011Fatigue is the most over-used word in creative reporting. Most ads called tired12are being served to a smaller audience, or into a more expensive auction, and13pausing them fixes nothing.1415## Use this skill when1617One named creative that previously performed is declining across consecutive18weeks and the proposed action is to pause or refresh it. Also use it before19approving a refresh budget, so the money goes to a creative whose decline was20actually caused by the creative.2122## Required input2324- A daily export for that one ad ID: date, impressions, reach, link clicks,25 spend. Reach is required for both windows and impressions cannot stand in for26 it. Daily reach summed across each window runs the frequency signals; where27 the platform reports deduplicated reach for the window, use that and say which28 of the two you used, since only the deduplicated figure supports the new-reach29 reading below.30- The reach the creative had already accumulated before the prior window opens,31 where the export can give it, since new reach cannot be computed without it.32- Placement breakdown for the same dates, or a statement that placements did33 not change.34- The date of the last creative edit on that ad.35- Which Advantage+ creative enhancements were on during each window and the date36 any of them changed (`test.creative_enhancements_rule`). Text variation, image37 expansion, music and 3D animation change the asset per impression, so an38 enhancement switched on between the two windows means the second window is a39 different asset and not a tired one.40- Any budget, bid or audience change inside the comparison period, with dates.4142## Analysis workflow43441. Confirm the ad has been live longer than `fatigue.refresh_floor` since its45 last creative edit. Below that, stop and report that delivery has not46 settled; anything read now is the learning period.472. Establish that both windows ran the same asset, using48 `test.creative_enhancements_rule`. The rule is about the CHANGE between the49 windows, not the enabled state: an enhancement on for both is a constant and50 costs nothing here, while one switched on between them makes the CTR decline51 a comparison of two different assets. This is the single most likely52 manufactured cause of a false fatigue verdict on a current account, because53 these features get enabled account-wide by default and nobody logs the date.54 Where the answer is unknown, say so, cap the read at `low`, and do not write55 the word fatigue.563. Set the comparison period from `fatigue.window` and state it as two date57 ranges, not as a duration. Apply the recalibration rule in that entry only58 if it fires, and say so if you moved it.594. Gate on volume before reading any rate. Both windows must clear60 `fatigue.volume_floor`. Below it in either window no signal may fire at all,61 whatever the percentages look like, and the honest output is that this62 creative has not been served enough to be read.635. Run `../scripts/fatigue_curve.py` over the daily export for the signals it64 implements, which its own docstring names. This arithmetic is not eyeballed.65 A rate compared by eye across two uneven windows is the most reliable way to66 produce a confident wrong answer.676. Read the CTR change against `fatigue.ctr_decline_flag`, confirming the68 figure is link CTR as defined in the denominators table, and confirming the69 change is in the rate rather than the click count. Without this one there is70 no fatigue verdict to reach, whatever else moved.717. Read all three candidate second signals from the same export and the same72 two windows, since which one discriminates depends on the audience.73 `fatigue.frequency_rise_flag` is the relative move and is the one that works74 on retargeting and any small pool. `fatigue.saturation_flag` is the new75 reach decline and is the one that works on broad prospecting; compute new76 reach as window reach minus the reach the creative already had, and where the77 export cannot give that figure, say so in the output and fall back to78 `fatigue.frequency_ceiling`. Read `fatigue.frequency_ceiling` windowed, for79 the same window as the CTR you just used, and reject any lifetime frequency80 figure outright.818. Apply `fatigue.verdict_rule` as revised: the CTR decline plus at least one of82 those three, inside the same window, both windows above the volume floor.83 Then apply its void condition. Where the only second signal is84 `fatigue.frequency_ceiling` and frequency was already above the ceiling in85 the prior window, the verdict is void, because a flag that was lit before the86 decline began did not fire. Say void, not fatigue, and name what the CTR fall87 points at instead.889. Read CPM against `fatigue.cpm_rise_flag` last, and only as context on the89 auction. It is not one of the second signals in `fatigue.verdict_rule` and it90 never carries a verdict. Report it in the same breath as the CTR result or91 leave it out of the output entirely.9210. Check whether spend, placements or audience moved inside the window. If they93 did, the finding drops to `low` confidence under the confidence scale, and94 the change becomes the leading hypothesis.9596## Decision rules9798- `fatigue.ctr_decline_flag` crossed and at least one of99 `fatigue.frequency_rise_flag`, `fatigue.saturation_flag` or100 `fatigue.frequency_ceiling` crossed in the same window, both windows above101 `fatigue.volume_floor`: this is fatigue. `cut` the creative or `test` a102 refresh of the same angle, and say which, on whether the angle has ever103 produced a winner. Name in the row which second signal fired.104- CTR crossed and the only second signal is `fatigue.frequency_ceiling`, which105 the creative was already above in the prior window: the verdict is void under106 `fatigue.verdict_rule`, not fatigue. `investigate` the audience, and say107 plainly that the ceiling was already lit and therefore did not fire.108- CTR crossed and no second signal moved: `investigate` the audience and the109 auction. Label the creative reading `hypothesis`, not `export`.110- A second signal moved and CTR did not: `investigate` audience size. The111 finding is the audience, and the creative stays live.112- Either window under `fatigue.volume_floor`: no verdict of any kind.113 `monitor`, and state how much longer at current delivery puts both windows114 over the floor.115- New reach cannot be computed from the export: say so, read116 `fatigue.frequency_ceiling` in its place as `fatigue.saturation_flag`117 instructs, and mark the row `needs_data` on the saturation question rather118 than treating a missing signal as a negative one.119- CPM moved while CTR held: report no CPM row and issue no fatigue verdict, per120 `fatigue.cpm_rise_flag`, which is reported alongside121 `fatigue.ctr_decline_flag` or not at all. Decision `investigate`, because the122 auction is outside the creative.123- An Advantage+ enhancement switched on between the two windows, or the change124 dates are unknown: no fatigue verdict. `investigate` under125 `test.creative_enhancements_rule` and say that the current window served a126 different asset from the prior one, so the decline is a comparison of two127 creatives rather than a decay of one.128- Ad live under `fatigue.refresh_floor`: `monitor`, no verdict, state the date129 it becomes readable.130131## Output format132133Open with one line: is this creative tired, yes, no or void, naming the CTR134result and which second signal fired or failed to.135136| Signal | Current window | Prior window | Change | Threshold key | Crossed | Evidence | Confidence |137|---|---|---|---|---|---|---|---|138139This is the pack's time-series skill, so it labels confidence on the140time-series conditions in `../references/output-standard.md` rather than on the141comparison ones, and it can reach `high`. Those conditions, stated here so a row142can be checked rather than trusted: the signals came from an export, both143windows were set by `fatigue.window` and are stated as dates, both windows clear144`fatigue.volume_floor`, and no creative edit and no Advantage+ creative145enhancement change fell inside either window146(`test.creative_enhancements_rule`). All four. Exactly one missing, and not the147volume one, is `medium` with the missing condition named. Either window under148`fatigue.volume_floor` is `low` on its own, as is any window carrying a creative,149budget, placement or enhancement change. `test.volume_floor` is not read here at150all: it counts conversions per variant in a test, and this skill is not reading a151test.152153Then the decision, then `What this could not see`, `Missing data`, `Approval154gates`.155156## Practical example157158Illustrative made-up account data, not a real advertiser.159160Input, one ad ID, two consecutive windows set by `fatigue.window`. Current161window: 412,000 impressions, 158,000 reach, 1,030 link clicks, 4,120 spend.162Prior window: 380,000 impressions, 152,000 reach, 1,330 link clicks, 3,420163spend. Last creative edit 61 days ago, so `fatigue.refresh_floor` is cleared.164Enhancements: text variation was on across both windows and image expansion was165switched on four days into the current one. Under166`test.creative_enhancements_rule` the first is a constant and the second is not,167so before any signal is read the output records that part of the current window168served an asset the prior window never carried.169Both windows sit well above `fatigue.volume_floor` on impressions, so the170signals are allowed to fire at all, which is the first thing checked.171172`../scripts/fatigue_curve.py` output: current link CTR 1,030 divided by 412,000173is 0.250 per cent. Prior link CTR 1,330 divided by 380,000 is 0.350 per cent.174The relative change is 0.100 over 0.350, a fall of 28.6 per cent, which crosses175`fatigue.ctr_decline_flag`.176177Second signals, all three read on the same two windows. Windowed frequency is178412,000 over 158,000, which is 2.61 impressions per person, and sits below179`fatigue.frequency_ceiling`. Prior windowed frequency is 380,000 over 152,000,180which is 2.50, so the relative rise is 2.61 against 2.50, about 4 per cent, far181under `fatigue.frequency_rise_flag`. New reach cannot be computed: the export182supplied no cumulative reach before the prior window, so183`fatigue.saturation_flag` is `needs_data` and, as that entry instructs, the184fallback is the frequency ceiling, which is not crossed either. CPM is 10.00185against 9.00, a rise of 11.1 per cent, under `fatigue.cpm_rise_flag`, and it is186reported here only because the CTR result is being reported beside it.187188Verdict: no. Two things stop it independently. The CTR decline crossed and not189one of the three second signals did, so `fatigue.verdict_rule` is not satisfied;190and the image expansion switched on mid-window means the two windows are not the191same asset, so under `test.creative_enhancements_rule` the decline has a192manufactured explanation before fatigue is even reached. This skill cannot193conclude fatigue. Falling CTR against flat frequency and unknown new reach points at the194audience or the auction, and reach grew alongside impressions, so the leading195`hypothesis` is a placement or audience shift. Decision `investigate`, not196`cut`. Three things would settle it: the placement breakdown, the creative's197cumulative reach, which would let `fatigue.saturation_flag` be read rather than198assumed absent, and a re-read on two windows that both sit entirely after the199enhancement change.200201## Guardrails202203- Never declare fatigue on one signal, whatever the client expects.204- Never use lifetime frequency.205- Never compare click counts where the rule is written on rates.206- Never read fatigue on account impressions instead of creative impressions.207- Do not pause anything. Recommend, and let a human approve.