Skip to Content
AgentsRecipesBatch edits with canvas_apply_batch

Batch Edits

When a change touches more than one node, do it as one batch. A batch is atomic: either every operation applies or none does, so a malformed operation can never leave half a workflow on the canvas.

Batches are free. They cost no AI credits, however many nodes they create.

Scopes. canvas:read to validate; canvas:read and canvas:write to apply.

Call sequence

canvas_get → canvas_validate (proposed) → canvas_apply_batch → canvas_validate (current)

On the CLI:

canvas get → canvas apply --file batch.json → canvas get

Operation types

TypeRequired fields
canvas.updateCanvas-level fields
node.createnode; optional clientId
node.updatenodeId, patch
node.movenodeId, position
node.resizenodeId, width, height
node.deletenodeId
connection.createEither connection, or from and to; optional clientId and mode
connection.deleteconnectionId

A batch holds 1 to 200 operations.

Node types are text, sticky, image, and video. Width and height range from 40 to 10,000. Node titles are at most 160 characters, and metadata.content at most 8,000. Serialized metadata must stay under 64 KiB.

Batch-local references

Within a batch you can reference a node you are creating in the same batch. Give the node.create a clientId, then use client:THAT_ID as the from or to of a connection.create.

{ "type": "connection.create", "clientId": "brief-to-generator", "from": "client:brief", "to": "client:generator" }

clientId values are 1–100 characters of letters, digits, underscores, and dashes, with an optional client: prefix. This is what lets you build a whole connected workflow in one atomic write instead of a create-then-connect sequence that can half-fail.

Steps

Read the canvas

gavana canvas get canvas:OWNER_UID:CANVAS_ID --pretty

Check. Note three things:

  • The revision — this is your baseRevision.
  • The existing node handles you will reference.
  • The current visible bounding box. New work goes outside it: to the right with at least 160 units of outer spacing, or below with the same spacing if the canvas would otherwise get unreadably wide.

Compose the batch

{ "baseRevision": "REVISION_FROM_STEP_1", "idempotencyKey": "research-cluster-2026-08-04-a", "operations": [ { "type": "node.create", "clientId": "objections-section", "node": { "type": "text", "title": "Customer objections", "position": { "x": 2400, "y": 200 }, "width": 1040, "height": 720, "metadata": { "isSection": true } } }, { "type": "node.create", "clientId": "objection-price", "node": { "type": "sticky", "title": "Price hesitation", "position": { "x": 2448, "y": 300 }, "width": 280, "height": 200, "metadata": { "content": "Buyers stall at the price reveal on mobile." } } }, { "type": "node.create", "clientId": "objection-trust", "node": { "type": "sticky", "title": "Trust gap", "position": { "x": 2760, "y": 300 }, "width": 280, "height": 200, "metadata": { "content": "No social proof above the fold." } } }, { "type": "node.create", "clientId": "synthesis", "node": { "type": "text", "title": "Synthesis", "position": { "x": 2448, "y": 560 }, "width": 592, "height": 260, "metadata": { "content": "Both objections resolve on the same screen." } } }, { "type": "connection.create", "clientId": "price-to-synthesis", "from": "client:objection-price", "to": "client:synthesis" }, { "type": "connection.create", "clientId": "trust-to-synthesis", "from": "client:objection-trust", "to": "client:synthesis" } ] }

Layout rules worth following, because validation checks some of them and reviewers notice the rest:

  • Sections are text nodes with metadata.isSection: true, at least 200 by 160. The title goes in title; leave metadata.content empty unless a description was asked for.
  • Place each child so its centre lies inside its Section’s bounds. A Section may overlap its children by design; ordinary nodes should not overlap each other.
  • 48 units of inner Section padding, 32 between siblings, at least 80 between major stages.
  • Keep workflow direction consistent — normally left to right, inputs before transformations, outputs after.
  • Never connect a Section as if it were a workflow step.

Validate before applying

