Content generation

The topic-selection, grounding, and dedup pipeline behind every generated draft.

GenerationService.generate(configId) is the single entry point every draft goes through, whether it was triggered manually from Content Studio or fired by a config's recurring schedule.

The steps

Pick a topic

topic-selector.ts is a pure function choosing between rotate, weighted, or random strategies over the config's assigned topics. The weight it works from isn't just what you set manually. See The loop for how real engagement multiplies it.

Optionally pull live news

If the chosen topic is flagged for live news, the news module calls OpenRouter's web-search plugin, scoped by the config's news keywords if any are set.

Assemble the prompt

context-composer.ts's composeGenerationContext calls four independent providers and merges their output: brand voice (from the account's knowledge profile), anti-repeat history (recent posts to avoid echoing), the news pulled above, and the account's own performance summary (a bounded aggregate read straight off SocialAccount.performanceSummary, no live elizaOS query needed on the hot path).

Generate and validate

The assembled prompt goes to GatewayService.complete() (the single point every LLM call in the system passes through, see below), and the result is checked against two things: a Postgres pg_trgm similarity check plus a SHA-256 exact-match check for duplicates, and a 280-character weighted length check via X's own twitter-text parser. A failure on either retries the whole generation, up to maxRegen + 1 times.

Persist or auto-publish

A draft that passes is saved as pending_approval. If the config has requireApproval: false ("autopilot"), it's enqueued for publishing immediately instead. If that enqueue call itself throws, the post is marked failed rather than left silently stranded in an ambiguous state.

The gateway is the only way out to the model

OpenRouterClient is the sole egress point to the LLM provider: a deliberate LiteLLM-gateway- style pattern. GatewayService.complete() wraps every call with a budget gate (billing.ensureBudget(), see Cost & Credits), the call itself, a cost record, and a credit debit. Nothing else in the codebase is allowed to call OpenRouter directly.

Reply generation is the same shape, deliberately smaller

ReplyGenerationService mirrors this pipeline for Reply Manage (brand-voice grounding, gateway call, length check) but skips topic selection, news, and dedup-against-corpus entirely. A reply doesn't need a topic to rotate through or a news hook; it needs to sound like the account and fit the platform's length limit, and that's what it checks.

On this page