Skip to main content
The Content department is the writer behind three surfaces: the AI chat on a content item under Marketing > Content, talk_to_department({ domain: "content" }) from an MCP client, and the Content plays in the Claude Code plugin and the VS Code extension. All three run the same department, so a draft is grounded the same way whichever one asked for it.
New to this? Follow Have the AI Write a Blog Post, a step-by-step walkthrough with screenshots. To give the department a customer to write for, follow Create an Ideal Customer Profile.
Until September 2026 that grounding was doctrine the plugin taught and nothing enforced: the department could write a blog post without opening the account’s avatars, name no journey stage, invent an internal link, and publish a body with two H1s and no meta description. This page describes what the department now does before, during and after a draft, and the contract each behaviour rests on.

Foundation first

Before any outline, draft or rewrite, the department reads the account’s foundation from its hydrated data files, and falls back to the matching list tool when a file is missing: Two of those rows changed in this release. The brand file used to omit the copy guardrails, so the department could not see ai_forbidden_phrases, copy_dos or copy_donts even when the brand guide had them; it now carries them, along with ai_preferred_phrases, ai_brand_adjectives, ai_prompt_rules and copy_reading_level. The content file used to carry a slug and no address; every item now carries url (see Internal links from real URLs). With the foundation read, four rules shape the draft:
  • No usable avatar, no draft. An avatar needs a name, pain_points, motivations and buying_behavior.objections. When none has them, the department says which fields are missing, points at the Customer Avatars page (/dashboard/marketing/customer-avatar) to fill them in, and offers to draft for the closest active avatar only when one is usable. Placeholder names, empty pain points and a “small business owners” persona with nothing specific behind it are reported, not used.
  • The avatar’s fields are the outline, not the industry. Pain points and frustrations become the angle and the H2s, motivations become the payoff, objections become the sections that answer them, and the typical quote can open the piece.
  • The stage decides the job. An awareness-stage reader gets the problem named and an explanation worth saving; a consideration-stage reader gets the comparison and the proof; a decision-stage reader gets the offer, the objection handling and the booking call to action.
  • The grid item supplies the promise. The draft writes the before the customer described and the after they got. It never invents a before the customer did not say.
Every draft opens with one header line, in prose before the body, so a reviewer can see the grounding without reading the piece:
The first four values in that line are the ones the department records on the item as avatar_id, journey_id with journey_stage, before_after_grid_id and target_keyword, through the same content_create and content_update calls that carry the copy (see Who this is for below). The keyword lives in the item’s target_keyword column, mirrored to settings.target_keyword while older readers move over, and is the same value the marketing editor’s Target keyword field reads and writes, the pre-publish check checks placement against, and publishing enrols in rank tracking. Sources counts the claims the draft cites from the research run and the knowledge bases, and the Channels: line reads the item’s distribution plan back from the row (Channels: none planned when it has none); both are described under After the draft. The three lines added on 2026-09-12 read from the row too: By: is the item’s author_id resolved to a name (else the account’s default author), Intent: and Type: are the search_intent and page_type the SERP brief stamped, Thesis: comes from the brand positioning or the piece’s own claim, Hook: is the opener’s pattern persisted as settings.hook_pattern, and Cluster: reads topic_cluster_id, cluster_role and page_role (Cluster: none when the item has no cluster). See Before the outline.

Who this is for: the grounding on the row

Until this round a content item had nowhere to say who it was written for. The department could name an avatar in its reply, the plugin could put one in a tag, and the editor’s AI assistant asked for the audience again in every chat, because none of that reached the row. Five typed columns on the content item now hold it, and every writer and every reader uses the same five: The three ids are checked against the account before anything is written. An id from another account, or a malformed one, is refused with 400 and code: "invalid_reference" naming the field, and nothing on that request is saved, not the body it carried and not the other ids. The keyword goes to the target_keyword column and is mirrored into settings.target_keyword; a row written before 2026-09-12 carries the keyword only in settings, and every reader falls back to it, so no keyword an agent recorded before the column existed is lost. Every read returns the five plus the names behind the ids, so a writer, the editor and the department need one read to say who a piece is for:
Every key is present and null when unset; the three relation objects are null when the id is. The same shape comes back from GET and POST /api/olympus/marketing/content, GET and PATCH /api/olympus/marketing/content/:contentId, the dashboard’s POST /api/marketing/content and GET / PUT /api/marketing/content/{id}, the content_list, content_get, content_create and content_update MCP tools, and each item in the department’s data/content_items.json. On the dashboard PUT, which echoes the whole row on every autosave, a keyword typed into settings.target_keyword wins over the echoed top-level value so a keyword typed seconds earlier is never overwritten; on the Olympus routes the declared target_keyword parameter wins.

