Creative Test Readout
Shared quality bar: ../references/output-standard.md. All numbers cited here
live in ../references/thresholds.md.
Most creative tests end in a tie and get reported as a win. That is how a
programme spends a quarter rolling out differences that were never there.
Use this skill when
A test with a named control has finished and the next step is a rollout, a
budget shift or a production brief based on the result. Also use it mid-flight
to check whether the test can ever become readable at current spend.
Required input
- The variant list with a control named explicitly, and what single element
differs in each pair.
- Per variant: impressions, link clicks, conversions, spend, over the test
window.
- The test window as start and end dates.
- Whether any creative, budget or audience change happened inside the window.
- Which conversion action counted, primary only or a real-money subset.
- Impressions per variant, needed for
test.impression_share_skew, and whether
the variants co-existed in one ad set or ran as a real split.
- Which Advantage+ creative enhancements were enabled on the ads, or a
statement that nobody knows.
Analysis workflow
- Confirm a control is named. Without one there is no test, only several ads,
and the readout stops here.
- Ask which Advantage+ creative enhancements were on. Under
test.creative_enhancements_rule, text variation, image expansion, music or
3D animation change the served asset per impression, so you changed one
element and the platform changed several more per person. With any of them
on, the readout says so and no creative conclusion is drawn. Where nobody
can say, the readout stops at low confidence and says why.
- Check each pair against
test.one_change_rule. A pair differing in two
elements produces a result that cannot be applied anywhere else, so report
it as unusable rather than ranking it.
- Check the elapsed window against
test.min_runtime and state it as dates.
- Check the variant count being READ as a test against
test.concurrent_variant_cap.
- Check delivery before reading any result, and separate the two states low
spend can mean before the skew check runs. A variant under
delivery.never_delivered_floor did not lose the test, it never entered it:
drop it out of the compared set entirely, give it its own row saying so, and
do not count its impressions in the total the skew check divides by. A
variant that cleared exposure and is still under delivery.starved_flag is
the opposite case and carries a verdict of its own, but under
delivery.ranking_floor it may not be compared with another variant, so it
leaves the winner comparison too and the row says which of the two floors
moved it. Then compute each remaining variant's
share of the compared set's total impressions and put it against
test.impression_share_skew. Below the floor, the variants ran against
different people at different times, so the comparison is confounded rather
than merely noisy and it is void: report no gap, name the skew, and stop.
No conversion count repairs this, so this check comes before the arithmetic,
not after it.
- Run
../scripts/test_readout.py over the variant CSV. It reports, per
variant, whether it clears test.volume_floor and test.click_floor, the
relative gap against the control, and whether that gap clears
test.material_gap. It also prints the minimum detectable difference at
each variant's own sample size for test.statistical_gate, so the gate is
read rather than trusted. Do not compute any of this by eye.
- Drop any variant under
test.volume_floor out of the winner comparison. It
can still be reported, but at low confidence under the confidence scale,
and it cannot win.
- Apply BOTH gates to every surviving variant. A winner has to clear
test.material_gap, which says the difference is worth acting on, AND
test.statistical_gate, which says the difference is there at all. Clearing
one and failing the other is a tie, and the output names which of the two
failed, because the two failures lead to opposite next moves: failing the
material gap means stop testing this element, failing the statistical gate
means keep going for more sample.
- Cap confidence on structure. Under
test.assignment_rule, variants that
co-existed in one ad set were allocated by predicted performance rather
than at random, so the readout cannot exceed medium however clean the
numbers are. high needs a real split: the platform's own A/B test tool,
or separate cells.
- Confirm which conversion denominator was used, primary actions or the
real-money subset, and state it. Also check
reporting.no_view_through_in_ctr before ranking anything on clicks. Where
the readout compares this window's conversions with a prior window, either
wait out the attribution window or state reporting.conversion_lag_bias
beside the number, since the current window is still filling in and the
comparison is biased against it by construction.
- Count the distinct live angles in the ad set against
coverage.angles_per_ad_set after collapsing them under
coverage.angle_distinctness. Below the floor, an angle effect and an
execution effect cannot be separated, so every verdict here drops to low.
- Write what would separate the variants next time: more spend, a wider
single change, or a different measure.
Decision rules
- Variant under
delivery.never_delivered_floor: it is not a result of any
kind. Keep it out of the compared set, out of the impression total behind
test.impression_share_skew, and out of the cut list, and say the finding is
allocation rather than the creative. A variant above that floor but under
delivery.ranking_floor ran and may carry a verdict elsewhere, but its share
of delivery is too thin for a like-for-like comparison, so it leaves the
winner comparison and the row names the floor that moved it.
- Any verdict arriving from the
early.* block: it does not enter this readout.
Under blocking rule 10 in ../AGENTS.md, everything in that block is low by
definition and is a cheap decision on creatives that will never reach
test.volume_floor, so an early hook or cost verdict is not a variant result,
is never compared against a control here, and does not become a finding
because it was repeated across a batch. Where somebody offers one as a test
input, say which skill produced it and read the test without it.
- Gap measured on cost per acquisition: judge it on
money.relevance_floor,
which owns that comparison everywhere in this pack, and read
test.material_gap only on rate gaps such as link CTR. test.statistical_gate
still applies to either. Naming which of the two relevance keys you used is
part of the output, because a cost gap judged on the rate key is how one
skill calls actionable what another calls a tie.
- Any compared variant under
test.impression_share_skew: the comparison is
void. investigate the allocation, report no gap at all, and say the test
cannot be repaired by running it longer. A void comparison carries no
confidence label at all, because void is the absence of a finding rather than
a weak one. This rule is applied before any other rule that reads a number,
and after only the two delivery exclusions above, which decide who is in the
compared set in the first place. A gap reported from skewed delivery is worse
than no gap.
- Gap clears
test.material_gap AND test.statistical_gate, and the winner
clears test.volume_floor: ship the winner, and name the one element that
carried it.
- Gap clears one gate and fails the other: tie, and the output names which one
failed. Inside
test.material_gap the decision is test again with a bigger
single change; failing test.statistical_gate alone the decision is
monitor and keep collecting, because the difference may be real and is not
yet readable. Never ship on one gate.
- Variants co-existed in one ad set: confidence is capped at
medium under
test.assignment_rule, whatever the volume, and the row says so. high is
available only on a real split.
- Any Advantage+ creative enhancement enabled, or nobody able to say: no
creative verdict under
test.creative_enhancements_rule. investigate which
enhancements ran, and cap the readout at low.
- Winner under
test.volume_floor: no winner, monitor, confidence low,
and say how many conversions short it is.
- Window under
test.min_runtime: monitor, no verdict, state the readable
date.
- Distinct live angles in the ad set below
coverage.angles_per_ad_set: every
verdict here drops to low, because the losing variant's angle cannot be
told apart from its execution. Name the count that capped it.
- Pair breaking
test.one_change_rule: investigate, and say the result
cannot be generalised.
Output format
Open with one line: winner, tie or void, naming the threshold key that decided
it. Where it is a tie, the line names which of the two gates failed,
test.material_gap or test.statistical_gate, because that is the part a
reader acts on.
| Variant |
Impressions |
Link clicks |
Conversions |
Rate |
Gap vs control |
Clears volume floor |
Evidence |
Decision |
Confidence |
Then one sentence on what would separate the variants, then What this could not see, Missing data, Approval gates.
Practical example
Illustrative made-up account data, not a real advertiser.
Input: three variants over a window that clears test.min_runtime, all three
co-existing in one ad set. Variant A is the control, 260,000 impressions, 1,240
link clicks and 62 conversions. Variant B changes the hook only, 248,000
impressions, 1,190 link clicks and 71 conversions. Variant C changes the hook
and the image, 44,000 impressions, 210 link clicks and 9 conversions. No budget
or creative change inside the window, and the advertiser confirms every
Advantage+ creative enhancement was off, so test.creative_enhancements_rule
does not bite. The ad set holds 3 distinct live angles once
coverage.angle_distinctness is applied, at coverage.angles_per_ad_set, so no
coverage cap fires.
Delivery first. C is out of the comparison before any arithmetic, on
test.one_change_rule and on 9 conversions against test.volume_floor, so the
skew check runs across A and B, which total 508,000 impressions. A holds 51.2
per cent and B 48.8 per cent, both clear of test.impression_share_skew, so
the comparison is readable rather than void.
../scripts/test_readout.py output: A converts at 62 over 1,240, which is 5.00
per cent. B converts at 71 over 1,190, which is 5.97 per cent. The relative gap
is 0.97 over 5.00, which is 19.3 per cent, and that sits below
test.material_gap. The minimum detectable difference at these two sample
sizes is 1.81 percentage points against an observed difference of 0.97
percentage points, so the gap also fails test.statistical_gate. Both A and B
clear test.volume_floor and test.click_floor.
Declined: B is not named the winner, and the readout says which gates it
failed rather than reporting a bare tie. It failed both, which is the least
ambiguous outcome available here: the difference is neither big enough to act
on nor big enough to be sure it exists, so more sample would not rescue this
particular pair. Variant C is refused entirely, on the two counts above; even a
large gap on C would be unusable.
Output: "Tie. Failed test.material_gap and test.statistical_gate." A and B
rows are export at medium confidence, not high: they co-existed in one ad
set, so test.assignment_rule caps them by construction, and the row states
that rather than implying the numbers were the limit. C row needs_data and
low. Decision test again with a wider single change to the hook. What would
separate them: a change large enough to clear the material gap, run as a real
split rather than two ads sharing one ad set's delivery.
Guardrails
- Never name a winner on one gate.
test.material_gap and
test.statistical_gate both, or it is a tie.
- Never report a gap from a comparison that failed
test.impression_share_skew. Unequal delivery is confounding, not noise.
- Never label a readout on co-existing variants
high.
- Never let a variant under
test.volume_floor win on a large-looking rate.
- Never rank on link CTR alone where the objective is conversions.
- Never mix primary and real-money conversion counts across variants.
- Do not shift budget or pause a variant. Recommend, and let a human approve.
1---2name: creative-test-readout-ad-creative3description: Creative Test Readout4---56# Creative Test Readout78Shared quality bar: `../references/output-standard.md`. All numbers cited here9live in `../references/thresholds.md`.1011Most creative tests end in a tie and get reported as a win. That is how a12programme spends a quarter rolling out differences that were never there.1314## Use this skill when1516A test with a named control has finished and the next step is a rollout, a17budget shift or a production brief based on the result. Also use it mid-flight18to check whether the test can ever become readable at current spend.1920## Required input2122- The variant list with a control named explicitly, and what single element23 differs in each pair.24- Per variant: impressions, link clicks, conversions, spend, over the test25 window.26- The test window as start and end dates.27- Whether any creative, budget or audience change happened inside the window.28- Which conversion action counted, primary only or a real-money subset.29- Impressions per variant, needed for `test.impression_share_skew`, and whether30 the variants co-existed in one ad set or ran as a real split.31- Which Advantage+ creative enhancements were enabled on the ads, or a32 statement that nobody knows.3334## Analysis workflow35361. Confirm a control is named. Without one there is no test, only several ads,37 and the readout stops here.382. Ask which Advantage+ creative enhancements were on. Under39 `test.creative_enhancements_rule`, text variation, image expansion, music or40 3D animation change the served asset per impression, so you changed one41 element and the platform changed several more per person. With any of them42 on, the readout says so and no creative conclusion is drawn. Where nobody43 can say, the readout stops at `low` confidence and says why.443. Check each pair against `test.one_change_rule`. A pair differing in two45 elements produces a result that cannot be applied anywhere else, so report46 it as unusable rather than ranking it.474. Check the elapsed window against `test.min_runtime` and state it as dates.485. Check the variant count being READ as a test against49 `test.concurrent_variant_cap`.506. Check delivery before reading any result, and separate the two states low51 spend can mean before the skew check runs. A variant under52 `delivery.never_delivered_floor` did not lose the test, it never entered it:53 drop it out of the compared set entirely, give it its own row saying so, and54 do not count its impressions in the total the skew check divides by. A55 variant that cleared exposure and is still under `delivery.starved_flag` is56 the opposite case and carries a verdict of its own, but under57 `delivery.ranking_floor` it may not be compared with another variant, so it58 leaves the winner comparison too and the row says which of the two floors59 moved it. Then compute each remaining variant's60 share of the compared set's total impressions and put it against61 `test.impression_share_skew`. Below the floor, the variants ran against62 different people at different times, so the comparison is confounded rather63 than merely noisy and it is void: report no gap, name the skew, and stop.64 No conversion count repairs this, so this check comes before the arithmetic,65 not after it.667. Run `../scripts/test_readout.py` over the variant CSV. It reports, per67 variant, whether it clears `test.volume_floor` and `test.click_floor`, the68 relative gap against the control, and whether that gap clears69 `test.material_gap`. It also prints the minimum detectable difference at70 each variant's own sample size for `test.statistical_gate`, so the gate is71 read rather than trusted. Do not compute any of this by eye.728. Drop any variant under `test.volume_floor` out of the winner comparison. It73 can still be reported, but at `low` confidence under the confidence scale,74 and it cannot win.759. Apply BOTH gates to every surviving variant. A winner has to clear76 `test.material_gap`, which says the difference is worth acting on, AND77 `test.statistical_gate`, which says the difference is there at all. Clearing78 one and failing the other is a tie, and the output names which of the two79 failed, because the two failures lead to opposite next moves: failing the80 material gap means stop testing this element, failing the statistical gate81 means keep going for more sample.8210. Cap confidence on structure. Under `test.assignment_rule`, variants that83 co-existed in one ad set were allocated by predicted performance rather84 than at random, so the readout cannot exceed `medium` however clean the85 numbers are. `high` needs a real split: the platform's own A/B test tool,86 or separate cells.8711. Confirm which conversion denominator was used, primary actions or the88 real-money subset, and state it. Also check89 `reporting.no_view_through_in_ctr` before ranking anything on clicks. Where90 the readout compares this window's conversions with a prior window, either91 wait out the attribution window or state `reporting.conversion_lag_bias`92 beside the number, since the current window is still filling in and the93 comparison is biased against it by construction.9412. Count the distinct live angles in the ad set against95 `coverage.angles_per_ad_set` after collapsing them under96 `coverage.angle_distinctness`. Below the floor, an angle effect and an97 execution effect cannot be separated, so every verdict here drops to `low`.9813. Write what would separate the variants next time: more spend, a wider99 single change, or a different measure.100101## Decision rules102103- Variant under `delivery.never_delivered_floor`: it is not a result of any104 kind. Keep it out of the compared set, out of the impression total behind105 `test.impression_share_skew`, and out of the cut list, and say the finding is106 allocation rather than the creative. A variant above that floor but under107 `delivery.ranking_floor` ran and may carry a verdict elsewhere, but its share108 of delivery is too thin for a like-for-like comparison, so it leaves the109 winner comparison and the row names the floor that moved it.110- Any verdict arriving from the `early.*` block: it does not enter this readout.111 Under blocking rule 10 in `../AGENTS.md`, everything in that block is `low` by112 definition and is a cheap decision on creatives that will never reach113 `test.volume_floor`, so an early hook or cost verdict is not a variant result,114 is never compared against a control here, and does not become a finding115 because it was repeated across a batch. Where somebody offers one as a test116 input, say which skill produced it and read the test without it.117- Gap measured on cost per acquisition: judge it on `money.relevance_floor`,118 which owns that comparison everywhere in this pack, and read119 `test.material_gap` only on rate gaps such as link CTR. `test.statistical_gate`120 still applies to either. Naming which of the two relevance keys you used is121 part of the output, because a cost gap judged on the rate key is how one122 skill calls actionable what another calls a tie.123- Any compared variant under `test.impression_share_skew`: the comparison is124 void. `investigate` the allocation, report no gap at all, and say the test125 cannot be repaired by running it longer. A void comparison carries no126 confidence label at all, because void is the absence of a finding rather than127 a weak one. This rule is applied before any other rule that reads a number,128 and after only the two delivery exclusions above, which decide who is in the129 compared set in the first place. A gap reported from skewed delivery is worse130 than no gap.131- Gap clears `test.material_gap` AND `test.statistical_gate`, and the winner132 clears `test.volume_floor`: `ship` the winner, and name the one element that133 carried it.134- Gap clears one gate and fails the other: tie, and the output names which one135 failed. Inside `test.material_gap` the decision is `test` again with a bigger136 single change; failing `test.statistical_gate` alone the decision is137 `monitor` and keep collecting, because the difference may be real and is not138 yet readable. Never `ship` on one gate.139- Variants co-existed in one ad set: confidence is capped at `medium` under140 `test.assignment_rule`, whatever the volume, and the row says so. `high` is141 available only on a real split.142- Any Advantage+ creative enhancement enabled, or nobody able to say: no143 creative verdict under `test.creative_enhancements_rule`. `investigate` which144 enhancements ran, and cap the readout at `low`.145- Winner under `test.volume_floor`: no winner, `monitor`, confidence `low`,146 and say how many conversions short it is.147- Window under `test.min_runtime`: `monitor`, no verdict, state the readable148 date.149- Distinct live angles in the ad set below `coverage.angles_per_ad_set`: every150 verdict here drops to `low`, because the losing variant's angle cannot be151 told apart from its execution. Name the count that capped it.152- Pair breaking `test.one_change_rule`: `investigate`, and say the result153 cannot be generalised.154155## Output format156157Open with one line: winner, tie or void, naming the threshold key that decided158it. Where it is a tie, the line names which of the two gates failed,159`test.material_gap` or `test.statistical_gate`, because that is the part a160reader acts on.161162| Variant | Impressions | Link clicks | Conversions | Rate | Gap vs control | Clears volume floor | Evidence | Decision | Confidence |163|---|---|---|---|---|---|---|---|---|---|164165Then one sentence on what would separate the variants, then `What this could166not see`, `Missing data`, `Approval gates`.167168## Practical example169170Illustrative made-up account data, not a real advertiser.171172Input: three variants over a window that clears `test.min_runtime`, all three173co-existing in one ad set. Variant A is the control, 260,000 impressions, 1,240174link clicks and 62 conversions. Variant B changes the hook only, 248,000175impressions, 1,190 link clicks and 71 conversions. Variant C changes the hook176and the image, 44,000 impressions, 210 link clicks and 9 conversions. No budget177or creative change inside the window, and the advertiser confirms every178Advantage+ creative enhancement was off, so `test.creative_enhancements_rule`179does not bite. The ad set holds 3 distinct live angles once180`coverage.angle_distinctness` is applied, at `coverage.angles_per_ad_set`, so no181coverage cap fires.182183Delivery first. C is out of the comparison before any arithmetic, on184`test.one_change_rule` and on 9 conversions against `test.volume_floor`, so the185skew check runs across A and B, which total 508,000 impressions. A holds 51.2186per cent and B 48.8 per cent, both clear of `test.impression_share_skew`, so187the comparison is readable rather than void.188189`../scripts/test_readout.py` output: A converts at 62 over 1,240, which is 5.00190per cent. B converts at 71 over 1,190, which is 5.97 per cent. The relative gap191is 0.97 over 5.00, which is 19.3 per cent, and that sits below192`test.material_gap`. The minimum detectable difference at these two sample193sizes is 1.81 percentage points against an observed difference of 0.97194percentage points, so the gap also fails `test.statistical_gate`. Both A and B195clear `test.volume_floor` and `test.click_floor`.196197Declined: B is not named the winner, and the readout says which gates it198failed rather than reporting a bare tie. It failed both, which is the least199ambiguous outcome available here: the difference is neither big enough to act200on nor big enough to be sure it exists, so more sample would not rescue this201particular pair. Variant C is refused entirely, on the two counts above; even a202large gap on C would be unusable.203204Output: "Tie. Failed `test.material_gap` and `test.statistical_gate`." A and B205rows are `export` at `medium` confidence, not `high`: they co-existed in one ad206set, so `test.assignment_rule` caps them by construction, and the row states207that rather than implying the numbers were the limit. C row `needs_data` and208`low`. Decision `test` again with a wider single change to the hook. What would209separate them: a change large enough to clear the material gap, run as a real210split rather than two ads sharing one ad set's delivery.211212## Guardrails213214- Never name a winner on one gate. `test.material_gap` and215 `test.statistical_gate` both, or it is a tie.216- Never report a gap from a comparison that failed217 `test.impression_share_skew`. Unequal delivery is confounding, not noise.218- Never label a readout on co-existing variants `high`.219- Never let a variant under `test.volume_floor` win on a large-looking rate.220- Never rank on link CTR alone where the objective is conversions.221- Never mix primary and real-money conversion counts across variants.222- Do not shift budget or pause a variant. Recommend, and let a human approve.