Skip to content
BetaDocs

Blocks Tools

Pages, partials, Page templates all share one editing surface: a tree of blocks. The AI agent reaches for blocks instead of writing raw HTML — it’s faster, more consistent, and the same edits the AI agent makes are what you see in the editor’s drag-and-drop panel.

For a guided introduction, start with Pages, blocks and templates. Browse the complete built-in block catalog, follow Create a Page template or Create your own block type, then use this reference for detailed settings and tool behavior.

The block reference has these pages:

There are two distinct kinds of block tools — keep them straight:

  • Block-instance tools — add_block, update_block, move_block, remove_block, duplicate_block, set_block_responsive, get_page_blocks. These place + edit instances of blocks inside a container.
  • BlockType tools — list_block_types, read_block_type, create_block_type, update_block_type, delete_block_type, find_pages_using_block_type, export_block_types, import_block_types. These manage the type definitions — the things that show up in the block picker.

Asking “add another testimonial to the home page” is instance work. Asking “make me a custom price-card block with a built-in star rating” is type work.

A container is anything that holds a tree of blocks. Three kinds:

Kind What it is target.id
page A regular page in blocks-mode The page’s id
partial A header/footer/free block in blocks-mode "header", "footer", or a free-block id
template A page template The template id (e.g. "blog-post")

Every instance tool accepts the same shape:

target: { kind: "page" | "partial" | "template", id: string }

For pages, page_id: "<id>" is a shorthand for target: { kind: "page", id: "<id>" }. Older prompts still work; new patterns should prefer target.

"Add a feature_grid in the header that appears on every page."
→ add_block target={kind:"partial", id:"header"} block={type:"core/feature_grid", ...}
"Build a blog post template — featured image at the top, then title, then the post body."
→ add_block target={kind:"template", id:"blog-post"} block={type:"template/page_featured_image"}
→ add_block target={kind:"template", id:"blog-post"} block={type:"template/page_title"}
→ add_block target={kind:"template", id:"blog-post"} block={type:"template_content_slot"}
"Change how every blog post lays out — show the date next to the title, then a wide image."
→ add_block target={kind:"template", id:"article-layout"} block={type:"template/page_title"}
→ add_block target={kind:"template", id:"article-layout"} block={type:"template/page_date"}
→ add_block target={kind:"template", id:"article-layout"} block={type:"template/page_featured_image"}
  • move_block — re-parent or reorder inside the same container. Cycles rejected.
  • duplicate_block — clones a block (subtree included), inserts the copy right after the original, returns the new id. Easiest way to add another card to a feature grid.
  • remove_block — drops a block and everything inside it.
Tool Purpose
get_page_blocks Read the block tree of any container
add_block Insert a block (at top level, in a parent’s children, or a named slot)
update_block Shallow-merge new field values into block.data; null removes a key
move_block Re-parent or reorder a block
remove_block Remove a block + its subtree
duplicate_block Clone a block + insert the copy right after
set_block_responsive Set per-breakpoint values on a responsive field

All accept target: { kind, id } or the page_id shorthand.

The write tools (add_block, update_block, move_block, remove_block, duplicate_block, set_block_responsive) answer with the block they changed instead of the container’s whole tree, which ran to tens of kilobytes for one add_block on an article page:

{
"added_id": "blk_3f2a9c1d7e4b",
"block": {
"id": "blk_3f2a9c1d7e4b",
"type": "core/heading",
"data": { "text": "Related articles", "level": "h2" }
},
"parent_id": "sec_main",
"slot_index": null,
"position": 2,
"has_unsaved_changes": true
}
Field Meaning
block The block after the write, with its own children and slots (the copy for duplicate_block)
parent_id The containing block, or null at the container’s top level
slot_index The parent’s slot holding the block (slot containers such as core/columns), else null
position 0-based index in that list
has_unsaved_changes The working copy differs from the saved page or partial. Always false for a template, which has no draft
warnings As described under Write warnings; absent when there is nothing to report

add_block also returns added_id and duplicate_block duplicate_id. remove_block returns removed_id and the parent_id, slot_index and position the block was removed from, without block. move_block reports where the block now sits.

MCP asks for this by default. Pass response: "tree" to get the whole tree instead, or read it with get_page_blocks.