In the editor

The marketing editor’s sidebar carries a Who this is for panel below Status: an Avatar list, a Journey list with a Stage list once a journey is chosen (Any stage leaves the stage empty; changing the journey clears the stage, because a stage name belongs to one journey), a Before/after grid list and a Target keyword field. A summary line above the controls reads the way the department’s header does, For Marcus, at Problem Aware in Trade contractor journey. Promise: Harlow Street refit. Keyword: sand between coats., and says Nothing chosen yet. The assistant and the pre-publish check use what you set here. until something is. Clear empties all four. The lists come from the account’s avatars, journeys and grids; an account with none is offered the page to create one, and a stage an agent recorded that is not on the journey’s list stays selectable rather than being dropped on the next save. The Target keyword field on the SEO tab and the one in the panel are one value. The New content modal offers Who is this for (optional), avatar and journey stage, when the account has avatars or journeys, so a new row starts with its audience; the grid and the keyword are chosen in the editor once there is a draft to attach them to. The panel saves with the row. When the row that comes back does not carry the choice that was sent, the editor shows what the row holds and says so once; a 400 invalid_reference is reported the same way. The AI assistant on the item receives the five fields and the names as its context, so a draft written in the editor is grounded on the row without re-picking the audience in every chat.

On the API and in the list

The Olympus list filters on the grounding, so “content for avatar X at stage Y” is one call and the avatar-by-stage coverage matrix is one call per cell:
avatar_id, journey_id and before_after_grid_id match exactly (a value that is not a UUID is a 400, not an empty page); journey_stage is a case-insensitive exact match on the stage name. The content_list MCP tool takes the same four. A row with avatar_id null is a piece written for nobody, and the department’s data file counts them (3 of 12 name an avatar) rather than guessing an audience from a title.

Every writer, the same five

  • The Content department records the avatar, journey and stage, grid and keyword it drafted for on the row through content_create or content_update, and its header line is read back from the row (customer_avatar.name, journey_stage, before_after_grid.name, target_keyword), never re-typed from the brief. Its data/content_items.json carries the five and the three names per item.
  • The Create Marketing Content workflow node takes the five as template-resolvable fields and forwards them under the same names; an unset field is not sent. When none of the five is set and the account has at least one avatar, the step still completes and records a warning naming the missing fields on the run log, so a draft written for nobody is visible in the run inspector. The shipped weekly trending-topic blog draft template picks one avatar, one stage and one keyword before the content agent writes, and records them on the row.
  • The Claude Code plugin and the VS Code extension write the five with the draft and treat an echo that does not carry them as a write that did not land; the free-text settings keys and persona: / stage: tags earlier versions used are not read by anything any more.
  • The content calendar shows when a grounded piece publishes; the coverage matrix built from the list filters says whether the calendar covers every avatar and stage. See Scheduling and the Content Calendar.

Before the outline: the brief, the author and the thesis

