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

# Distribution, Performance, Proof and Research

> How a content item carries its distribution plan from brief time, fires a publish event the repurpose and digest automations run on, credits its derivatives through UTM keys, reports leads per piece in the list, the editor and the API, cites the account's own proof, is drafted as a case study from a won deal, and keeps its research in the Content research knowledge base

Until 2026-09-12 a published piece got one post and stopped, the content list showed page views and nothing else, a figure in a draft had no source to cite because the account's own testimonials, reviews, tickets and grid results lived behind six other modules, and the research a writer did stayed in the chat that did it. Four changes close those gaps, and each one is a contract the [Content department](/marketing/content-department), the editor, the API and the Claude Code plugin share.

## Distribution is part of the asset

### The distribution plan on the row

A content item carries its distribution plan from the moment it is briefed, under `settings.distribution_plan`:

```json theme={null}
{
  "owned_first": true,
  "channels": [
    { "channel": "email_digest", "format": "digest feature", "status": "planned", "scheduled_for": "2026-09-22T08:00:00.000Z" },
    { "channel": "social", "format": "thread and three posts over four weeks", "status": "drafted", "link": "social_post:8d2f..." },
    { "channel": "community", "format": "answer where the question is asked, linking the piece", "status": "planned" },
    { "channel": "outreach", "format": "cited sources and people named in the piece", "status": "planned" },
    { "channel": "paid", "format": "retargeting amplification", "status": "planned", "note": "Winner only: amplify once the scorecard shows the piece earned it", "scheduled_for": "2026-10-15T08:00:00.000Z" }
  ],
  "written_at": "2026-09-15T10:00:00.000Z"
}
```

| Key                        | Meaning                                                                                                                             |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `owned_first`              | Always `true`. The email digest row, when present, is the first channel: the owned list is the one surface no platform can throttle |
| `channels[].channel`       | `email_digest`, `social`, `community`, `partner`, `outreach` or `paid`; at most 12 rows                                             |
| `channels[].format`        | What the channel gets, in plain words                                                                                               |
| `channels[].status`        | `planned`, `drafted`, `scheduled`, `done` or `skipped`                                                                              |
| `channels[].scheduled_for` | When the derivative should go out; set only where a rule exists and the item has a publish date to anchor to, never guessed         |
| `channels[].link`          | The derivative once it exists: `social_post:<id>`, `campaign:<id>`, `task:<id>` or a URL                                            |
| `channels[].note`          | A rule the executor must respect, or what was decided                                                                               |

The channels follow the item's journey stage and content type: an awareness piece gets the widest plan, a decision piece leans on outreach and the digest, and a social post is itself a derivative and gets an empty plan rather than a plan to repurpose a repurpose. `paid` is always planned as winner-only, dated thirty days after publish, because nothing is amplified until the [scorecard](#the-scorecard-leads-per-piece) says the piece earned it.

The key is written through the same `settings` object every other writer uses (`content_create`, `content_update`, the Olympus `PATCH` and the dashboard `PUT` all merge top-level keys), so a plan never overwrites a sibling key and a sibling never overwrites the plan. A malformed plan reads as no plan, never as half a plan.

**The department writes the plan with the brief.** When the Content department creates a piece it writes `settings.distribution_plan` in the same call as the copy, starting from the default for the stage and type and edited to the brief, and its header line gains a `Channels:` line:

```
Channels: email digest (digest feature); social (thread and three posts over four weeks); community (answer where the question is asked, linking the piece); outreach (cited sources and people named in the piece); paid (retargeting amplification)
```

`Channels: none planned` is what it says when the row has no plan. The social and email departments mark a row `drafted` or `done` with the derivative id in `link` after they persist a post or a campaign, so the plan is also the record of what was actually made.

**A publish without a plan is said out loud.** When a piece is published with no plan or an empty `channels` list, the department states *no distribution plan* before it treats the publish as done, and the editor's pre-publish warnings gain *No distribution plan: the piece will get one post and stop*. Neither blocks the publish.

**In the editor**, the sidebar's **Distribution** section reads the plan and shows one row per channel with its format, a status chip (planned, drafted, scheduled, done, skipped), the date and the derivative as a link (a social post opens in the calendar, a campaign opens the campaign, a task opens the tasks page, a URL opens the URL). A row's status, date and link are editable in place and the whole plan is validated before it is saved, so a rejected plan shows the validator's message and writes nothing; an item with no plan offers **Add default plan**, which writes the default plan for the item's stage and type. A social post, whose default plan has no channels, gets neither the pre-publish note nor the action, and an unsaved item shows no section.

