Revise an existing entry
Authoring an article and revising one are different jobs. When you write a new entry you produce the whole body once. When you revise, most of the body is already right, and rewriting it to change one sentence is slow, expensive, and risky — a regenerated body drifts, drops a paragraph, or mangles an asset:// token. cms_update_entry therefore takes two surgical inputs alongside data:
textReplacements— exact find-and-replace edits inside a field's stored text, the same way a code editor edits a file.blockEdits— add, replace, remove and reorder individual blocks of ablocksfield by their key.
Reach for the first to change what the text says, the second to change which blocks exist. Both can ride on one call.
TL;DR
cms_get_entry— read the entry and hold on toversion.cms_update_entrywithifVersion—textReplacementsto reword,blockEditsto restructure,datafor short fields you replace whole.cms_get_preview_linkif a human should see it before it goes live (skill://cms/preview-entry).cms_publish_entryif the entry was a draft. A published entry is live the moment the update lands.
Step 1 — read the entry
{ "name": "cms_get_entry", "arguments": { "id": "<entryId>" } }
You need the current text to quote it exactly, so read the field you are editing in full. If you only need to confirm a title, status or version, pass fields and skip the body:
{ "name": "cms_get_entry", "arguments": { "id": "<entryId>", "fields": ["title", "dek"] } }
Step 2 — replace text in place
{
"name": "cms_update_entry",
"arguments": {
"id": "<entryId>",
"ifVersion": 7,
"textReplacements": [
{ "field": "body", "oldText": "We ship every tuesday.", "newText": "We ship every Tuesday." },
{ "field": "body", "oldText": "## Pricing\n\nPlans start at $10.", "newText": "## Pricing\n\nPlans start at $12." },
{ "field": "dek", "oldText": "$10", "newText": "$12" }
]
}
}
Rules the server enforces:
oldTextmust match the stored text exactly, whitespace and punctuation included, and it must occur exactly once in that field. Zero matches fails withcms_replacement_no_match; several matches fail withcms_replacement_ambiguous. Widen the quote until it is unique, or set"replaceAll": trueto change every occurrence (a renamed product, a changed year).- Edits apply in order, all or nothing. A later edit sees the result of an earlier one. If any edit fails, nothing is written and
versiondoes not move. - A field is sent one way or the other. Put a field in
textReplacementsor indata, never both in the same call. Usedatafor short fields you are replacing whole (category,read_time_minutes) and for anything that is not text. - Blocks are searched as one body. On a
blocksfield,fieldis the blocks field itself and the match runs across the prose of every block — alead, aprosemarkdown block, apull_quote. Block type names, keys and non-text props are never matched, sooldText: "prose"finds the word in your copy, not the block type. Replacing a whole paragraph is one edit whoseoldTextis that block's full text. To add, remove or reorder a block rather than reword one, useblockEditsbelow. - Inline images can be quoted as you saw them.
cms_get_entryshows inline images as public URLs; the stored text holdsasset://<id>. Either form works inoldTextandnewText— the server maps the URL back to the token before matching, so the image stays tracked.
The response is the updated entry at the new version. Pass "responseFormat": "summary" when you do not need the body echoed back — in a long editing session the echo is the biggest cost — and read the summary's fieldSummary to confirm the word count moved the way you expected.
Inserting and deleting
There is no separate insert operation because a replacement already does it. To insert after a paragraph, quote the paragraph and give it back with the new text appended:
{
"field": "body",
"oldText": "That is the whole argument.",
"newText": "That is the whole argument.\n\nOne caveat: it only holds for teams that already ship weekly."
}
To delete, make newText an empty string. To insert an image between two paragraphs, quote the end of the first and append the image line (skill://cms/upload-asset-and-embed).
Step 2b — add, remove or reorder blocks
textReplacements reword what a block says. blockEdits change which blocks exist and in what order, addressing them by their stable key:
{
"name": "cms_update_entry",
"arguments": {
"id": "<entryId>",
"ifVersion": 7,
"blockEdits": [
{ "field": "body", "op": "set", "key": "caveat", "block": { "type": "callout", "props": { "tone": "warn", "body": "Pricing changed in May." } }, "after": "pricing" },
{ "field": "body", "op": "delete", "key": "stale-aside" },
{ "field": "body", "op": "move", "key": "takeaways", "position": "end" }
]
}
}
setreplaces the block with that key, or inserts it when the key is new.blockis the whole block —typeplus every prop — and replaces the old one outright rather than merging. Omitkeyto insert under a minted one.deleteremoves the block with that key.moverepositions it, and requires one ofbefore,afterorposition.
before and after name another block's key, position is "start" or "end", and at most one of the three goes on any edit. Edits apply in order and all-or-nothing: insert a block and then move it in the same call, and if any edit fails nothing is written. An unknown key fails with cms_block_not_found; two blocks sharing the addressed key fail with cms_block_ambiguous.
Both kinds of edit can ride on one call. Block edits run first, so a text replacement in the same call sees the blocks you just inserted. That also means an oldText matching both an old block and a newly inserted one becomes ambiguous — quote enough context to stay unique.
Retargeting an inline entry link
An inline ref://<entryId> is ordinary text, so you repoint one with a replacement like any other edit:
{ "field": "body", "oldText": "ref://<oldEntryId>", "newText": "ref://<newEntryId>" }
You rarely need to. The id names a translation group, so a token already resolves to the target in the locale being delivered — retarget only when the link should point at different content, never to "fix" the language.
Introducing a token into a field that doesn't read inline refs is rejected on write (cms_invalid: … contains a ref:// token). Prose fields (markdown, rich_text) always read them; a text or array-of-text field needs inlineRefs: true in the collection schema (skill://cms/design-collection). The failure is loud on purpose — the old behaviour was a link that silently vanished at render time.
When a whole-field rewrite is the right call
If most of a field is changing — a rewritten introduction, a translated body, a restructured argument — send the new value in data instead. textReplacements earns its keep when the edit is small relative to the field; a replacement whose oldText is 90% of the body saves nothing. The same goes for a blocks field being rebuilt from scratch: past a handful of blockEdits, send the array.
Step 3 — preview, then publish
A draft stays a draft until you publish it. A published entry goes live with the update, so for anything beyond a typo on a live article consider unpublishing first, or making the edit on a draft copy and swapping.
{ "name": "cms_publish_entry", "arguments": { "id": "<entryId>", "ifVersion": 8 } }
Publish, unpublish and schedule return a summary by default because they do not change content; pass "responseFormat": "full" if you need the body back.
Recovering
cms_version_conflict— someone wrote between your read and your write. Re-read withcms_get_entry, check youroldTextstill matches, and retry with the newversion.cms_replacement_no_match— you are quoting text that is not there. The usual causes are a paraphrase instead of a copy, a straight quote where the text has a curly one, or an edit that an earlier edit in the same call already changed. Re-read and copy the passage exactly.- A bad edit went live —
cms_list_versionsthencms_restore_version(skill://cms/publish-entry, step 4). Every update is a version.
What NOT to do
- Don't regenerate the body to change a sentence. That is what
textReplacementsis for. A regenerated body is a new text that happens to resemble the old one. - Don't send the same field in
dataandtextReplacementsorblockEdits. A whole-value replace and a surgical edit of the same field conflict, and the call is rejected. Block edits and text replacements on one field are fine together. - Don't reorder blocks by deleting and re-adding them.
movekeeps the block's key, its props and its asset references intact; a delete-then-set round-trip risks losing props you didn't restate. - Don't retry a
no_matchwith a guessed variant of the quote. Read the entry again and copy. - Don't edit a
jsonfield this way.jsonis opaque; the server refuses text replacements on it.
Related
skill://cms/publish-entry— versions, optimistic locking, scheduling and rollback.skill://cms/author-with-blocks— how block bodies are structured, so you know what a replacement can reach.skill://cms/upload-asset-and-embed— adding or swapping an image in the body.skill://cms/preview-entry— a signed preview link for a human review before publishing.