Since 2026-09-12 four things happen between the foundation read and the outline, each on a contract described on Authors, the SERP Brief, Positioning and Titles:
  • No SERP brief, no draft. The department calls content_brief_build(content_id) (one live SERP read against the monthly research cap, the top three pages parsed) before the outline, and reads Intent: and Type: back from the row. The page type sets the skeleton (a guide, a listicle, a comparison, a landing page, a tool, a news piece or a how-to), the outline covers what_the_top_3_all_say and lands inside the word-count band, the people-also-ask questions become the FAQ, and the thesis is the sentence that disagrees with the consensus. A brief that exists is read with content_brief_get, never rebuilt. A draft written before the brief is a draft written to a guess, and the department says so in the same shape it refuses a draft with no usable avatar. When the research cap is spent (402 seo_research_limit) it says that too, and waits.
  • A named author, or none is invented. The By: line names the item’s author, else the account default. When the account has no authors the department says so and offers to create one (content_authors_create) before drafting. It always sets author_id (or checks the default exists) before content_create, because the check refuses an anonymous post.
  • The answer and the questions go on the row. The 40 to 60 word direct answer under the H1 is written into answer_block and as the first paragraph of the body; the three to six real questions the brief and the proof pack surfaced go into faq, each answered in one to three sentences, never padded. The FAQPage markup is generated from that field on publish.
  • A thesis and a hook. Thesis: is required: one sentence the piece argues, from positioning.thesis or one of its beliefs, else the piece’s own claim; a draft without one is not a draft. Hook: is the opener’s pattern, one of the sixteen hook slugs, written to settings.hook_pattern through content_update. After the row exists the department calls content_titles_generate(content_id, count: 5), presents the five with their pattern and rationale, and content_titles_pick(content_id, index) for the one the user chooses (default: the candidate whose pattern it used for the opener). It never writes title_candidates by hand.
  • The cluster and the keyword. At brief time the department sets topic_cluster_id and cluster_role on content_create, takes the keyword from the cluster’s missing_subtopics when the brief names none, and reads the keyword map first: a keyword the map lists as a collision is refused unless the owner says consolidate. A keyword_already_targeted warning on the create or update response is relayed with the other item named, and never retried around.
One rule holds the row honest: the department never writes serp_brief, serp_brief_captured_at, decay_status, refresh_priority, top_declining_keywords or refreshed_at through content_update. The brief route writes the first two, the Sunday decay run the next three, the publish path the last; a value sent for any of them is 400 read_only_field, and the tool reports it as such rather than retrying.

Brand voice is enforced, not assumed

A draft is checked against the brand guide before it is persisted: brand_is words present, brand_is_not words absent, copy_dos followed, copy_donts avoided, copy_reading_level respected. Any phrase from ai_forbidden_phrases in the title, excerpt, meta or body is a hard fail, and so is any phrase on the department’s own banned list:
elevate, unlock, unleash, seamless, game-changer, game-changing, cutting-edge, best-in-class, world-class, in today’s fast-paced world, in today’s digital age, ever-evolving, navigate the landscape, delve, dive in, let’s dive in, leverage, empower, supercharge, revolutionize, robust, holistic, synergy, at the end of the day, excited to announce, thrilled to share, your journey, it’s not just X it’s Y, here’s the thing, ready to level up, pro tip, discover how, look no further, in conclusion
The department never persists a draft under a hard fail. It rewrites once; if the rewrite still fails it says exactly what fails and stops. The same forbidden phrases are an error in the pre-publish check, so a phrase that slips past the department is still caught in the editor and on the API. Long-form drafts also run through a four-rule rubric before content_create or content_update, and a draft that fails is rewritten once, then reported:
  1. Specific claims with a source. Every number, result or quote points at a grid item, a knowledge-base document, a tool result or a URL the department fetched. No source, no claim.
  2. No filler openers. The first sentence carries the persona’s pain or the payoff; no rhetorical question the next line answers.
  3. One idea per paragraph, one job per section, under a heading that says what the section proves.
  4. A call to action matched to the stage, from the plan. The piece’s stage row in the conversion plan (content_conversion_plan, read per piece so the names are the account’s own journey stage names) says the job, the allowed hooks, the proof type to place beside the ask, and the CTA verb and destination. The department writes the placements only, never the CTA copy: ::cta{variant=inline} after the second section, ::cta{variant=end} after the last, ::upgrade when the piece has a lead magnet, each on a line of its own, so a changed offer reaches every published piece. When the plan warns lead_magnet_missing it proposes one in the brief (the checklist from the checklist post, the template from the how-to) and sets it with content_update({ lead_magnet }) before drafting; when it warns offer_missing on a decision piece it asks the owner for one through brand_offers_set before publishing. The three buckets this rule used to name (awareness subscribes, consideration downloads, decision buys) are retired. See Conversion inside the piece.
  5. One sentence a competitor would not publish. The body carries at least one belief from the positioning, or a stated disagreement with what the top three results all say (the brief’s consensus line is the thing to disagree with).