Over HTTP the default stays the tree, so existing callers see no change. Ask for the compact answer with response: "summary" as a body key (POST, PATCH, PUT) or a query parameter (any method, including DELETE):

POST /api/v1/sites/{siteId}/pages/{pageId}/blocks?response=summary
PATCH /api/v1/sites/{siteId}/containers/{kind}/{id}/blocks { "block_id": "...", "data": {...}, "response": "summary" }
DELETE /api/v1/sites/{siteId}/containers/{kind}/{id}/blocks?block_id=...&response=summary
POST /api/v1/sites/{siteId}/containers/{kind}/{id}/blocks/duplicate?response=summary
POST /api/v1/sites/{siteId}/containers/{kind}/{id}/blocks/responsive?response=summary

The body key wins over the query parameter. Any other value is refused with 400. A Core release without compact answers ignores the query parameter and answers with the tree, which is why MCP sends it that way.

A small label above a heading belongs to the heading block, not to a separate text block. Put it in the heading’s Eyebrow field. From render version 2 a heading also has a Subtitle, and each part has its own style field (eyebrow_style_id, subtitle_style_id); without one, they use the site’s Eyebrow and Lead styles. The three parts render in one <hgroup>, so the eyebrow and subtitle never become extra headings in the outline.

update_block page_id=… block_id=… data={ eyebrow: "For leadership teams", eyebrow_style_id: "eyebrow", text: "AI partner", level: "h1" }

Saving a classed label in a text block directly above a heading returns a heading_part_as_text warning that says so.

Every block’s Advanced settings has CSS class and Anchor id (style_overrides.custom_class and html_id). Use the class from site or page custom CSS. From render version 2 it lands on the meaningful element (the heading or the button link) instead of the block wrapper. For a look that recurs, create a named style instead.

The text editor applies paragraph styles like a word processor: Paragraph, Heading 2–4 or any of the site’s text styles (such as Lead or Small text). The style class (s-<id>) is stored on the paragraph.

Text that holds HTML the visual editor cannot keep (other classes, images, tables) opens in the source editor. To make such content editable, rebuild it with heading, text, image and button blocks. Typeroll does not convert HTML into blocks.

Tool Purpose
list_block_types Every type usable on this site
read_block_type Full schema, parts, template and styles of a type
create_block_type New custom type (origin=ai, optional script)
update_block_type Patch a custom or third-party type
delete_block_type Remove a custom/third-party type (core protected)
find_pages_using_block_type Reverse-index: which pages reference this type?
export_block_types / import_block_types .tcblocks package format for portability

Every built page carries data-* attributes on <body> describing which route it is, so a stylesheet can vary by route without duplicating a block tree behind visibility branches. Preview writes the same attributes, so a page-scoped rule looks the same in preview and on the published site.

Attribute Value
data-route home, facet, item or page — always present
data-page the page id
data-content-type the content type id
data-template the Page template id, when the page has one
data-facet-field / data-facet-value the taxonomy scope, on a facet route
data-depth segments below the root: / is 0, /guides/a/ is 2
data-page-number the slice number, only on pages after the first

To vary a block in a shared partial by page, give that block a style_overrides.custom_class and select it under the page attribute. Do not mark the page content or select on data-source-block-id or .block-*; those are not part of this contract.

body[data-route="home"] .header-banner {
background: #007cba;
}
body[data-route="home"] .banner {
--banner: var(--color-primary);
}
body[data-route="item"][data-content-type="article"] .banner {
--banner: teal;
}

Absent and empty are different. A page with no template omits data-template rather than emitting an empty one, so [data-template] means “has a template” and never matches a page that does not. The same holds for every optional attribute above.

These are a contract. Once a site styles against them, renaming one breaks that site, so the set is added to rather than reshaped.

There is deliberately no archive route kind. A listing is a block inside an ordinary page rather than a route of its own, so nothing in a built page identifies one — and a guess would be worse than an absence, because stylesheets would be written against it.

A successful write can carry warnings alongside its result. They report things that were stored and will never be read — a write that looks correct and is not.

{
"ok": true,
"warnings": [
{
"code": "unknown_style_override",
"block_id": "b1",
"path": "blocks[0].style_overrides.class",
"key": "class",
"message": "style_overrides.class is not a style override and will never be applied. Supported keys: spacing_before, spacing_after, custom_css, custom_class, html_id."
}
]
}

