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 fromcanvas_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.
Finish with a link
{ "name": "open_canvas", "arguments": { "canvasId": "canvas:abc123:product-shots" } }Saved as
node:img8q4oncanvas: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
| Constraint | Value |
|---|---|
imageUrl scheme | HTTPS only. Plain http:// is rejected. |
| URL length | 4,096 characters maximum |
file_id length | 1–1,000 characters |
title | 1–160 characters |
idempotencyKey | 8–200 characters, required |
x, y | −100,000 to 100,000, both optional |
| Source image size | 50 MB ceiling on what the server will read |
| Accepted formats | PNG, JPEG, WebP, GIF — verified by sniffing the bytes, not by trusting the extension |
When it fails
| Symptom | Cause | Fix |
|---|---|---|
| Validation error, “Provide an image file or imageUrl” | Neither parameter supplied | Send one of them. |
Validation error on imageUrl | Not HTTPS, or over 4,096 characters | Use a real HTTPS URL. |
403 insufficient_scope | Almost always the missing scope is image:generate | Reconnect or recreate the token with all four scopes. |
| Fetch or format failure | The URL 404s, needs auth, or serves something that is not a raster image | Fix the URL. Retrying an unreachable URL will not help. |
413 on a later get_canvas_image | The image is large | The node saved fine. Use the full-resolution link instead of the inline preview. |
502 “did not return a destination canvas handle” | Server-side | Report the X-Request-ID. Do not loop. |
Common mistakes
| Mistake | Why it hurts |
|---|---|
Saving to agent-canvas, then passing a different canvas as a video destination | The frame is not on that canvas. Save where you will generate. |
| Discarding the returned node handle | Everything downstream needs it. |
| Leaving the default title | An untraceable canvas a week later. |
| Reusing one idempotency key for several different images | One key, one intended payload. Different image, different key. |
Assuming canvas:write is enough | image:generate is what governs writing image bytes. |
| Reporting the save as durable without re-reading | Confirm the node carries stored content before building on it. |