The last test is the competitor swap: replace the brand name with a competitor’s. If the piece still reads true, it has no specificity, and it is rewritten with the detail only this account owns. The department links at least two existing published pieces from every draft, by their live URL, with anchor text that names what the reader gets there. When fewer than two published pieces exist it writes Links: 0 or Links: 1 in the header and says so rather than inventing links to make the count. Since 2026-09-12 the links are chosen for the site’s architecture, not only for the count. Every draft links a money page (a consideration or decision piece must, or the check errors money_link_missing) and, when the item is a cluster spoke, its pillar (pillar_link_missing otherwise), using the suggested_anchor the link list returns, and the department names the money page and the pillar it linked in its publish summary. content_site_links({ project_id, content_id }) returns the rows in that order: the money pages, the item’s pillar (marked is_pillar_for_item), the other pillars, then the rest; a pillar item gets no pillar row, because it links down to its spokes. See Links that build the architecture. The URLs come from data, never from guesswork. Three reads return them:
  • content_list and content_get now carry url on every row: the live URL of a published, site-linked item, or null. content_get adds url_source, which names the rule that produced the address: published_route (stamped by publish-to-site from the collection’s route pattern and the entry’s real fields), route_pattern (the collection’s pattern resolved with the entry slug alone) or page_list (the route the project’s own page list gives the entry’s content file). A null means the row cannot resolve its route.
  • data/content_items.json carries the same url per item, plus website_project_id, cms_collection_id, cms_entry_slug and published_route, so the department can link without a tool call.
  • GET /api/olympus/marketing/content/site-links?project_id=<uuid>&content_id=<uuid> (the content_site_links MCP tool) lists every published post and page on one website project with its URL, money pages first and the item’s pillar next: the list a writer picks link targets from.
type is post for content items and page for page and landing_page items and for routes from the page list or the crawled index (source is content_item, page_list or crawl); role is the stored page role, suggested_anchor the target’s keyword or its title without the brand suffix, and is_pillar_for_item marks the pillar of the item named by content_id (which is itself never listed). limit defaults to and caps at 500; capped says when the cap cut the list. A missing or non-UUID project_id is 400 project_id_required; a project outside the account is 404 project_not_found. Every omission is explained in notes rather than hidden: a project with no production host returns an empty list and says to deploy it or add a domain, and a published item whose route cannot be derived is counted in posts.without_url with the fix named (set routePattern on the collection, or republish the item).
A URL is derived only for a row whose status is published, on a project with a production host (the active primary production domain, else the project’s custom domain, else its deployed production tier), from a route that can be resolved. Anything else is null. A null is honest; a guessed URL is a broken link on a live page.

Images for the piece

Until 2026-09-12 a content item could carry one featured image and nothing inline, and the weekly blog workflow had no image step at all. A long-form piece now gets its pictures as one step, run after the draft is on the row and before the pre-publish check: a hero and one image under each major section where a picture adds information the words do not (the thing itself, the place, the process, the comparison, the before and the after), never decoration and never a stock cliche. Three surfaces run the same pipeline:
  • The editor: Generate images on a content item under Marketing > Content opens with what a run would do (which H2s have no image yet, whether the item has a featured image) and how many generations are left this month, then applies the result in place.
  • The Content department and the plugin: the department illustrates a draft it has just written, and the Claude Code plugin and the VS Code extension call content_images_generate after the draft is stored and before the quality gate.
  • The API: POST /api/olympus/marketing/content/{id}/images under an API key, and POST /api/marketing/content/{id}/images from the dashboard; GET on either is the free preview. See Article Images.
  • A workflow: the Create Marketing Content node’s Generate images switch, described below.