### The publish event

Every native publish of a content item fires one `content.published` event: the editor's **Publish**, `POST /api/olympus/marketing/content/{id}/publish-to-site` and the scheduled-publish cron all go through it. A workflow starts on it with the **Content Published** trigger (`contentPublishedTrigger`; `content_published_trigger` on the API), which carries:

| Key                                                                                          | Value                                                                                                                                               |
| -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `content_id`, `account_id`, `title`, `slug`, `excerpt`, `featured_image_url`, `content_type` | The item, with the slug the page serves at                                                                                                          |
| `url`                                                                                        | The live URL, or `null` until the site has a host and the entry a route                                                                             |
| `avatar_id`, `journey_stage`, `target_keyword`                                               | The grounding on the row                                                                                                                            |
| `project_id`, `provider`                                                                     | The website project and `native` or `webflow`                                                                                                       |
| `published_at`, `timestamp`                                                                  | When the publish happened and when the event fired                                                                                                  |
| `visibility`                                                                                 | `live`, `deploy_required` (a native entry that goes live on the next deploy) or `publish_required` (a Webflow item staged until the site publishes) |
| `is_first_publish`                                                                           | `false` on a re-publish of an edited item                                                                                                           |
| `publish_key`                                                                                | What makes the publish unique: one run per workflow, item and publish, so a re-fire of the same publish is deduplicated                             |

The trigger's node config filters on `project_id`, `content_types` (an array or a comma-separated string) and `first_publish_only`, and a filter value it cannot read fails closed. It fires once, at the entry write, and not again from the deploy record, because firing at both would run every repurpose workflow twice per post: a workflow that must act only on a live page filters on `visibility` or waits for the deploy trigger. A status flip to `published` on an item with no site binding fires nothing (there is no page to distribute), and a Webflow item published from the Designer fires the Webflow CMS trigger instead. `GET /api/olympus/workflows/meta/event-trigger-types` lists the trigger with its output keys and a `sample_output`, a representative payload with one value per key, so a workflow author can wire the template variables without waiting for a real publish.

### Auto-repurpose and the weekly digest

Two workflow templates ship the plan's first two channels as automations. Both stage to a draft or an approval; nothing is posted or sent by itself.

* **Content Published to Social Drafts** (`content-published-repurpose`). On the first publish of a piece, the social agent reads the source package (`social_repurpose_source`: the title, a plain-text excerpt, the section headers, candidate numbers, quotes and names to confirm, the hero as a Media Library asset and the UTM-stamped live link per platform), writes three posts in the brand voice across the platforms you name, saves each as a **draft** carrying `linked_content_id`, the platform link and a first comment, and emails the person who approves posts. A re-publish of an edited item does not re-draft. Variables: `PLATFORMS`, `RECIPIENT_EMAIL`, `PROJECT_ID`.
* **Weekly Content Digest Newsletter** (`content-digest-weekly`). Every Tuesday at 9 AM, lists everything published on the site in the last seven days (the Olympus content list with `status=published`, `published_since` and `sort=published`), has the email agent write the digest from the pieces' excerpts for the account persona, saves it as a **draft** newsletter campaign to the audience you name with every link stamped for crediting, waits for a named approver, and sends only after the approval. When nothing was published the run ends with a short email saying so and creates no campaign. Variables: `AUDIENCE_ID`, `FROM_EMAIL`, `APPROVER_EMAIL`, `DIGEST_NAME`, `TIMEZONE`.

Both are listed by `workflow_templates_list` and instantiated by `workflow_create_from_template`; see [Standing Playbooks](/marketing/standing-playbooks) for the mechanics. The two workflow nodes they use gained the keys they need: **Create Social Post** (`socialCreatePost`) forwards `link_url`, `first_comment` and `linked_content_id`, dropping a blank value rather than sending an empty string, and **List Marketing Content** (`marketingListContent`) accepts `published_since`, `published_within_days` (turned into `published_since` when the run starts) and `sort`.

### UTM crediting

