Skip to main content
Hook exhaustion is Mission Control’s answer to “is my creative tired?” It’s a per-(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:
And the gate that decides exhausted vs. healthy:
All three conditions must hold. An archetype with high spend share but flat ROAS isn’t exhausted — it’s just dominant. An archetype with declining ROAS but minor spend share isn’t exhausted — it’s just a small failing bet.

Refresh schedule

Each tick:
  1. Find all client_ids that have at least one CreativeAnalysis row.
  2. For each tenant, build the per-archetype signal map for the last 28 days.
  3. update_or_create a HookExhaustionSnapshot per (client_id, archetype_slug, window_end=today).
  4. Compare to the prior is_exhausted=True set; for any new exhausted archetype, fire creative.hook_exhausted.
Re-running on the same data overwrites the snapshot — there is no append. Re-firing the automation trigger only happens on a False→True transition.

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.
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).
The analytics page Hook Exhaustion card uses these to render a click-through drawer: tap an archetype bar → see the bottom-3 creatives and the top-3 alternative archetypes side by side.

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.
Returns { "snapshots": [<HookExhaustionSnapshot>, ...] } ordered by (window_end desc, refreshed_at desc).

Bob tool

Returns a JSON list of snapshot objects. Bob uses it inline when a user asks “is my creative tired?” or “should I retire any archetypes?” The tool is read-only — it reads materialized snapshots, never recomputes.

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 on before_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.pyrefresh_hook_exhaustion_cache, _archetype_signals_for_client, _perf_for_window, _weekly_slope, _net_new_ratio, _alternatives, _emit_exhaustion_trigger.
  • marketing_resources/models/hook_exhaustion.pyHookExhaustionSnapshot.
  • marketing_resources/views/creative_health.py — the read endpoint.
  • automations/triggers/__init__.pycreative.hook_exhausted registration.