What one run does, in order:
  1. Plans the set from the stored body (never from a chat transcript), the title and excerpt, the row’s grounding (avatar, journey stage, target keyword and the before/after transformation) and the account’s brand image profile. The plan is one hero when the item has no featured image, plus at most one image per H2 that does not already carry an image directly below it, and only where a picture earns its place; the planner skips a section rather than pads it. Each planned image has a prompt that names what is in the picture in concrete terms (no words, letters or logos inside the image) and an alt text: one plain sentence saying what the image shows, for a reader who cannot see it, with no “image of” and no keyword list.
  2. Renders the hero first, at 1600 x 900, with the brand references attached; then every section image with the brand references and the hero as reference images, told to match the hero’s palette, lighting and level of realism without repeating its subject, so the set reads as one.
  3. Registers each image in the Media Library with its alt text, prompt and model, under the tags content_image and content:<item id>, so a post’s images are one filter away.
  4. Writes each section image directly under its heading in the body (markdown or HTML, whichever the item is) and sets the item’s featured_image_url and featured_image_alt when a hero was made. Insertion is idempotent: a heading that already has an image is left alone, and the response says so.
The allowance is read first and the run is metered as it goes. Each image is one generation from the account’s monthly image allowance; a run asks for at most eight images (four by default, hero included), refuses with 402 quota_exhausted and the remaining count before anything is planned when nothing is left, and a run that hits the allowance part-way returns the images it made plus a warning naming the ones it did not. A render that fails is refunded and the run moves on: the hero failing costs the sections their style reference, not their existence. An assistant quotes the allowance and confirms the count before spending; the plugin’s play does the same. The alt text is mandatory and it is checked. Every image the run places has alt text on it, the featured image gets featured_image_alt, and the pre-publish check flags inline_image_alt_missing and hero_alt_missing as errors, so an image that lost its alt text on a later edit is caught before the piece ships. The department reads the alt text back and rewrites any that reads like a filename. Brand honesty holds here as on every image surface: brand_applied is false when the account has no active brand guide (the images are made unbranded, and the response says so), brand_reference_ids lists which brand references reached the model, and a brand guide that cannot be read refuses the run before anything is spent. In a workflow, the Create Marketing Content node carries a Generate images switch (off by default, because images spend the allowance) and a Max images cap. When it is on, the node saves the item, verifies the row, then runs this step once; the count and any warning land on the step’s output.images (requested, generated, max_images, warning, error_code, data) and on the run log, so a notification email can say how many pictures the draft arrived with. The step never fails because of images: an exhausted allowance or a route error is a warning, the draft exists, and the images can be generated from the editor later. The shipped Weekly Trending Topic Blog Draft template turns the switch on with a cap of four and reports the count in its heads-up email.

After the draft: distribution, proof and research

Three more contracts travel with the piece since 2026-09-12, each described in full on Distribution, Performance, Proof and Research:
  • The distribution plan is written with the brief. When the department creates a piece it writes settings.distribution_plan in the same content_create or content_update call as the copy: the default plan for the item’s stage and type (owned email first, then the social, community, outreach and paid channels the stage calls for, paid winner-only until the scorecard says the piece earned it), edited to the brief. The Channels: line in its header reads the plan back from the row. The social and email departments mark a row drafted or done with the derivative id once they persist a post or a campaign. When a piece is published with no plan or an empty channel list, the department says no distribution plan before it treats the publish as done; it never blocks the publish. See Distribution is part of the asset.
  • Proof comes from the account, with a citation. For a consideration or decision piece the department reads the proof pack (content_proof_pack with the avatar and the stage): public testimonials, review text, the first message of support tickets in the segment, call summaries, survey answers, measured before/after grid results and the objection library, each entry with a source, a ready-made [source: <label>:<id>] citation and a consent flag. An entry with consent is quoted verbatim with its attribution; one without is paraphrased as the customer’s words, never named. The citation goes beside the figure, because the pre-publish check treats a figure with no source as an error at the decision stage and expects a proof element under every H2 of a consideration or decision piece. A case study is drafted from a won deal with content_case_study_draft, the quote verbatim and the numbers from the grid. See Proof and case studies.
  • Search before you write. For each H2 it plans, the department runs knowledge_search_query across every knowledge base in the account (brand guides, service menus, earlier research). When the pinned item has no settings.research, or the stamp is older than 30 days, it runs content_research_run(content_id) and reads content_research_get before the outline. Every number, quote or third-party fact in the draft cites a claim’s source_url inline, or names the knowledge base for a passage that came from one (per our service menu); a page it relied on that is not in the run’s sources[] goes into the Content research knowledge base with its URL, so the next writer finds it. The header’s Sources: <n> counts those citations. See Research with the knowledge base.
  • What the piece did is on the row. content_analytics_get returns the piece’s scorecard (views, form submits, leads, contacts, deals, rank, the linked social posts, the lead rate), and the department reads it before it picks the next stage and angle to write, and before it moves a paid row past planned. See The scorecard.
  • After the publish, the response is read. Every publish_warnings[] line from content_publish_to_site is relayed one per line like the SEO warnings[] (the Webflow no custom code field line especially), data.author being null on a collection with an author field is mentioned, and a 422 author_entry_failed is answered by relaying its details.warnings[], not by asking for another author. refreshed: true means the piece republished on its URL it already held; llms_txt_regenerated: true that the site’s llms.txt was rewritten.

