Content OS

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.

SurfaceEntry pointOutput
BloggenerateBlogDraftFromQueueItem()HTML body, title, meta, SEO keywords
ScriptgenerateScriptDraftFromQueueItem()Scene-structured short-form video script
HooksgenerateExperimentalHooks()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

ConcernFile
Blog generatorapp/src/services/blogs/blog-generator.ts
Blog auto-flowapp/src/services/blogs/blog-auto-flow.ts
Blog master promptapp/src/services/blogs/blog-master-prompt.ts
SEO keyword selectionapp/src/services/blogs/blog-keywords.ts, blog-seo-validation.ts
First-use boldingapp/src/services/blogs/blog-keyword-bold.ts
Humanizationapp/src/services/blogs/blog-humanize.ts
Refine chatapp/src/services/blogs/blog-refine.ts
CMS deliveryapp/src/services/blogs/blog-delivery.ts, adapters/
Script generatorapp/src/services/scripts/script-generator.ts (~3 600 lines)
Hook policy enforcementapp/src/services/scripts/hook-policy.ts
Hook generationapp/src/services/hooks/generation.ts
Job workerapp/src/lib/jobs/worker.ts, run-generation-job.ts
Editor + inline marksapp/src/components/modules/rich-text-editor.tsx

Routesblogs/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:

ContextSource
Source articlenews item or social snapshot; content pruned to 6 000 chars
BrandresolveBrandContext() — voice, avoid, description, categories
Blog master promptblogPrompt brand-guideline field (cap 12 000 chars)
Compliance rulesgetTenantComplianceRules()
Competitor briefbuildCompetitorBrief(), pruned to 1 200 chars
SEO keywordsselectBlogKeywords() — short-tail + long-tail
Competitor inspirationattached posts from the queue item
CMS base URLfrom 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.ts applies 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 (default openai,gemini,nvidia) only drives lib/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_PROVIDER does 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 structurallyevidence, 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

ConditionBehaviour
Structural quality issues after repair502, retriable — no draft saved
SEO placement issuesDraft saves; issues surface in content_warnings
ZeroGPT unavailableFails open, warning recorded
Humanization degrades qualityReverted, original retained, warning recorded
Compliance pipeline exhausts attemptsDraft 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 ① Creative line is Static — I · GIF — I · Video — E. Today the agent writes text; static, GIF and video are all unbuilt. blogs/drafts/[draftId]/thumbnail is 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. Vernac appears 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.
Source: roadmap/marketing-os/agents/creative-agent/technical.md