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. 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 keyword in that line is the one the department passes to content_create or content_update as target_keyword. It is stored on the item as settings.target_keyword, which is the same value the marketing editor’s Target keyword field on the SEO tab reads and writes, the pre-publish check checks placement against, and publishing enrols in rank tracking.

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. Awareness earns a save or a subscribe, consideration earns a comparison or a checklist, decision asks for the booking or the purchase with the brand’s cta_primary.
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. 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> (the content_site_links MCP tool) lists every published post and page on one website project with its URL: 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). 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.

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
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). The CMS link travels with the item 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 and the CMS link no longer fall off between the department and the row.
2

Draft and refine

content_update with the grounded draft, the meta fields and the featured image.
3

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).
4

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.

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.