Creative Agent — Technical Reference
Takes an approved topic and produces publishable copy — blog drafts, video scripts, hooks — under brand guidelines, with quality repair and SEO enforcement built into the loop.
Audience: engineers. Non-technical version: overview.md. Back to the agents index.
What it does
Three generators sharing one shape: brand context in → JSON payload out → deterministic repair passes → reviewable draft.
| Surface | Entry point | Output |
|---|---|---|
| Blog | generateBlogDraftFromQueueItem() | HTML body, title, meta, SEO keywords |
| Script | generateScriptDraftFromQueueItem() | Scene-structured short-form video script |
| Hooks | generateExperimentalHooks() | Opening lines against creator hook formulas |
The blog path is the most complete — it is the one with the auto-flow, the humanization pass and the CMS delivery. Treat it as the reference.
Where it lives
| Concern | File |
|---|---|
| Blog generator | app/src/services/blogs/blog-generator.ts |
| Blog auto-flow | app/src/services/blogs/blog-auto-flow.ts |
| Blog master prompt | app/src/services/blogs/blog-master-prompt.ts |
| SEO keyword selection | app/src/services/blogs/blog-keywords.ts, blog-seo-validation.ts |
| First-use bolding | app/src/services/blogs/blog-keyword-bold.ts |
| Humanization | app/src/services/blogs/blog-humanize.ts |
| Refine chat | app/src/services/blogs/blog-refine.ts |
| CMS delivery | app/src/services/blogs/blog-delivery.ts, adapters/ |
| Script generator | app/src/services/scripts/script-generator.ts (~3 600 lines) |
| Hook policy enforcement | app/src/services/scripts/hook-policy.ts |
| Hook generation | app/src/services/hooks/generation.ts |
| Job worker | app/src/lib/jobs/worker.ts, run-generation-job.ts |
| Editor + inline marks | app/src/components/modules/rich-text-editor.tsx |
Routes — blogs/queue, blogs/drafts/[draftId]/{generate,refine,retry,submit,thumbnail},
scripts/generate, scripts/generated/[scriptId]/regenerate, hooks/generate.
Trigger
Semi-automatic — a human chooses the topic, the agent does the rest.
Blog
Queue a news item → autoProcessBlogQueueItems() generates the draft in the
background and stops at state ready. It does not publish.
Script
POST /scripts/generate enqueues an async job in content_generation_jobs.
MongoDB is the queue — an in-process worker (lib/jobs/worker.ts, started by
instrumentation.ts) drains it:
- concurrency capped at 2 (free-tier safety)
- stale jobs reclaimed after 60 s
- multiple app instances coordinate via atomic claim
Input
Assembled per generation, all tenant-scoped:
| Context | Source |
|---|---|
| Source article | news item or social snapshot; content pruned to 6 000 chars |
| Brand | resolveBrandContext() — voice, avoid, description, categories |
| Blog master prompt | blogPrompt brand-guideline field (cap 12 000 chars) |
| Compliance rules | getTenantComplianceRules() |
| Competitor brief | buildCompetitorBrief(), pruned to 1 200 chars |
| SEO keywords | selectBlogKeywords() — short-tail + long-tail |
| Competitor inspiration | attached posts from the queue item |
| CMS base URL | from the tenant's blog integration, for internal links |
The blog master prompt
Per-tenant, edited on the Guidelines page. When set,
blog-master-prompt.ts renders it verbatim as a BRAND BLOG BRIEF section that
outranks the built-in tone/structure/SEO defaults — but never the
compliance rules or the JSON output contract. Blank means built-in only.
Unlike voice/avoid, it is deliberately absent from describeBrandForPrompt,
so it reaches blogs only — never script or image prompts.
npm run seed:iifl-brand -- <tenantId>
Output & the repair loop
Generation is not one shot. The blog path runs this sequence:
1. generateOnce() OpenAI, JSON, maxOutputTokens 8000
2. sanitizeBlogHtml() strip unsafe markup
3. ensureSourceLinkInBody() source attribution is not optional
4. applyKeywordBolding() first-use <strong> on assigned keywords
5. qualityIssues() + keywordIssues()
└─ if either non-empty → ONE quality-repair call, then re-check
6. hard fail if structural issues remain → 502, retriable
SEO issues do NOT block — they land in content_warnings
7. compliance pipeline: evaluate → optional regenerate with feedback
8. ZeroGPT detect → up to 2 structure-preserving humanization passes
9. deterministic blocked-phrase scan → content_warnings
10. save as state "ready"
Humanization detail
detectAiScore() → if over threshold, up to 2 humanizeText() passes. Each
pass is reverted if qualityIssues() regress after restoration — the original
draft is kept rather than shipping a degraded rewrite. Both ZeroGPT operations
fail open, recording "AI-detection check skipped" in warnings.
Script-specific enforcement
- Hook formulas are enforced, not suggested —
hook-policy.tsapplies the creator's formula and a cooldown (applyHookFormulaCooldown,opensLikeRecentHooks) so successive scripts do not open the same way. - Versions, regenerate, scene-level patching (
mergeScenePatchIntoPayload) and a full history/restore trail.
Model & prompt
Hard-collapsed to OpenAI. Not a preference — a pin, in every generator:
blog-generator.ts, script-generator.ts, hooks/generation.ts,
lib/claude.ts, lib/pipeline-providers.ts, lib/create-scripts-model.ts.
script-generator.ts still imports the provider chain, but its no-override
branch is unreachable because the file pins preferredProvider = "openai".
SCRIPT_GENERATION_CHAIN(defaultopenai,gemini,nvidia) only driveslib/ai/provider-chain.ts. Groq is not in the chain — it rate-limited constantly and 400s on JSON mode — but remains available as an explicitly user-selected provider.SCRIPT_GENERATION_PROVIDERdoes not exist in code.
Token budgets: blog draft and quality repair 8 000 each; usage tracked under
feature blogs, operations draft_generation / quality_repair.
Refine chat
blog-refine.ts — instruction capped at 600 chars. A refinement returns a
proposal the reviewer accepts or rejects; it is never applied silently. The
route writes nothing: accepting fills the editor, and Save persists.
The model returns the full revised body — the right contract for a model, the
wrong one for a reviewer — so the route also returns changed_blocks, a
block-level Before/After from lib/blog-body-diff.ts. That is what renders, in
the chat bubble that produced it (blog-refine-proposal.tsx), with Accept/Reject
on the card. The full Current vs Proposed comparison remains as an opt-in View
full body. A proposal whose base no longer matches the editor is marked stale
and cannot be accepted — accepting replaces the whole body and would discard the
manual edit.
This is also the delivery mechanism for the compliance agent's "Ask AI to fix"
action. That path additionally passes the finding structurally — evidence,
message, and the cited rule's title and rationale — so the model fixes the
cause the rule names instead of paraphrasing around the flagged words. Accepting
such a proposal records a resolution that clears the finding at the CMS gate; see
the compliance agent reference.
Failure mode
| Condition | Behaviour |
|---|---|
| Structural quality issues after repair | 502, retriable — no draft saved |
| SEO placement issues | Draft saves; issues surface in content_warnings |
| ZeroGPT unavailable | Fails open, warning recorded |
| Humanization degrades quality | Reverted, original retained, warning recorded |
| Compliance pipeline exhausts attempts | Draft saves with attempt count + score in warnings |
| No keywords matched | "No SEO keywords matched this topic." |
The pattern throughout: structure is hard-blocking, quality signals are advisory and visible. A reviewer sees every soft failure rather than an agent silently deciding it was good enough.
Access model
Blogs are not capability-gated. The BLOG_ALLOWED_EMAILS allowlist, the
per-user policy grant, the modules.blogs.enabled check and the
/dev/blog-admin console were all removed deliberately. Any session that owns
the tenant can generate, edit and publish to the live CMS.
The one exception: writing the CMS integration itself requires an admin
session (requireBlogCmsAdmin), configured in Settings → CMS integration. A
saved row is authoritative — bad credentials throw rather than silently
falling back to BLOG_CMS_* env vars, which apply only when a tenant has no row.
Known gaps
Measured against pages 4–6 of ../../../roadmap_ref_extracted.md:
- No image or video generation. The roadmap's
① Creativeline is Static — I · GIF — I · Video — E. Today the agent writes text; static, GIF and video are all unbuilt.blogs/drafts/[draftId]/thumbnailis the only image surface. - No Creative Repository (deliverable ⑦) — drafts live per-tenant, nothing indexes reusable assets across them.
- No template + UGC system. Page 4 calls for templates (standard + dynamic) under guard rails; today the brand prompt is free text.
- No vernacular fan-out.
Vernacappears on pages 3, 4 and 6 as a guard rail — no localization pass exists. - No RBI checkpoints inside the factory loop — see the compliance agent gaps.
- No social-media E2E automation (deliverable ③). Scripts generate and schedule to the calendar; nothing posts.
- Localization + Topicality and "Diff Approaches → Diff Themes" are not modelled — one prompt produces one angle.