edit.pink
Pink Editor docs

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

ParameterTypeRequiredDescription
page_idintegeryescm_pages.id to brief.
include_recallbooleanInclude vector recall hits across the site corpus (default true).
recall_kintegerTop-K recall hits when include_recall=true. Default 8, max 50.

Input schema

{
  "type": "object",
  "properties": {
    "page_id": {
      "type": "integer",
      "description": "cm_pages.id to brief."
    },
    "include_recall": {
      "type": "boolean",
      "description": "Include vector recall hits across the site corpus (default true)."
    },
    "recall_k": {
      "type": "integer",
      "description": "Top-K recall hits when include_recall=true. Default 8, max 50."
    }
  },
  "required": [
    "page_id"
  ]
}

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

ParameterTypeRequiredDescription
page_idintegercm_pages.id to publish. Required on the FIRST call; optional on follow-ups when run_token is set.
run_tokenstringEcho back the run_token returned by the previous call to advance the run. Omit on the first call.
payloadobjectRequired 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

{
  "type": "object",
  "properties": {
    "page_id": {
      "type": "integer",
      "description": "cm_pages.id to publish. Required on the FIRST call; optional on follow-ups when run_token is set."
    },
    "run_token": {
      "type": "string",
      "description": "Echo back the run_token returned by the previous call to advance the run. Omit on the first call."
    },
    "payload": {
      "type": "object",
      "description": "Required when the previous response asked for one. Shape: { copies: { \"<page_id>\": \"<markdown prose>\" }, 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\")."
    }
  },
  "required": []
}

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

ParameterTypeRequiredDescription
page_idintegeryes

Input schema

{
  "type": "object",
  "properties": {
    "page_id": {
      "type": "integer"
    }
  },
  "required": [
    "page_id"
  ]
}

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

ParameterTypeRequiredDescription
idintegeryescm_pages.id

Input schema

{
  "type": "object",
  "properties": {
    "id": {
      "type": "integer",
      "description": "cm_pages.id"
    }
  },
  "required": [
    "id"
  ]
}

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

ParameterTypeRequiredDescription
idintegeryescm_pages.id
copystringReplacement article as plain markdown prose. Internal links as Title. Omit to keep the current copy.
statuspublished, draftDefaults to published.

Input schema

{
  "type": "object",
  "properties": {
    "id": {
      "type": "integer",
      "description": "cm_pages.id"
    },
    "copy": {
      "type": "string",
      "description": "Replacement article as plain markdown prose. Internal links as [Title](page:123). Omit to keep the current copy."
    },
    "status": {
      "type": "string",
      "enum": [
        "published",
        "draft"
      ],
      "description": "Defaults to published."
    }
  },
  "required": [
    "id"
  ]
}

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

ParameterTypeRequiredDescription
idintegeryescm_questions.id

Input schema

{
  "type": "object",
  "properties": {
    "id": {
      "type": "integer",
      "description": "cm_questions.id"
    }
  },
  "required": [
    "id"
  ]
}

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

ParameterTypeRequiredDescription
idintegeryescm_questions.id
copystringReplacement answer as plain prose, citing [src-N]. Omit to keep the current copy.
statuspublished, draftDefaults to published.

Input schema

{
  "type": "object",
  "properties": {
    "id": {
      "type": "integer",
      "description": "cm_questions.id"
    },
    "copy": {
      "type": "string",
      "description": "Replacement answer as plain prose, citing [src-N]. Omit to keep the current copy."
    },
    "status": {
      "type": "string",
      "enum": [
        "published",
        "draft"
      ],
      "description": "Defaults to published."
    }
  },
  "required": [
    "id"
  ]
}

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

ParameterTypeRequiredDescription
idintegeryescm_questions.id

Input schema

{
  "type": "object",
  "properties": {
    "id": {
      "type": "integer",
      "description": "cm_questions.id"
    }
  },
  "required": [
    "id"
  ]
}

On this page