AI Agents
AI agents that use your app, such as ChatGPT's agent or Claude in Chrome, can build and edit designs in the builder without dragging blocks. The builder gives them a JSON API: they read the design, change it, and load it back. Your users see the result on the canvas and can undo it like any other change.
It's on by default, and you can turn it off.
Builder version 1.519.0 or later. Use version: 'latest' or pin a newer version. A builder loaded without a version uses the stable version, which doesn't include this yet.
unlayer.init({
id: 'editor',
version: 'latest',
});
How agents use it
There are two ways in. Both do the same thing.
WebMCP tools
Some agents can't run code in your page. They use WebMCP, a browser feature that lets a page offer tools to agents. The builder registers these tools on your page:
| Tool | What it does |
|---|---|
unlayer_describe_editor | Explains how to edit the design and lists what this builder supports: content types, custom tools, merge tags and fonts. Agents call this first. |
unlayer_get_design_schema | Returns the JSON Schema of the design, including your custom tools. |
unlayer_get_design | Returns the current design. |
unlayer_load_design | Checks a design and loads it if it's valid. |
unlayer_preview | Opens the preview at a device size (desktop, tablet or mobile), or closes it with none. |
unlayer_export_html | Exports the current design as HTML. |
WebMCP is new, so browser support is still limited:
-
Chrome and Edge offer it through an origin trial. Register your own domain on the Chrome Origin Trials page (search for "WebMCP") or the Edge origin trial, then add the token to your pages:
<meta http-equiv="origin-trial" content="YOUR_TOKEN" />To try it locally, turn on
chrome://flags/#enable-webmcp-testingand restart Chrome. -
Some agent browsers, such as ChatGPT's, support WebMCP on their own.
-
Firefox and Safari don't support it.
In a browser without WebMCP, the builder skips the tools. Nothing else changes.
JavaScript: unlayer.agent
Agents that can run code in your page use unlayer.agent directly. It works in every browser.
// 1. Learn how to edit designs in this builder.
const { guide, contentTypes, customTools } = await unlayer.agent.describe();
// 2. Read the current design.
const design = await unlayer.agent.getDesign();
// 3. Change the design JSON. This adds a row with a heading and a button.
// Every new block needs an id that isn't already in the design.
const newId = (name) => `${name}-${crypto.randomUUID().slice(0, 8)}`;
design.body.rows.push({
id: newId('sale-row'),
cells: [1],
columns: [
{
id: newId('sale-column'),
contents: [
{
id: newId('sale-heading'),
type: 'heading',
values: { headingType: 'h1', text: 'Fall sale: 20% off' },
},
{
id: newId('sale-button'),
type: 'button',
values: {
text: 'Shop the sale',
href: {
name: 'web',
values: { href: 'https://example.com/sale', target: '_blank' },
},
},
},
],
values: {},
},
],
values: {},
});
// 4. Load it back.
const result = await unlayer.agent.loadDesign(design);
if (!result.ok) {
// Nothing changed. result.errors lists what to fix: [{ path, message, code }]
}
The guide from describe() explains the design format in full. schema() returns its JSON Schema, and the Design object page describes it too. Some types need more than this example shows: a paragraph, for example, stores its text in values.textJson.
| Method | What it does |
|---|---|
describe() | Returns a guide written for agents, plus what this builder supports. Call it first. |
schema() | Returns the JSON Schema of the design, including your custom tools. |
getDesign() | Returns the current design. |
loadDesign(design) | Checks a design and loads it if it's valid. |
preview(device) | Opens the preview at 'desktop', 'tablet' or 'mobile'. null closes it. |
exportHtml(options) | Exports the current design. Takes the same options as unlayer.exportHtml. |
All methods return promises, and they can fail with an error. That happens when no builder on the page offers the API, when the builder isn't ready in time, or when it doesn't answer within 15 seconds (60 seconds for getDesign, loadDesign and exportHtml). loadDesign is the exception while the builder loads: it returns ok: false with the code not_ready instead.
What happens when an agent loads a design
- It's checked first. If something is wrong, the design doesn't change, and the agent gets a list of errors. Each error names the JSON path to fix.
- It's one undo step. A valid design replaces the current one, and your users can undo it like any other change. It also replaces changes made after the agent read the design, so agents should read the design again right before changing it.
- Protected content stays. In
livedesign mode (the default), content your template permissions protect keeps its current value. Protected means locked, or set so it can't be edited, deleted or moved. The result lists what was kept inprotectedElements. - Your autosave sees it. The builder fires
design:updatedwith the typeagent:modified. - The audit runs. The result includes the builder's checks, such as missing alt text or empty links.
A successful load returns:
{
ok: true,
loaded: true,
errors: [],
errorCount: 0,
protectedElements: [],
audit: { status: 'PASS', issues: [], issueCount: 0 },
}
When ok is false, nothing changed. Each error in errors has a path, a message and a code:
| Code | Meaning |
|---|---|
invalid_type and other format codes | The design doesn't match the format at path, such as a paragraph without values.textJson. |
unknown_type | The design adds a block this builder doesn't offer, or one you turned off. |
design_changed | A different design was opened after the agent read it. The agent should read the design again. |
protected_content | The change can't be applied without changing content your template protects. Ordinary edits to protected content don't cause this: they're kept as they were, and the load succeeds. A new block that reuses an id already in the design also returns this code. |
not_ready | Only from loadDesign: the builder was still loading. Try again. |
Several builders on one page
describe() lists the builders that offer the API in editors, each with its index and the containerId of the element it's in. Builders with agentApi: false aren't listed, so the indexes only count the others.
To pick a builder, pass its index in the last argument. The first builder is the default:
const { editors } = await unlayer.agent.describe();
// [{ index: 0, containerId: 'editor' }, { index: 1, containerId: 'newsletter' }]
const design = await unlayer.agent.getDesign({ editor: 1 });
await unlayer.agent.loadDesign(design, { editor: 1 });
await unlayer.agent.preview('mobile', { editor: 1 });
WebMCP tools take an editor field with the same index, for example { "editor": 1 }.
Turn it off
unlayer.init({
id: 'editor',
agentApi: false,
});
With agentApi: false, this builder is left out of unlayer.agent and the WebMCP tools. unlayer.agent itself still exists on the page, so don't check for it to see whether the API is on. Call describe() instead: it fails with an error when no builder on the page offers the API, and otherwise lists the builders that do in editors.
How agents find it
The builder's iframe gets an accessible name that points agents to the API:
Design editor. AI agents: skip the UI, call unlayer.agent.describe() or unlayer_describe_editor
Nothing changes on screen. Screen readers announce this name too.
Related
- Unlayer MCP is for agents that work outside your app, such as Claude Desktop or coding agents, with the templates in your Unlayer account.
- Load and save designs covers
loadDesignandsaveDesignfor your own code.