(client × hook_archetype) composite signal materialized hourly into the HookExhaustionSnapshot table. When an archetype crosses the exhaustion threshold for the first time, the creative.hook_exhausted automation trigger fires — wire it to a Slack alert, a Note, or a custom agent handoff.
The signal
For each archetype that the tenant has any creatives in over the last 28 days, four sub-signals are computed:| Sub-signal | Definition | Interpretation |
|---|---|---|
spend_share | This archetype’s spend ÷ tenant’s total spend, in window. | How concentrated the bet is. |
roas_slope | Linear regression of weekly average ROAS, signed. | Negative = declining. |
ctr_slope | Same regression, on weekly average CTR. | Secondary fatigue indicator. |
net_new_ratio | Fraction of in-window creatives whose nearest cosine-prior is ≥ 0.15 distance away. | High = library is iterating; low = repeating. |
Refresh schedule
- Find all client_ids that have at least one
CreativeAnalysisrow. - For each tenant, build the per-archetype signal map for the last 28 days.
update_or_createaHookExhaustionSnapshotper(client_id, archetype_slug, window_end=today).- Compare to the prior
is_exhausted=Trueset; for any new exhausted archetype, firecreative.hook_exhausted.
The automation trigger
recommended_alternatives is a derived helper — top 3 archetypes (excluding the exhausted one) ranked by average ROAS in the same window.
Wire it through the Automations builder:
The trigger only fires on the transition. Once an archetype is exhausted, subsequent ticks re-write the snapshot but do not re-fire — so your Slack channel doesn’t get spammed every hour.
Drilldown
Each snapshot carries:top_underperformer_ids— the 3 lowest-ROAS creatives in this archetype.recommended_alternatives— the 3 highest-avg-ROAS archetypes (excluding the current).
Endpoint
Latest snapshot per
(client_id, archetype_slug). Tenant-scoped via shared.auth.get_accessible_client_ids.Query params:client_id— optional; required for non-superusers.
{ "snapshots": [<HookExhaustionSnapshot>, ...] } ordered by (window_end desc, refreshed_at desc).Bob tool
Tenant scoping
The_net_new_ratio computation has a subtle correctness bug if you’re not careful: cosine kNN on CreativeEmbedding would naturally walk across tenant boundaries. v2 adds explicit client_id=str(client_id) and kind='analysis_text' filters on both the prior-embeddings query and the in-window query. Cross-tenant leakage is impossible.
Tunable knobs
| Setting | Default | Purpose |
|---|---|---|
LOOKBACK_DAYS | 28 | Rolling window for spend / slope / net-new computation. |
NET_NEW_DISTANCE_THRESHOLD | 0.15 | Cosine distance below which an in-window creative is “near-prior” (not novel). |
| Composite weights | 0.4 / 0.3 / 0.3 | ROAS slope vs. recycling vs. spend concentration. |
| Threshold | composite ≥ 0.6 AND spend_share ≥ 0.20 AND roas_slope < 0 | The gate. |
_archetype_signals_for_client — adjusting them is a code change, not a settings flag, because changing the gate retroactively re-classifies historical snapshots. If you need a softer / harder gate per tenant, add a config row before changing the constants.
Worked example
A tenant running heavy onbefore_after for 8 weeks:
| Week | Spend share | Avg ROAS | CTR | Net-new | is_exhausted? |
|---|---|---|---|---|---|
| Week 1 | 0.18 | 2.6 | 1.4% | 0.45 | False (composite 0.41) |
| Week 4 | 0.24 | 2.1 | 1.3% | 0.32 | False (composite 0.51) |
| Week 6 | 0.27 | 1.7 | 1.1% | 0.18 | True (composite 0.69, slope −0.18) |
(False → True) transition fires creative.hook_exhausted once. The user’s Slack rule posts the alert. Subsequent ticks (Week 6.x) re-write the snapshot but do not re-fire. If the tenant pivots and the next week’s signal recovers (is_exhausted=False), then later regresses, the trigger fires again on that next False→True transition.
Where the code lives
marketing_resources/tasks/refresh_hook_exhaustion.py—refresh_hook_exhaustion_cache,_archetype_signals_for_client,_perf_for_window,_weekly_slope,_net_new_ratio,_alternatives,_emit_exhaustion_trigger.marketing_resources/models/hook_exhaustion.py—HookExhaustionSnapshot.marketing_resources/views/creative_health.py— the read endpoint.automations/triggers/__init__.py—creative.hook_exhaustedregistration.
.png?fit=max&auto=format&n=Frm2GFbmok4D-yJA&q=85&s=93c3ebd47542af65d1cd06d8563a7f6e)