(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:
The composite:
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:
1
Pick the trigger
In the Automations editor, choose
creative.hook_exhausted as the trigger.2
Add a Slack action
Action:
slack.send_message. Use the trigger payload as the body — for example:3
(Optional) Auto-create a Note
Action:
notes.create. Pre-fill title with the archetype name and link to /ad-creative-analytics so the team can acknowledge / track the response.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
The composite weights are baked in to
_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:
At Week 6, the
(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)