Skip to Content
MCPRecipesSave a generated image

Save a generated image

Goal. An image exists in the conversation — ChatGPT just made it, or it sits at a public URL. Make it a real thing in Gavana with a handle other tools accept.

Tools used. save_image_to_canvas, then canvas_get and get_canvas_image to verify, and open_canvas to finish.

Requires. The full endpoint, and four scopes: canvas:read, canvas:write, asset:read, and image:generate.

Costs nothing. Saving is not generating. save_image_to_canvas is not one of the three paid tools.

You finish holding. A node: handle and a canvas: handle — which is exactly what generate_video wants for a firstFrame, and what create_canvas_workflow wants for a pinned default.

This tool needs image:generate even though it generates nothing. That scope governs writing image bytes into Gavana storage, not just producing them. A token with canvas:write but no image:generate returns insufficient_scope here, which is surprising unless you know why.


The sequence

Choose a destination

Two options, and the difference matters:

  • agent-canvas — your persistent Agent Canvas, created automatically if it does not exist yet. This is the right default when the image has no home, and it is what the parameter defaults to.
  • An exact canvas: handle — when the image belongs with existing work. Get the handle from canvas_list; do not assemble one.

If you plan to use this image as a video first frame, save it to the same canvas you will pass as the video’s destination. Mismatching the two is the most common failure in the video flow.

Save it

With a client-supplied file — the preferred path when your client has one:

{ "name": "save_image_to_canvas", "arguments": { "image": { "download_url": "https://…client-file-url…", "file_id": "file-abc123", "mime_type": "image/png", "file_name": "hero-draft.png" }, "destination": "canvas:abc123:product-shots", "title": "Hero draft — warm editorial", "idempotencyKey": "hero-draft-2026-08-04-01" } }

Or with a public HTTPS URL:

{ "name": "save_image_to_canvas", "arguments": { "imageUrl": "https://example.com/hero-draft.png", "destination": "canvas:abc123:product-shots", "title": "Hero draft — warm editorial", "idempotencyKey": "hero-draft-2026-08-04-01" } }

Supply one or the other. Supplying neither fails validation with “Provide an image file or imageUrl.”

Set title to something meaningful. It defaults to Image from ChatGPT, and a canvas full of nodes with that title is unusable a week later.

You can pass explicit x and y (each between −100,000 and 100,000) to place the node, or omit them and let Gavana choose.

Check: the response gives you a node: handle and the resolved canvas: handle. Keep both.

Do not just note “the image was saved”. The handle is the deliverable — every downstream tool takes it and nothing else. An agent that reports success without capturing the handle has to re-read the canvas to find it.

Confirm it is really there

Saving returns quickly. Confirm the node exists on the canvas with real content before you build anything on top of it:

{ "name": "canvas_get", "arguments": { "canvasId": "canvas:abc123:product-shots" } }

Check: your node is present, is of type image, and carries server-owned media fields rather than being an empty placeholder.

To confirm the pixels are what you expected, pull it back:

{ "name": "get_canvas_image", "arguments": { "canvasId": "canvas:abc123:product-shots", "nodeId": "node:img8q4" } }

That returns an inline preview the model can inspect. A 409 here means the node exists but has no stored result — do not treat the save as complete.

{ "name": "open_canvas", "arguments": { "canvasId": "canvas:abc123:product-shots" } }

Saved as node:img8q4 on canvas:abc123:product-shots, titled “Hero draft — warm editorial”. Confirmed on the canvas with stored image content. See it here: https://app.gavana.ai/… 


What the handle unlocks

This is why the recipe exists. The handle is an input to two other flows:

As a video first frame. A video cannot start from an image that only exists in a chat.

{ "name": "generate_video", "arguments": { "model": "model:seedance-2-pro", "prompt": "Slow push in, warm afternoon light.", "destination": "canvas:abc123:product-shots", "firstFrame": "node:img8q4", "idempotencyKey": "hero-video-2026-08-04-01" } }

The destination must be the canvas the node lives on. generate_video is a paid tool — get explicit current-turn approval first, and call find_video_models for a real model: handle rather than guessing.

As a pinned workflow default. In create_canvas_workflow, an image-typed input takes sourceNodeId and useAsDefault:

{ "key": "product", "label": "Product shot", "type": "image", "sourceNodeId": "node:img8q4", "useAsDefault": true }

Later runs then omit product entirely and pass only what changes. Image inputs must use sourceNodeId — the value field is for text and sticky inputs only, and an image input carrying value fails the strict schema.

Constraints worth knowing before you call

ConstraintValue
imageUrl schemeHTTPS only. Plain http:// is rejected.
URL length4,096 characters maximum
file_id length1–1,000 characters
title1–160 characters
idempotencyKey8–200 characters, required
x, y−100,000 to 100,000, both optional
Source image size50 MB ceiling on what the server will read
Accepted formatsPNG, JPEG, WebP, GIF — verified by sniffing the bytes, not by trusting the extension

When it fails

SymptomCauseFix
Validation error, “Provide an image file or imageUrl”Neither parameter suppliedSend one of them.
Validation error on imageUrlNot HTTPS, or over 4,096 charactersUse a real HTTPS URL.
403 insufficient_scopeAlmost always the missing scope is image:generateReconnect or recreate the token with all four scopes.
Fetch or format failureThe URL 404s, needs auth, or serves something that is not a raster imageFix the URL. Retrying an unreachable URL will not help.
413 on a later get_canvas_imageThe image is largeThe node saved fine. Use the full-resolution link instead of the inline preview.
502 “did not return a destination canvas handle”Server-sideReport the X-Request-ID. Do not loop.

Common mistakes

MistakeWhy it hurts
Saving to agent-canvas, then passing a different canvas as a video destinationThe frame is not on that canvas. Save where you will generate.
Discarding the returned node handleEverything downstream needs it.
Leaving the default titleAn untraceable canvas a week later.
Reusing one idempotency key for several different imagesOne key, one intended payload. Different image, different key.
Assuming canvas:write is enoughimage:generate is what governs writing image bytes.
Reporting the save as durable without re-readingConfirm the node carries stored content before building on it.
Last updated on