Validation runs the canonical operation engine and the graph validator without writing, without consuming an idempotency key, and without requiring canvas:write.

MCP: canvas_validate with the canvas id and the proposed operations.

API / CLI passthrough, on a canvas you own:

gavana api POST /canvases/CANVAS_ID/operations --file ./validate.json

where validate.json is your batch with "validateOnly": true and no baseRevision or idempotencyKey.

Check. Read summary.passed and summary.reviewRequired, then read the findings. Each finding names exact handles and links a guideUri. Resolve errors. Explain any warning you decide to keep rather than silently accepting it.

The CLI has no dedicated validate command, and the raw passthrough cannot set the ownerUid query parameter on a POST — so for a shared canvas, validate through MCP canvas_validate or a direct API call instead.

Check the destructive impact

If your batch contains any node.delete or connection.delete, validate it first and read the proposed_destructive_change findings. They report, per deleted node, how many attached connections go with it, and separately flag media nodes and generated outputs.

Deletion is only ever appropriate when the user explicitly asked for that scope.

Apply once

gavana canvas apply canvas:OWNER_UID:CANVAS_ID --file ./batch.json

For a batch containing any delete, --yes is required:

gavana canvas apply canvas:OWNER_UID:CANVAS_ID --file ./cleanup.json --yes

You can also pass operations inline:

gavana canvas apply canvas:OWNER_UID:CANVAS_ID \ --operations '[{"type":"node.move","nodeId":"node:NODE_ID","position":{"x":900,"y":240}}]' \ --base-revision REVISION \ --idempotency-key move-hero-a

If you omit --base-revision the CLI reads the canvas for you; if you omit --idempotency-key it generates a UUID. Supply both explicitly for anything you might need to retry — a generated key is not stable across attempts.

Handle a conflict correctly

A 409 (CLI exit 4) means someone changed the canvas between your read and your write. The response includes the current revision.

The wrong fix is to re-send the same operations with the newer revision. That is exactly the overwrite the check exists to prevent.

The right fix:

  1. Read the canvas again.
  2. Look at what changed.
  3. Rebase your intended addition around the newer graph, preserving the concurrent user change.
  4. If the payload genuinely did not change, reuse the same idempotency key. If you rebased, that is a new payload — use a new key.

Verify

gavana canvas get canvas:OWNER_UID:CANVAS_ID --jq '.canvas.revision' -r

Then validate the current graph again and report the exact created handles.

What you end up with

  • A new canvas revision
  • The exact node: and connection: handles created — these are your audit trail
  • A validation result showing no new structural errors

Single-command shortcuts

For a one-node change, the node and connection commands do the same thing with less ceremony. Each is applied as a single-operation atomic batch, with the revision read for you:

gavana node create canvas:OWNER_UID:CANVAS_ID --type sticky --title "Observation" --content "..." --x 2448 --y 300 gavana node move canvas:OWNER_UID:CANVAS_ID node:NODE_ID --x 900 --y 240 gavana connection create canvas:OWNER_UID:CANVAS_ID --from node:A --to node:B gavana node delete canvas:OWNER_UID:CANVAS_ID node:NODE_ID --yes

Use a batch when the operations belong together — a Section and its children, a node and the connection that gives it meaning. Partial application of those is worse than no application.

Common mistakes

MistakeWhat happens
Creating every node at (0, 0)Everything stacks. Validation reports node_overlap
A wide, short text node used as a headingValidation reports fake_section_header. Use a real Section
Connecting a Section to workflow nodesValidation warns unclear_connection
A first-frame edge from a non-image, or to a non-videoValidation error. Frame modes connect an image to a video
Two first-frame edges into one video nodeValidation error. One of each per video node
Writing an image URL into metadata.contentRejected. Media content is server-owned — import or generate instead
Reorganising existing work when asked to addOff contract. “Add” never means “reorganise”
Re-sending after a 409 with a fresher revisionOverwrites the concurrent user change
Last updated on