The pre-publish check

Before content_publish_to_site (and before manage_cms_write_entry on a site-only entry) the department states the pre-publish list in its reply, with a pass or fail per item, and fixes every fail first:
  • meta_title is 60 characters or fewer and carries the keyword
  • meta_description is 160 characters or fewer
  • the body has at most one H1 and the title or H1 carries the target keyword
  • the keyword appears in the title, the slug and the first 100 words
  • the hero image has alt text (featured_image_alt) and every inline image has alt text
  • at least two internal links to published pieces, none invented
  • no phrase from ai_forbidden_phrases or the banned list
  • every figure has a source in its paragraph (a link or a [source: ...] citation); at the decision stage a figure without one is an error, and a consideration or decision piece carries a proof element under every H2
  • a distribution plan with at least one channel, or the reply says no distribution plan before the publish is treated as done
  • the item has an author or the account has a default; the answer block is 40 to 60 words; faq matches the FAQ section in the body
  • the title carries a number, a name, a claim or a contrast
  • a link to a money page on a consideration or decision piece, and to the cluster’s pillar on a spoke
  • no other item in the account targets the keyword
  • on a comparison or alternatives page, a table whose every rival row carries a source and a checked date, and one decision call to action
  • a stage-matched ::cta{variant=end} on a line of its own (and an inline one in a long piece), placed by the conversion plan
The publish route then runs the same check on the stored row and hands the findings back as warnings[] and a structured seo_check, so a department that skipped the list is still told. The check never blocks a publish. What it checks, how it scores, and every surface it runs on are on Pre-Publish SEO Check.

From draft to the live site

A blog or article meant for the customer’s site is one content item with its CMS link on the row, not two separate writes:
1

Create the item with its destination

content_create(..., website_project_id, cms_collection_id, target_keyword, avatar_id, journey_id, journey_stage, before_after_grid_id, author_id, topic_cluster_id, cluster_role, page_role). The CMS link travels with the item, the grounding says who it is for, the author is the byline, the cluster fields place it in the site, and the keyword is what closes the measurement loop at publish time. Every content_create and content_update parameter the routes read is declared on the tools, so featured_image_alt, meta_keywords, canonical_url, settings, the CMS link and the five grounding fields no longer fall off between the department and the row.
2

Build the brief

content_brief_build(content_id): the SERP read, the top three parsed, the intent and page-type verdicts stamped on the row. The outline is written to the brief, and Intent: and Type: are read back from the row (see Before the outline).
3

Research and gather proof

content_research_get(content_id), and content_research_run(content_id) when the item has no research or its stamp is older than 30 days; content_proof_pack with the avatar and the stage for a consideration or decision piece. Every figure in the draft takes its source from one of the two (see After the draft).
4

Draft and refine

content_update with the grounded draft, the meta fields, the citations beside each figure, the answer block and the FAQ on the row, settings.hook_pattern, and the distribution plan under settings.distribution_plan.
5

Title it and place the calls to action

