> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hiveku.com/llms.txt
> Use this file to discover all available pages before exploring further.

# The Content Department

> How the Content department grounds every draft on a customer avatar, a journey stage, a before/after promise and one target keyword, enforces the brand guide, links to real published URLs, states the pre-publish check, and how to pick a long turn back up when the MCP bridge times out

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:

| File                              | What the draft takes from it                                                                                                                                                    | Fallback tool            |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| `data/customer_avatars.json`      | The persona the piece is written for: `pain_points`, `frustrations`, `motivations`, `buying_behavior.objections`, `content_preferences`, `typical_quote`                        | `customer_avatar_list`   |
| `data/customer_journey_maps.json` | The stages of that persona's journey; the piece serves one stage, named the way the map names it                                                                                | `customer_journey_list`  |
| `data/before_after_grids.json`    | The transformation proof per persona (Have, Feel, Average day, Status; before and after); the piece promises one grid item                                                      | `before_after_grid_list` |
| `data/brand.json`                 | `brand_voice`, `brand_is` / `brand_is_not`, `cta_primary` / `cta_secondary`, `copy_dos` / `copy_donts`, `copy_reading_level`, `ai_preferred_phrases` and `ai_forbidden_phrases` | `brand_guide_get_active` |
| `data/content_items.json`         | Every existing item with its slug, status, type, excerpt, meta and live `url`: the source of internal links and the duplicate check                                             | `content_list`           |
| `data/knowledge_index.json`       | The knowledge bases the department can search for proof                                                                                                                         | `knowledge_search_query` |

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](#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:

```
For: <avatar> | Stage: <journey stage> | Promise: <before/after item> | Keyword: <target keyword> | Links: <n internal links>
```

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](/marketing/content-seo-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](/marketing/content-seo-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.

## Internal links from real URLs

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.

```json theme={null}
{
  "data": [
    { "id": "a1b2c3d4-...", "title": "Why CNC uptime drops in year three", "url": "https://www.example.com/blog/cnc-uptime-year-three", "type": "post", "published_at": "2026-08-30T14:02:11.000Z", "slug": "cnc-uptime-year-three", "source": "content_item" },
    { "id": "page:/services", "title": "Services", "url": "https://www.example.com/services", "type": "page", "published_at": null, "slug": "services", "source": "page_list" }
  ],
  "total": 2,
  "capped": false,
  "project": { "id": "f7a0ec1d-...", "name": "Example site", "host": "https://www.example.com" },
  "posts": { "listed": 1, "without_url": 0 },
  "pages": { "listed": 1 },
  "notes": []
}
```

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

<Note>
  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.
</Note>

## 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](/marketing/content-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:

<Steps>
  <Step title="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.
  </Step>

  <Step title="Draft and refine">
    `content_update` with the grounded draft, the meta fields and the featured image.
  </Step>

  <Step title="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](/integrations/webflow/content)).
  </Step>

  <Step title="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](/marketing/content-calendar).
  </Step>
</Steps>

## 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:

```json theme={null}
{
  "turn_id": "9c1e...",
  "session_id": "4d7a...",
  "status": "running",
  "domain": "content",
  "user_message": "Draft a consideration-stage post for the plant manager avatar",
  "started_at": "2026-09-11T15:02:10.000Z",
  "finished_at": null,
  "last_event_at": "2026-09-11T15:04:41.000Z",
  "stale": false,
  "error_message": null,
  "stop_reason": null,
  "num_turns": null,
  "response": "For: Plant manager | Stage: Consideration | ...",
  "tool_calls": [{ "seq": 3, "tool_call_id": "toolu_01...", "name": "content_create", "input_summary": "{\"title\":...}", "ok": true, "result_preview": "{\"data\":{\"id\":..." }],
  "data_updates": [{ "seq": 5, "entity": "content_item", "action": "created", "id": "a1b2..." }],
  "since_seq": 0,
  "last_seq": 6,
  "event_count": 6,
  "events_available": true,
  "events_truncated": false
}
```

`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.

<Warning>
  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.
</Warning>

## Related pages

* [Pre-Publish SEO Check](/marketing/content-seo-check) — every check, its level and the field it names, and the surfaces it runs on
* [Scheduling and the Content Calendar](/marketing/content-calendar) — the scheduler that executes, and the month view of what will publish
* [Use the Marketing Content Review Workflow](/how-tos/content-review-workflow) — the dashboard side: drafts, reviews, approvals and the editor
* [Customer Journey](/how-tos/customer-journey), [Before/After Grid](/how-tos/before-after-grid) and [Brand Guide](/how-tos/brand-guide) — the foundation the department reads first
* [Webflow as the Content Source](/integrations/webflow/content) — the same publish path on a Webflow-hosted site
