Skip to Content
MCPRecipesValidate a batch before applying

Validate a batch before applying

Goal. Add a contained group of nodes to an existing canvas — and delete one obsolete node — without moving, breaking, or overwriting anything else, and without losing a concurrent edit made by someone else while you were thinking.

Tools used. canvas_get, guide_search, guide_get, canvas_validate, canvas_apply_batch, open_canvas.

Requires. The full endpoint, and canvas:read plus canvas:write. No provider cost at any step — a batch never starts generation.

You finish holding. A new revision, exact handles for everything created, a post-write validation summary, and a review link.


The sequence

Read the canvas and keep the revision

{ "name": "canvas_get", "arguments": { "canvasId": "canvas:abc123:brand-refresh" } }

Check: you have the full node list and a revision — say rev_00291. That string is the thing that makes everything after this safe.

Compute the current bounding box from the node positions. You need it in the next step.

Read the layout and preservation rules

{ "name": "guide_get", "arguments": { "guideId": "sections-layout" } }

and, because this batch deletes something:

{ "name": "guide_get", "arguments": { "guideId": "existing-canvases" } }

The rules these encode, which the rest of this recipe follows:

  • Place a new Section to the right of the existing bounding box with at least 160 units of outer spacing. If that makes the canvas absurdly wide, place it below with the same spacing.
  • 48 units of inner Section padding, 32 between sibling nodes, 80 between major stages.
  • A Section is a text node with metadata.isSection: true, at least 200 × 160. Put the title in title and leave metadata.content empty.
  • “Add” never means “reorganise”. Do not move, resize, retitle, or reconnect an existing node.

Build the batch

Related creates and their connections belong in one batch. Use clientId and client: references to connect nodes that do not exist yet:

{ "canvasId": "canvas:abc123:brand-refresh", "baseRevision": "rev_00291", "idempotencyKey": "objections-section-2026-08-04-01", "operations": [ { "type": "node.create", "clientId": "objections-section", "node": { "type": "text", "title": "Customer objections", "position": { "x": 3200, "y": 200 }, "width": 1040, "height": 720, "metadata": { "isSection": true } } }, { "type": "node.create", "clientId": "objection-price", "node": { "type": "sticky", "title": "Price", "position": { "x": 3248, "y": 296 }, "width": 280, "height": 200, "metadata": { "content": "Too expensive versus the incumbent." } } }, { "type": "node.create", "clientId": "synthesis", "node": { "type": "text", "title": "What this means", "position": { "x": 3560, "y": 296 }, "width": 480, "height": 200, "metadata": { "content": "Lead with total cost of ownership, not unit price." } } }, { "type": "connection.create", "clientId": "price-to-synthesis", "from": "client:objection-price", "to": "client:synthesis" }, { "type": "node.delete", "nodeId": "node:old4x5" } ] }

Note the geometry: the Section starts at x: 3200, which is 160 units clear of the existing bounding box; children sit 48 units inside it; siblings are 32 apart. Each child’s centre falls inside the Section bounds, which is what makes it a child rather than a neighbour.

Dry-run it

Send the exact same canvasId, baseRevision, and operations to canvas_validate — no idempotencyKey, because validation never consumes one:

{ "name": "canvas_validate", "arguments": { "canvasId": "canvas:abc123:brand-refresh", "baseRevision": "rev_00291", "operations": [ "…the same array…" ] } }

The server runs the real operation engine with validateOnly set, builds the graph that would exist, and lints that. Nothing is persisted.

Check, in this order:

  1. destructiveImpact. This is the part to show a human.

    { "deletedNodeHandles": ["node:old4x5"], "deletedConnectionHandles": ["connection:z1z2"], "mediaNodeHandles": [], "generatedNodeHandles": [], "findings": [ { "code": "proposed_destructive_change", "severity": "warning", "message": "The proposed batch deletes node:old4x5 and 1 attached connection(s). Apply only after explicit user intent." } ] }

    Deleting a node takes its connections with it — the message says how many. A non-empty generatedNodeHandles means you are about to delete work that cost money to produce; that needs an explicit confirmation, not a summary line.

  2. summary.errors. Must be zero. An error means the batch would produce an invalid graph, and applying it will fail.

  3. summary.warnings. Read every one. A node_overlap here means your new nodes collide with existing ones — go back to step 3 and move them, do not move the existing nodes out of the way.

  4. proposal versus currentCanvas. The dry-run returns both. Diffing node counts is a cheap sanity check that you are adding what you think you are adding.