content_titles_generate(content_id) then content_titles_pick(content_id, index) for the one chosen. content_conversion_plan(content_id) for the stage row; the ::cta{variant=inline}, ::cta{variant=end} and ::upgrade lines go into the body, the lead magnet onto the row when the plan asks for one, and settings.cta_stage_table is written from the stage table so the check and the draft agree.
6

Illustrate

content_images_generate(content_id) once the draft is on the row: the hero and the section images, alt text on each, metered against the image allowance (see Images for the piece). Then content_seo_check before publishing.
7

Publish to the site

content_publish_to_site(content_id) maps the fields to the collection schema, validates, saves the versioned entry file, resolves the live route and enrols the target keyword in rank tracking. On a Hiveku-hosted site the entry lands in the working tree and goes live on the next deploy; on a Webflow-backed project the same call stages the item in Webflow and it goes live with an item publish or the next site publish (see Webflow as the Content Source).
8

Or schedule it

A linked item can be timed instead: the schedule lands on the CMS scheduler that runs, and an item not linked to a collection cannot be scheduled. See Scheduling and the Content Calendar.

Before the next piece: refresh, prune, and the bottom-funnel play

The department no longer proposes a net-new topic first. Before it does, it reads the refresh queue (content_list ordered by refresh_priority, the items whose decay_status is decaying) and the prune list (content_prune_candidates): a decaying page on the keyword is a refresh, never a new page, and a zero-traffic page on the keyword is consolidate-or-prune first. The refresh play runs from content_refresh_brief_get(content_id): re-read the SERP brief (capture one when missing), cover the declining keywords, close the outline gaps against the top three, add links from the newer donors the brief lists, publish a new version on the same URL (a version, then content_publish_to_site), re-run content_seo_check, and record the decision as review_disposition through content_update (double_down, refresh, rewrite, consolidate or prune). refreshed_at on the row is the proof the refresh shipped. See The decision loop. The bottom-funnel play, for the pages people read when they are choosing: content_bofu_plan({ project_id }) without seeding first, the candidates shown with their volume, the rival’s pricing URL and the proof counts and the dropped list with its reasons (a keyword_collision drop names the item to refresh instead), then the same call with seed_drafts: true for the ones the owner approves. For each seeded draft the rival facts come from the competitor analysis and from reading the URLs in settings.bofu.sources, every table row carries its source and checked_at, the account’s own column comes from the proof pack, the body follows the seeded template with its honest-concession section, and settings.decision_cta names one decision and the offer or URL it goes to. Never a rival price or feature from memory; the check refuses the row. See Bottom-funnel pages.

Resuming a department turn

talk_to_department waits about 110 seconds for a reply, while a department turn may legitimately run for up to 1200 seconds. A long drafting turn therefore times out at the bridge while the department is still working. The timed-out result is an error, but it now carries session_id, turn_id and whatever had already arrived (response, tool_calls, data_updates), and error states the exact next step. Two follow-ups exist. Poll the turn. GET /api/olympus/marketing/ai/turns/:turnId (the department_turn_get MCP tool) reads the department’s turn log under the same API key, scoped to the key’s account, and folds it into the shape a finished talk_to_department returns:
status is running, completed, errored or cancelled. stale is true when a running turn has produced no event for five minutes, which is a dead worker rather than a slow one. Pass since_seq=<last_seq> to read only what arrived since the last poll, and events=1 to include the raw { seq, event_type, event_data } rows. A turn keeps its events for 30 days; an older finished turn still answers with its status and timestamps, but response is empty and events_available is false, because the transcript itself is the chat session, not this route. A turn id outside the account is a 404. Resume the conversation. Pass the session_id back into talk_to_department and the department continues with its full prior context. When the timeout happened before any response headers arrived, turn_id is null and the error says so: a turn may or may not have started upstream, so re-read the records the ask would touch, wait 30 seconds, then retry or break the request into a smaller ask. A session_id the caller passed in is still returned on that path, so a resumed conversation never looks as lost as a fresh one.
Check tool_failures before relaying a finished turn’s answer. It is present only when a tool the department ran actually failed, and it names each one with the head of its error. A department can write a confident paragraph over a content_update that answered 401 underneath it; the prose is not evidence the work landed, and this field is.