style_overrides is a closed set — spacing_before, spacing_after, custom_css, custom_class, html_id — so any other key is inert. Writing class instead of custom_class stores the key, returns 200, builds and deploys, and never reaches the markup.

Warnings are returned by PUT/PATCH /pages/{pageId}, PUT/PATCH /partials/{partialId}, POST /pages/batch-write (where each entry carries its own, so a sweep says which page was affected), the working copy PUT /working-copy/{target} and the block routes (add_block, update_block). The field is absent when there is nothing to report.

They do not reject the write. A write that is otherwise right should land, and the caller has already spent the round trip. Check for warnings after a successful write; the absence of the field is the only clean signal. The one exception is an unknown data key in add_block or update_block, which is rejected (see below).

A data key that no field of the block’s type declares is stored and never rendered. Tree writes (PUT/PATCH /pages/{pageId}, PUT/PATCH /partials/{partialId}, POST /pages/batch-write and the working copy PUT) still land and report it as unknown_field:

{
"code": "unknown_field",
"block_id": "related-list",
"block_type": "core/repeater",
"path": "blocks[3].data.placeholder_background",
"key": "placeholder_background",
"suggestions": ["item_overrides.placeholder_background"],
"message": "blocks[3].data.placeholder_background is not a field of core/repeater; it is a field of the item block core/post_card. Did you mean item_overrides.placeholder_background? The key would be stored and never rendered."
}

add_block and update_block (POST/PATCH .../blocks, for pages and other containers) reject the write with 400 instead, because they name one block and its type. Nothing is written, valid keys in the same request included. The body lists every unknown key:

{
"error": "data.placeholder_background is not a field of core/repeater; it is a field of the item block core/post_card. Did you mean item_overrides.placeholder_background? The key would be stored and never rendered. Nothing was written.",
"code": "unknown_field",
"unknown_fields": [
{
"code": "unknown_field",
"block_id": "related-list",
"block_type": "core/repeater",
"path": "data.placeholder_background",
"key": "placeholder_background",
"suggestions": ["item_overrides.placeholder_background"],
"message": "..."
}
]
}

add_block checks the whole inserted block, children and slots included (block.children[0].data.colour). update_block checks only the keys it writes, so a key stored earlier does not block an unrelated write.

The suggestions say where the value probably belongs:

  • On a repeater, or an alias of one such as core/testimonials, core/page_list or core/gallery, a field of the item block (item_block, or the alias default) belongs in item_overrides.<key>.
  • A style override name (custom_class, html_id, …) belongs in style_overrides.
  • Otherwise the closest field names of the type, e.g. level for levle.

What counts as a field: the type’s schema, and for an alias the schema of the type it expands to (a core/testimonials block accepts every core/repeater field). The renderer also reads a few keys that a schema need not declare, so they are always accepted: responsive_breakpoints on any block, the names a site block type’s templates and composition read ({{subtitle}}, {{#each rows}}, {{props.title}}), and the names a conditional block’s condition reads. Blocks of a type the site does not know, and extension blocks, whose component receives all of their data, are not checked. read_block_type lists a type’s fields.

In update_block, a null value removes the key from block.data instead of storing null; the field then renders its default. Use it to clean up a key stored by mistake:

{ "block_id": "related-list", "data": { "placeholder_background": null } }

Removing is always allowed, also for a key the type does not declare.

A known field whose value has the wrong type is reported as field_type_mismatch, with the path, the field and the type it needs:

{
"code": "field_type_mismatch",
"block_id": "tabs-1",
"path": "tabs-1.data.active_font_weight",
"field": "active_font_weight",
"expected": "a number",
"message": "tabs-1.data.active_font_weight is \"bold\", but active_font_weight must be a number. The value is stored and will not render."
}

Number fields need a number; a string that is exactly a number, such as "600", is rendered as that number, including values already stored. Boolean fields need true or false, not the text "true". A select field needs one of its options. Responsive values are checked per breakpoint and array items per item. add_block and update_block (POST/PATCH .../blocks, for pages and other containers) return these warnings for the fields they write; page, partial and batch writes return them for the whole tree. Both check the site’s own block types as well as core.