A 409 at this step means someone edited the canvas after you read it. Do not “fix” it by dropping baseRevision — that just removes the detection. Go back to step 1.

Get explicit intent for the deletion

The proposed batch deletes a node. That needs the user to say so in the current turn, in response to the specific impact you just read.

This batch adds a “Customer objections” Section with 2 nodes and 1 connection, to the right of your existing work — nothing existing moves. It also deletes node:old4x5 and 1 connection attached to it. No generated media is affected. Apply?

If the answer is no to the deletion but yes to the addition, that is a different payload and needs a new idempotency key.

Apply

{ "name": "canvas_apply_batch", "arguments": { "canvasId": "canvas:abc123:brand-refresh", "baseRevision": "rev_00291", "idempotencyKey": "objections-section-2026-08-04-01", "operations": [ "…the same array you validated…" ] } }

Atomic: if any operation is rejected, nothing is applied and the canvas is untouched. There is no partial graph to clean up.

Check: the response carries the new revision, the created handles, and a validation block. The server runs the graph validator on the post-write canvas automatically and returns it here — you do not need a separate canvas_validate call to see the outcome.

Read validation.summary.reviewRequired. A batch can be structurally valid and still leave a warning worth mentioning.

{ "name": "open_canvas", "arguments": { "canvasId": "canvas:abc123:brand-refresh" } }

Applied at revision rev_00292. Created Section node:s5t6 with node:u7v8 and node:w9x0 inside it, connected node:u7v8 → node:w9x0. Deleted node:old4x5 and connection:z1z2. Post-write validation: no errors, no warnings. Review it here: https://app.gavana.ai/… 

Exact handles are the audit trail. “I added a section” is not.


Handling a 409 properly

A 409 is not an error to retry past. It means a human did something while you were working, and their change is now in the graph.

Re-read

{ "name": "canvas_get", "arguments": { "canvasId": "canvas:abc123:brand-refresh" } }

You now have rev_00293 and a graph that has changed.

Look at what changed

Do not skip this. If someone just added a Section where you were about to put yours, your carefully-computed coordinates now collide. If someone deleted the node your batch wanted to delete, your delete operation will fail.

Rebase, preserving their change

Recompute the bounding box. Move your new nodes; never move theirs. Drop any operation that no longer makes sense.

Decide about the idempotency key

  • Payload byte-identical after rebasing? Reuse the same key.
  • Anything changed — one coordinate, one dropped operation? New key.

Reusing a key for a different payload is the one reliable way to get a confusing result.

Re-validate against the new revision

{ "name": "canvas_validate", "arguments": { "canvasId": "canvas:abc123:brand-refresh", "baseRevision": "rev_00293", "operations": [ "…rebased…" ] } }

Then apply. If you hit a second 409, someone is actively working in that canvas — say so and stop rather than racing them.

The eight operation types

typeRequiredOptional
canvas.update—title, backgroundMode (dots | lines | blank), showImageInfo, viewport
node.createnodeclientId
node.updatenodeId, patch—
node.movenodeId, position—
node.resizenodeId, width, height—
node.deletenodeId—
connection.createconnection, or both from and toclientId, mode
connection.deleteconnectionId—

Nodes are image, video, text, or sticky, sized 40–10,000 on each axis. Connection modes are list, reference, first-frame, last-frame, or omitted for a plain dependency flow. Maximum 200 operations per batch, minimum 1.

Full field schemas are on the Canvases API resource.

Things the engine will reject

AttemptResult
Writing image or video content into metadata.contentRejected. Media is server-owned — use save_image_to_canvas or a generation tool.
Marking a non-text node as a Section, or one smaller than 200 × 160malformed_section error.
A width or height outside 40–10,000Rejected.
A first-frame connection that is not image → videounclear_connection error.
Two first-frame connections into one videounclear_connection error.
More than 200 operationsInput validation error. Split the batch.

Common mistakes

MistakeWhy it hurts
Skipping the dry-run on a deleteYou find out what a deletion takes with it after it is gone. destructiveImpact tells you first, for free.
Omitting baseRevision to avoid 409sYou have not avoided the conflict, only the detection. Someone’s work gets silently overwritten.
Creating nodes in one batch and connecting them in anotherThe first batch leaves disconnected nodes, and the second can fail — leaving them that way.
Stretching a wide text node as a headingfake_section_header warning. Use a real Section.
Reusing an idempotency key after rebasingDifferent payload, same key. Unpredictable.
Moving existing nodes to make room“Add” is not “reorganise”. Place your work clear of theirs.
Reporting success from the apply status aloneRead the returned validation block. It is right there in the same response.
Last updated on