Skip to main content

blog.rewrite_for_audience

blog.rewrite_for_audience translates a source document (markdown) into an original blog post for a stated audience and angle. It is not a summarizer — it leads with why-it-matters, de-jargons, connects to the audience's tools, and adds perspective, while staying grounded in source_content (no claims that aren't in the source). It is the generator pack at the heart of every *-rewrite-blog pipeline. It does not fetch sources (call doc.parse / web.scrape / research.deep first), does not insert inline citations (chain content.ground with rewrite:false after), and does not publish to a CMS (chain blog.publish to save the artifact).

Inputs

FieldTypeRequiredDefaultNotes
source_contentstringyesThe source markdown to rewrite.
audiencestringyesWho the post is for (e.g. "platform engineers").
modelstringnoresolves via defaultPackModel()A routable provider/model id.
anglestringnoThe editorial angle / thesis.
titlestringnoSuggested title.
personastringnogeneralTone preset (technical, marketing, executive, educational, academic, or freeform).
max_tokensnumbernoderived from targetCap on the LLM call. When set, must be at least the target × 1.7 tokens/word; otherwise the pack raises it so the chosen target isn't silently truncated.
length_intentstringnothoroughJIT length sizing — one of summary / thorough / exhaustive. Pack measures source_content, picks a target word range from the heuristic table below. See #525 for the convention.
inspectbooleannofalseWhen true, pack returns measurements + suggestion and does NOT call the model. Useful when an agent wants to negotiate length before committing.
target_words_minnumbernoExplicit numeric override. Both min and max must be set (and max ≥ min) to be honored; partial falls through to length_intent.
target_words_maxnumbernoExplicit numeric override. See target_words_min.

Length intent heuristic

The pack picks a chosen target by multiplying source_words × ratio, then clamping to the row's floor/ceiling. The reported range brackets the chosen target ±15%, re-clamped to the row bounds.

IntentCompression ratioFloorCeiling
summary0.103001200
thorough (default)0.308002500
exhaustive0.5515006000

Precedence: inspect:true short-circuits everything > both target_words_min and target_words_max set > length_intent > default (thorough). The chosen range is injected into the system prompt as an explicit override of the persona's word-count guidance.

Outputs

FieldTypeNotes
markdownstringThe rewritten blog post. Empty when inspect:true.
modelstringThe model used (always populated; reflects the resolved default when caller omitted).
persona_usedstringThe persona that shaped the output.
source_wordsnumberWhitespace-delimited word count of source_content.
target_words_chosennumberMidpoint of the chosen range, what the model was told to aim for.
target_words_minnumberLower bound of the chosen range.
target_words_maxnumberUpper bound of the chosen range.
output_wordsnumberWhitespace-delimited word count of the produced markdown.
compression_rationumberoutput_words / source_words.
length_intent_appliedstringWhere the chosen size came from — intent:summary / intent:thorough / intent:exhaustive / explicit.
truncatedbooleantrue when the model hit max_tokens. Strong signal is finish_reason=length; fallback heuristic fires when output is within 95% of target_words_max AND ends without sentence-terminating punctuation. Re-run with a smaller length_intent or a larger max_tokens.
inspectbooleantrue when this was an inspect-mode call (no generation).
suggested_targetnumberInspect mode only — what the pack would aim for if called for real.
suggested_target_min / _maxnumberInspect mode only — bracket around suggested_target.
reasonstringInspect mode only — human-readable explanation of the suggestion.

Inspect mode worked example

curl -fsS -X POST http://localhost:3000/api/v1/packs/blog.rewrite_for_audience \
-H "Authorization: Bearer $JWT" \
-H 'Content-Type: application/json' \
-d '{
"source_content": "# Long source...\n\n[~7000 words]",
"audience": "platform engineers",
"inspect": true,
"length_intent": "exhaustive"
}'

Response (no model call, no token cost):

{
"inspect": true,
"markdown": "",
"model": "openrouter/auto",
"source_words": 7000,
"suggested_target": 3850,
"suggested_target_min": 3272,
"suggested_target_max": 4427,
"length_intent_applied": "intent:exhaustive",
"reason": "source is 7000 words; applying intent:exhaustive for a target near 3850 words (range 3272-4427)"
}

Vault credentials needed

  • None directly — requires a configured AI gateway (gateway-gated pack).

Use it from your agent (OpenClaw chat-UI worked example)

OpenClaw chat capture pending.

Developer reference (curl)

curl -fsS -X POST http://localhost:3000/api/v1/packs/blog.rewrite_for_audience \
-H "Authorization: Bearer $JWT" \
-H 'Content-Type: application/json' \
-d '{
"source_content": "# Raw notes...",
"audience": "platform engineers",
"angle": "why this matters for CI",
"model": "openrouter/auto",
"length_intent": "thorough"
}'

Error codes

CodeTriggers
invalid_inputsource_content or audience missing/empty.
internalPack registered without a gateway dispatcher AND the call is not inspect:true. Inspect mode works without a dispatcher.
handler_failedThe model returned no usable output.

Session chaining

  • No session. Stateless; chains via content (output markdowncontent.ground / blog.publish).

Async behavior

Async: true — one gateway LLM call; call through pack.start or a SEP-1686-aware SDK to avoid client timeouts.

See also

  • Catalog row: PACKS.md.
  • Source: internal/packs/builtin/blog_rewrite_for_audience.go.
  • Companion packs: content.ground, blog.publish, doc.parse, web.scrape, research.deep.
  • Pipelines: builtin.brief-rewrite-blog, builtin.scrape-rewrite-blog, builtin.doc-rewrite-blog, builtin.research-rewrite-blog.