A derivative's clicks credit the piece only when the link carries the keys the content attribution resolver reads: `utm_medium=content` and `utm_content=<the item's slug>`. Every link the source package returns now carries them:

```
https://www.example.com/blog/why-dentists-need-local-seo?utm_source=linkedin&utm_medium=content&utm_campaign=why-dentists-need-local-seo&utm_content=why-dentists-need-local-seo
```

`utm_source` is the platform (`linkedin`, `facebook`, `instagram`, `twitter`, `tiktok`, `google_business_profile`, and `newsletter` on the digest, whose `utm_campaign` is `content-digest`), `utm_campaign` is the slug the page serves at and `utm_content` is the `content_items` row's slug. The package also returns `utm_medium` and `utm_content` at the top level so a writer can build a link for a platform it does not list. Before this change the links said `utm_medium=social` and carried no `utm_content`, so a click on a repurposed post never reached the piece.

<Note>
  Use the link the source package returns rather than building one from the trigger payload. The trigger's `slug` is the slug the page serves at, which is suffixed on a collision (`my-post-2`), while the credit resolves on the row's own slug; a link built from the wrong one credits nothing. Hand-written links in the old convention (`utm_medium=social`, no `utm_content`) still resolve to nothing.
</Note>

Once the credit resolves, the piece appears in [campaign ROI](/advertising/reports) as a `content_item` asset with its contacts, deals and revenue, and in the piece's own scorecard as `attributed_contacts`.

## The scorecard: leads per piece

Traffic rewards the wrong posts. The number that tells the calendar which stage and angle to write next is leads per piece, and until this round it existed nowhere: the list showed ClickHouse page views, `content_analytics` had no writer, and the parts of a scorecard sat in five tables nothing joined. One module now reads all five, and every surface reads through it, so the list, the editor and the department cannot report different numbers for the same page:

| Number                              | Where it comes from                                                                                                                                                      |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `views`, `visitors`, `form_submits` | Page views, sessions and `form_submit` events on the piece's site path in the analytics collector (one query per batch, never one per item)                              |
| `leads`, `contacts`                 | Rows in the forms ledger whose page path is the piece's path, with spam, duplicates and deleted rows excluded, and the CRM contacts they resolved to                     |
| `rank`                              | The rank-tracking rows publish-to-site seeded for the target keyword: best position and the keywords                                                                     |
| `social`                            | Posts carrying `linked_content_id`, with the latest analytics snapshot per published version: posts, published, engagement, impressions, likes, comments, shares, clicks |
| `attributed_contacts`, `deals`      | The attribution engine's `content_item` credits, and the revenue engine over the union of the ledger and attributed contacts                                             |
| `lead_rate`                         | `leads / views`, null when there were no views                                                                                                                           |

The path is `settings.published_route`, stamped by publish-to-site, normalised the way the analytics routes normalise (query and fragment stripped, trailing slash removed, case kept). A piece with no route has no page to count on: its views, form submits and lead rate are `null` and the Postgres sources still answer. The collector being unreachable is a degraded scorecard (views `null`, `degraded.clickhouse` true), never a thrown error and never a zero, because a caller that reads 0 where the collector was down would prune a working post.

**Which number is the lead count.** `form_submits` is what the tracking embed saw in the browser; `leads` is what reached the forms ledger. They differ when the embed captured a submit the site's own handler rejected, or when a submission arrived by webhook with no embed on the page. Read `leads` for how many people the page converted and `form_submits` beside `views` for on-page behaviour.

### In the list and the editor

The content list under **Marketing > Content** gained a **Leads** column beside views: the ledger count for the page, with the lead rate in the tooltip, and a **Leads** sort that orders by the count with ties broken by rate. A collector that could not answer renders as an em dash, never as a zero.

The editor's sidebar gained a **Performance** section: a window switch (7, 30 or 90 days, or all time) over views, visitors, form submits, leads and contacts, deals when there are any, the best rank position and the keywords, the linked social posts' engagement, and the lead rate, plus the date of the newest stored nightly row. An unsaved item and a piece without a page get a sentence instead of a row of dashes.

### The nightly writer

`content_analytics` finally has a writer. Every night the `content-scorecard` job upserts, for every published item with a site page, the row for the last fully closed UTC day: `views` and `unique_views` are that day's own counts, `likes`, `shares` and `comments` come from the linked social posts' latest snapshot, and `external_analytics` carries `{ writer: "content-scorecard", computed_at, scorecard, day }`, where `scorecard` is the rolling 30-day scorecard as of that night and `day` is `{ date, views, visitors, form_submits, leads }`. The typed columns therefore sum as a daily time series while the newest row's envelope answers how the piece is doing now.

The run is an idempotent upsert on `(content_item_id, date)`, so re-running a day converges on the same rows; each account and each item runs in its own try/catch, so one unreadable row never costs another account its rows; it is bounded to a wall-clock budget with the least recently written accounts first, so a night that runs out of time leaves the freshest accounts for the next one; and a collector outage writes `null` for the day's views with the reason in the envelope, never a misleading 0.

### On the API

* `POST /api/olympus/marketing/content/views` and its dashboard twin: batch page and lead stats for up to 200 `{ projectId, path }` pairs, the read behind the list's Leads column. Degradation is a 200 with `degraded: true`; an absent key means no rows, not zero.
* `GET /api/olympus/marketing/content/{id}/analytics` (`content_analytics_get`): the scorecard computed now for `?window` plus the stored daily rows, paged and date-filtered, and `last_stored`. `GET /api/marketing/content/{id}/scorecard?window=` is the editor's read of the same.
* `GET /api/olympus/marketing/reports/campaign-roi` and `GET /api/marketing/reports/campaign-roi`: attributed contacts, deals and revenue per marketing asset, content items included, from the same builder as the Reports hub, so the department reads the rows the customer sees.

Every shape is on the [Content API](/api-reference/marketing-content#performance-and-leads) page.

## Proof and case studies

### The proof pack

A consideration or decision piece needs a figure, a quote or a customer's own words beside each claim, and the account already holds that proof in six modules that never reached a brief. `GET /api/olympus/marketing/content/proof-pack` (`content_proof_pack`; `GET /api/marketing/content/proof-pack` from the dashboard) reads all of them in one call:

| Source        | What it contributes                                                                                                      | `consent`                                                      |
| ------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------- |
| `testimonial` | Public testimonials with consent granted and not revoked, with the author's public attribution                           | `true`: quote verbatim with attribution                        |
| `review`      | Review text from the connected profiles, with the rating                                                                 | `true`                                                         |
| `grid_result` | Measured results on the active before/after grids, the avatar's grids first                                              | `true`                                                         |
| `ticket`      | The first inbound message of support tickets in the segment (the avatar's tags; the newest tickets when none carry them) | `false`: internal voice of customer, paraphrase without a name |
| `call`        | Call summaries                                                                                                           | `false`                                                        |
| `survey`      | Free text from completed surveys                                                                                         | `false`                                                        |
| `objection`   | The objection library with its best response                                                                             | `false`                                                        |

Every entry names its source (`source_type`, `source_id`), carries a ready-made citation (`[source: testimonial:<id>]`, `[source: before_after_grid:<id>]`) the pre-publish check accepts beside a figure, and a `consent` flag read from the rows, never assumed. Entries are interleaved in the order the journey stage calls for (decision leads with testimonials and grid results, awareness with tickets and surveys), filtered by `avatar_id`, `journey_stage`, `keyword` and `since`, and capped by `limit` (default 40). A source that cannot be read is reported with `ok: false` in `sources` and a line in `notes`; the other six still return. Nothing is written.

### The proof rules in the check

The [pre-publish check](/marketing/content-seo-check) classifies the item's `journey_stage` by keyword into awareness, consideration, decision or retention and applies two proof rules:

* **`claims_without_source`** is a warning at every stage and an **error at the decision stage**: a sentence stating a figure with no source in the same paragraph, where a source is a link, a bare URL or a `[source: ...]` citation. That is the page the reader is deciding on.
* **`proof_per_section`** (warning) applies to consideration and decision pieces: every H2 section must carry one proof element under it, a figure, a quote, a link or an image, and the check names the sections that have none.

The `[source: ...]` marker is stripped before word counts and phrase matching, so it never counts as copy. The Content department reads the proof pack before it drafts a consideration or decision piece and puts the citation beside the figure, so the rule is survivable rather than a wall.

### Drafting a case study from a won deal

`POST /api/olympus/marketing/content/case-study` (`content_case_study_draft`; `POST /api/marketing/content/case-study` from the dashboard) turns one won CRM deal into a case-study draft. It reads the deal with its contacts and company, the contacts' support tickets and call summaries for the problem in the customer's words, the consented testimonial for the quote and a before/after grid's measured results for the numbers, asks the Content department for the prose, and saves a `content_items` row of type `case_study` in draft, grounded on the avatar given (else the grid's target avatar), at the Decision stage, with `settings.case_study` and a five-section body: *The problem*, *What we did*, *Results*, *In their words*, *What this means for you*. Every result line carries `[source: before_after_grid:<id>]` and the quote `[source: reputation_testimonial:<id>]`, so the draft passes the decision-stage rules as written.

Three rules protect the customer:

* **A won deal is one the account calls won.** Its status is one marked `is_won` in the account's CRM statuses, or the literal `won` or `closed_won`; anything else is `409 deal_not_won` and nothing is written.
* **No consent, no quote.** The testimonial must be public with consent granted and not revoked; otherwise `409 no_consent` and nothing is written. Ticket and call lines shape the problem paragraph in the customer's words and are never attributed.
* **Numbers never come from the model.** The results are parsed from the grid's `measurable_results` (*from X to Y*, *X -> Y*), each tagged with its source, and the model is told to use only those figures; the quote is copied from the testimonial row and never rewritten. What the model writes is prose.

The draft holds the request open for up to three minutes and honours `Idempotency-Key`, so a client that times out and retries gets the first answer instead of a second row and a second model call. The other refusals: `404 deal_not_found`, `testimonial_not_found`, `grid_not_found`; `400 invalid_reference` for an avatar outside the account; `502 draft_unavailable` or `draft_bad_response` when the department does not answer with the JSON asked for.

**In the editor**, an item of type `case_study` shows its record in five sections (**Client information**, **The challenge**, **Our solution**, **Measurable results**, **Client testimonial**), read from and written to `settings.case_study`; the sources each result and the quote were built from are shown read-only, because they are the records the draft came from, not something to type.

## Research with the knowledge base

### The Content research knowledge base

Each account has one knowledge base with `context_type: "content_research"`, named **Content research**, created on first use and never duplicated. It is where the research run keeps every page it relied on, every brief it wrote and, over time, every other research artifact the content program produces. It appears on the knowledge base hub like any other KB, filterable by its context type, and `kb_search` with no knowledge base id covers it along with every other KB in the account.

### The research run

`POST /api/olympus/marketing/content/{id}/research` (`content_research_run`; `POST /api/marketing/content/{id}/research` from the dashboard) researches one content item, in this order:

1. **Knowledge bases first.** Every non-archived knowledge base in the account is searched for each query (brand guides, service menus and earlier research all count), and each hit is a candidate claim of kind `kb`.
2. **The SERP.** The Google results for the item's target keyword, through DataForSEO and counted against the monthly SEO research cap. A SERP brief stamped on the item in the last 30 days (`settings.serp_brief`) is reused rather than paid for again; a cap hit returns `serp: null` plus a gap (*SERP not read: monthly research cap*), never an error.
3. **The web.** Up to `max_sources` pages (default 5, at most 10) found by search with their page text, preferring pages in the SERP top 10; a page already in the Content research KB is reused, and a page over 60,000 characters is skipped and reported as a gap rather than truncated silently.
4. **Extraction.** Claims, figures, competitor notes and gaps, each claim with a quote copied verbatim from its passage and a source URL. A quote that is not found in the passage is dropped and counted in `warnings`: the deterministic guard against invented evidence. Without a model key the passages themselves become the claims and `confidence_score` is 0.
5. **Indexing with provenance.** Every web page the run used is indexed into the Content research KB with its URL, and recorded as a source and a page, so the next `kb_search` finds it and a claim can point at the page it came from. One source failing is reported (`indexed: false`, a gap) and the run continues.
6. **The artifact.** A knowledge artifact of type `content_research`, named *Research: title*, with the full result as JSON and the rendered brief as markdown, the markdown also indexed as a document in the same KB so a semantic search surfaces the brief itself. A re-run refreshes the artifact the item already points at rather than piling up a new one.
7. **The stamp, last.** `settings.research` on the item: `{ version, artifact_id, knowledge_base_id, ran_at, queries, claims_count, stats_count, sources[] (max 10), serp_keyword, gaps[] (max 10) }`, merged into `settings` so sibling keys survive, and written only after the artifact exists and indexing finished.

The body accepts `queries` (up to five, defaulting to the target keyword or the title), `keyword`, `include_web`, `include_serp`, `max_sources`, `location_code` or `location_name`, `index_sources` and `serp_brief_from_item`. The response carries the artifact ids, the stamp, `claims[]`, `stats[]`, `competitor_notes[]`, `gaps[]`, `sources[]`, `serp`, the hit counts, `extraction` (`llm` or `fallback`), `confidence_score`, `spent` and `warnings[]`. `GET` on the same path returns the stored stamp and the artifact it points at. `POST /api/olympus/marketing/content/research` runs the same research for a topic that has no item yet (`topic` required); nothing is stamped and the artifact is the deliverable.

**What a run costs.** Search credits (one search per query, pages fetched inline), one SERP call unless a fresh SERP brief exists or `include_serp` is false, embeddings for every indexed page and for the brief, and one model completion for the extraction. The run is synchronous inside the request and may take up to five minutes; call it with a client timeout of four minutes or more, and send an `Idempotency-Key` so a retry replays the first answer. A re-run spends again for the SERP and the extraction but never re-indexes a URL the KB already holds.

### Artifacts, and how to find them

The Content research KB holds one artifact type per deliverable, each named *Type: subject*, each with `content_json.version` and `content_json.content_id` (or `null` for an account-level artifact), and each with its markdown indexed as a document in the same KB:

| `artifact_type`    | What it holds                                                                         | Stamp on the item                                              |
| ------------------ | ------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `content_research` | A research run: claims, stats, competitor notes, gaps, the SERP snapshot, the sources | `settings.research`                                            |
| `serp_brief`       | A SERP brief: intent, page type, the top 10, related questions and features           | `settings.serp_brief` (reused by the research run for 30 days) |
| `positioning`      | The account's positioning note, account-level; `is_verified` once the owner approves  | none                                                           |
| `proof_pack`       | A stored proof pack for an avatar                                                     | `settings.proof_pack`                                          |
| `case_study`       | The case-study record behind a drafted case study                                     | `settings.case_study`                                          |
| `data_study`       | An original data study: the question, the window, the aggregates                      | `settings.data_study`                                          |

Two reads find them without knowing the knowledge base id first: `GET /api/olympus/knowledge-bases/artifacts?artifact_type=&content_id=&is_verified=` (`kb_artifacts_list`) lists the account's artifacts by type, newest first, and `GET /api/olympus/knowledge-bases/artifacts/{artifactId}` (`kb_artifact_get`) reads the one a stamp points at. Semantically, `kb_search` finds a brief or a proof pack through its indexed markdown, whose rows carry `metadata.artifact_id` and `metadata.artifact_type`; a research source carries `metadata.url`.

### What the department does with it

The Content department searches before it writes: `knowledge_search_query` across every knowledge base for each H2 it plans, then, when the pinned item has no `settings.research` or the stamp is older than 30 days, it runs the research and reads it back 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 KB with its URL, so the next writer finds it. Its header line gains `Sources: <n>`. The knowledge index the department reads at the start of a turn lists the research KB's artifacts (id, type, name, updated at) beside the knowledge bases, and each row of its content data file carries the research stamp summary (`artifact_id`, `claims_count`, `ran_at`).

**In the editor**, the sidebar's **Research** section reads the stored stamp and the artifact it points at through `GET /api/marketing/content/{id}/research` and shows the brief: the claims with their source, the figures, the gaps, the sources, when it ran and a note once the run is older than 30 days. **Run research** (**Run again** once a brief exists) sends the same POST with a progress line while it runs, and the section states the spend before the run: one web search per query with up to five page fetches, one Google results read counted against the monthly SEO research cap (skipped when a SERP brief from the last 30 days exists), embeddings for each page it indexes and one model call for the extraction; a re-run refreshes the same brief and never re-indexes a page already in the knowledge base. An unsaved item shows no section.

### Known limits

* Related questions and SERP features are empty on a live SERP read; a stored SERP brief carries them.
* Two runs indexing the same new URL at the same moment can index it twice once; the dedupe is on the stored URL, and the next run reuses the first copy.
* There is no background job: a run is synchronous inside the request that asked for it.

## Related pages

* [The Content Department](/marketing/content-department) — the writer that plans distribution with the brief, cites the proof pack and researches before it drafts
* [Pre-Publish SEO Check](/marketing/content-seo-check) — the two proof rules, every other check, and the response shape
* [Standing Playbooks](/marketing/standing-playbooks) — the repurpose and digest templates beside the other ready-made workflows
* [Workflow Nodes](/integrations/workflow-nodes) — the Content Published trigger and the marketing nodes it feeds
* [Attribution](/marketing/attribution) — how a UTM-stamped click becomes a credited lead
* [Reports](/advertising/reports) — the campaign ROI report a credited piece appears in
* [Content API](/api-reference/marketing-content) — every route on this page with its params and response keys
* [Social Media API](/api-reference/marketing-social#repurpose-source) — the repurpose source package and its UTM links
