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:
- Block library: built-in blocks in detail, structured Page fields, repeaters, templates and site search.
- Responsive settings and presentation: breakpoints, fluid type, responsive defaults and presentation controls.
- Block parts and site block types: styling parts of a block and how site block types work.
- Layouts, navigation and cards: menus, cards, starting layouts, gradients, footers, navigation inputs and migration design controls.
- Block changes by Core version: when each change arrived.
The two verb families
Section titled “The two verb families”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.
Block containers
Section titled “Block containers”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"}Reorganising blocks
Section titled “Reorganising blocks”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.
Reference
Section titled “Reference”Block instance tools
Section titled “Block instance tools”| 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.
Compact write answers
Section titled “Compact write answers”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=summaryPATCH /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=summaryPOST /api/v1/sites/{siteId}/containers/{kind}/{id}/blocks/duplicate?response=summaryPOST /api/v1/sites/{siteId}/containers/{kind}/{id}/blocks/responsive?response=summaryThe 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.
Headings with an eyebrow and subtitle
Section titled “Headings with an eyebrow and subtitle”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.
Classes and anchors
Section titled “Classes and anchors”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.
Text blocks: paragraph styles
Section titled “Text blocks: paragraph styles”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.
BlockType tools
Section titled “BlockType tools”| 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 |
Page identity in built markup
Section titled “Page identity in built markup”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.
Write warnings
Section titled “Write warnings”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).
Unknown data fields
Section titled “Unknown data fields”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_listorcore/gallery, a field of the item block (item_block, or the alias default) belongs initem_overrides.<key>. - A style override name (
custom_class,html_id, …) belongs instyle_overrides. - Otherwise the closest field names of the type, e.g.
levelforlevle.
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.
Removing a key
Section titled “Removing a key”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.
Field types
Section titled “Field types”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.