Writing and publishing
Inspect a page brief, write and publish articles and answers, and correct what is live.
API v1
add_article is the only way to publish a page: a multi-step run in which your AI writes the prose and Pink structures it and pushes it to your site. See Sessions and publishing for the flow. get_article / set_article and get_answer / set_answer read and correct what is already live.
Each description below is the text your AI client receives for that tool, word for word. It is written for the model, which is why it reads like instructions.
get_article_brief
Return everything the LLM needs to understand one cm_pages row before publishing: foundation (brand/audiences/goals/CTAs — the brand voice is not included, it is applied server-side when add_article writes the article), page meta + creator_notes (first-party context), pre-assigned audience/goal/cta (the cta button links to the goal's CTA button URL, else the goal URL; cta is null with a warnings[] entry when the goal has CTA copy but no headline, button label or URL), attached questions[] with research-backed answers, deduplicated research facts + opinions, primary+secondary keywords with role markers, internal-link targets and sibling pages in the same topic (each with published: false while not yet live — a link to one publishes as plain text until it goes live), recall hits across the existing site corpus, the Publish Protocol block vocabulary (blocks.json verbatim), and the editorial scope rules. SERP: competitor_context is the stored top-10 for the primary keyword; when that snapshot is missing or older than 7 days, warnings[] carries serp_stale with the keyword — call serp_keyword_refresh with it first if you want current positions. FAST. Use this to inspect a page before calling add_article. To actually publish, call add_article(page_id) and follow the step-by-step instructions in each response.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
page_id | integer | yes | cm_pages.id to brief. |
include_recall | boolean | Include vector recall hits across the site corpus (default true). | |
recall_k | integer | Top-K recall hits when include_recall=true. Default 8, max 50. |
Input schema
add_article
Publish a page (Article, FAQ, Landing, Pillar, or Utility). This is the ONLY publish entry point. STATE MACHINE: each call returns { run_token, step, instruction, payload, next_call }. You MUST follow the instruction and call add_article again with the run_token (and the requested payload) until step="done". The done step returns payload.published — every page AND answer the run published, each with its live url (url is null, with a not_live note, while the site has no live address — then say so; never call those pages live) — review them with get_article / get_answer. Steps for an Article: queued → research (server-side, ~30-60s per call batch; researches the page's questions flagged for research — a page with none skips it, and the response says so in warnings[] with payload.research_status, meaning the article gets no sources) → write_copy (you write markdown prose) → done. Read warnings[] whenever a response carries it. Steps for a Pillar: queued → research → write_child_copy (one child per call) → write_pillar_copy → done. Your only job is the write_* step(s): the server hands you the EXACT prompt in payload.prompts[*].prompt — execute it verbatim and return markdown prose. The server handles all structuring (headings, lists, blocks per the Publish Protocol schema), internal-link resolution (page:ID → URL; a link to a page that is not live yet publishes as plain text and becomes a link when that page publishes — warnings[] link_target_not_live names it), citation collection, and the publish push. Do not return JSON or Block[]; do not try to format the page yourself. Submit ONE page per call — the server banks each copy and re-asks for the rest until every page is in; a single long article can be split across calls with append: true + more: true (drop more on the final chunk to publish). SLOW (minutes per page; longer for pillars).
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
page_id | integer | cm_pages.id to publish. Required on the FIRST call; optional on follow-ups when run_token is set. | |
run_token | string | Echo back the run_token returned by the previous call to advance the run. Omit on the first call. | |
payload | object | Required when the previous response asked for one. Shape: { copies: { "<page_id>": "" }, append?: true, more?: true }. Submit ONE page per call — the server holds what you send and re-asks for the rest, and only advances once every page_id in payload.expected_keys has copy. For a long article, split it on blank lines and send each continuation with append: true, plus more: true on every call except the last (more: true = "not finished, do not publish yet"). |
Input schema
get_pipeline_status
Check the current article-pipeline status for a page. Returns {status, message, is_pillar, pages_done, pages_total, has_article, last_updated}. status ∈ {idle, queued, researching, generating, done, error}. For pillar pages, pages_done/pages_total tells you child article progress. Call when the user asks about progress, or before deciding whether to re-fire add_article on the same page.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
page_id | integer | yes |
Input schema
get_article
Read a published article before changing it. Returns the copy prompt the writer ran, the article prose as published, the current published/draft status, and the [src-N] sources. id is cm_pages.id. For inputs (research, keywords, pains) use get_article_brief instead.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | yes | cm_pages.id |
Input schema
set_article
Replace a published article's prose, its status, or both. copy is plain markdown prose — never JSON or blocks; the server resolves text links (one to a page that is not live yet publishes as plain text until it goes live, named in warnings[]), structures it, and republishes with the same validation as add_article. status: "draft" takes the page down without deleting it. Omit copy to change status alone — or, after a failed add_article run for this page, to publish the copy that run saved (the result then carries reused_copy). id is cm_pages.id. The result carries published_url (the live link), or null with a not_live note while the site has no live address — then say so, not that it is live. Call get_article first: rewriting without reading throws away whatever the current article got right.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | yes | cm_pages.id |
copy | string | Replacement article as plain markdown prose. Internal links as Title. Omit to keep the current copy. | |
status | published, draft | Defaults to published. |
Input schema
get_answer
Read a Q&A page before changing it. Returns the exact prompt the generator ran, the research-backed sources with their [src-N] ids, the current published/draft status, and the answer copy itself. Answers are always machine-written first — this is how you see what was written and why.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | yes | cm_questions.id |
Input schema
set_answer
Replace a Q&A answer, its status, or both. copy is plain prose — never JSON or blocks; the server structures it, resolves [src-N] citations against the same sources get_answer returned, and republishes. status: "draft" takes the page down without deleting it, "published" puts it back. Omit copy to change status alone and the stored prose is re-published unchanged. The result carries url (the live link), or null with a not_live note while the site has no live address — then say so, not that it is live. A link to a page or answer that is not live yet publishes as plain text and comes back when that page publishes — warnings[] link_target_not_live names it. Call get_answer first: rewriting without reading the brief throws away whatever the current answer got right.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | yes | cm_questions.id |
copy | string | Replacement answer as plain prose, citing [src-N]. Omit to keep the current copy. | |
status | published, draft | Defaults to published. |
Input schema
add_answer
Generate and publish the answer for ONE researched question, without running the whole add_article pipeline. The server writes the copy from the question's research, structures it, and publishes it — you supply nothing but the id. The result carries the answer's live url, or url: null with a not_live note while the site has no live address (then tell the user that, not that it is live). A link to a page or answer that is not live yet publishes as plain text and comes back when that page publishes — warnings[] link_target_not_live names it. Requires research; returns {status:"skipped"} when the generator judges the question common knowledge. To write the answer yourself, use set_answer with copy.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | yes | cm_questions.id |
Input schema