CreativeBrief row, one per (client_id, date)) generated once per day from the full creative-library state, surfaced identically on the Ad Creative Analytics page, in the daily Slack digest, and through Bob’s recommend_next_creative_brief tool.
The brief is a single source of truth — the same row you see on the page is what Slack posts and what Bob returns. Regenerating from the UI updates all three surfaces atomically.
What’s in a brief
| Field | Type | What it is |
|---|---|---|
headline | string | One-sentence summary of the creative-health signal. |
hook_concept | string | 1–2 sentence creative concept to test. The actual brief. |
recommended_archetype | slug from hook_type | The hook archetype to bet on next. |
recommended_format | slug from format | UGC, studio shot, demo, etc. |
recommended_audience | slug from audience_signal | Gen Z, parents, value-conscious, etc. |
recommended_messaging_theme | slug from messaging_theme | Social proof, problem-solution, founder story, etc. |
rationale | array of strings | 3–5 evidence-backed bullets, each citing a specific data point (an exhausted archetype, a top performer, a benchmark). |
risks | array of strings | 1–2 risks of pursuing this bet. |
inspirations | array of {creative_id, why} | Top performers from the user’s own library that match the recommendation. |
benchmark_snapshot | JSON | Per-archetype p50/p75 ROAS / CTR at generation time, frozen for traceability. |
cost_cents, tokens_in, tokens_out, model | metadata | OpenAI cost ledger. |
input_payload_sha | sha256 | Hash of the LLM input payload — same hash + same (client_id, date) cache-hits without re-spending. |
status | enum | draft / published / failed. The page only renders published. |
Generation
The generator lives atmarketing_resources/services/brief_generator.py and is called from three places:
- The hourly Celery cron (
marketing_resources.fanout_daily_briefs) — fans out per-tenant generation when each tenant’s local time matches its configured digest hour. - The
POST /api/marketing_resources/creatives/daily-brief/regenerate/endpoint — async, returns a Celery task id. - Bob’s
recommend_next_creative_brieftool — synchronous if no brief exists for today, otherwise just fetches.
Context payload
Before calling the LLM,build_context_payload(client_id) assembles a structured JSON snapshot of the tenant’s creative library:
allowed_slugs— the LLM is given the live taxonomy and instructed to draw recommendations from it. Slug coercion runs on the response anyway as a belt-and-suspenders.as_of_date, notas_of— the payload uses date-only granularity so the SHA is stable across same-day re-runs.
LLM call
GPT-4o is called with a strict JSON schema andresponse_format='json_object'. Temperature is 0.4 — mild creativity, mostly evidence-driven. The system prompt enforces:
- Recommended slugs MUST come from
allowed_slugs. - Rationale bullets MUST cite specific data from the payload.
- At most 5 rationale, 2 risks, 5 inspirations.
- Don’t recommend an exhausted archetype unless every alternative is also exhausted.
Persistence + idempotency
status='failed' and the headline set to "Brief generation failed: <reason>". The UI shows a retry hint.
Endpoints
Latest published brief for the caller’s tenant.Query params:
client_id— optional if the caller has exactly one accessible tenant.date— optionalYYYY-MM-DD; defaults to most-recent.
{ "brief": <CreativeBrief> | null, "message"?: string }.POST /api/marketing_resources/creatives/daily-brief/regenerate/
Force a regeneration. Body:
{ "client_id": "..." }. Returns 202 Accepted with a Celery task id.Useful when:- The user wants to regenerate after a fresh creative ingest.
- You want to re-run with
force=Truesemantics.
Last N briefs for the sidebar history drawer.Query params:
client_id— required (or implicit for single-tenant users).limit— default 30, max 90.
{ "briefs": [<CreativeBrief>, ...] } ordered by (date desc, generated_at desc).shared.auth.get_accessible_client_ids. A non-superuser of tenant B asking for tenant A’s brief gets a 400 — never another tenant’s data.
Bob tool
- If a published brief exists for
(target, today)→ return its serialized form. - Else (or if
force_regenerate=True) → callgenerate_brief(target, force=force_regenerate)synchronously and return the result.
Slack delivery
Each morning, the existingslack_bot.daily_status_digest job (already running for tenants with daily_digest_enabled=True) loads today’s CreativeBrief and appends it to the Mission Briefing message:
/ad-creative-analytics. Tenants without a published brief for today simply omit the block.
Regeneration & history
The analytics page exposes:- A Regenerate button —
POST /daily-brief/regenerate/. Briefs typically come back in ~10–15 seconds. The page polls and replaces the rendered card on completion. - A History button — opens a side drawer reading
GET /daily-brief/history/?limit=30. Shows previous days’ headline + recommendation badges so trends are visible.
recommend_next_creative_brief with client_id plus a follow-up daily_brief?date=... query under the hood.
Slug coercion
LLMs occasionally drift outside the allowed slug set. The generator runs a defensive coercion step:recommended_archetype is always a valid hook_type slug — the analytics page badges never link to ghost archetypes.
.png?fit=max&auto=format&n=Frm2GFbmok4D-yJA&q=85&s=93c3ebd47542af65d1cd06d8563a7f6e)