Run a canvas workflow
Goal. Build a reusable image Recipe once, with the product shot pinned as a default, then run it repeatedly changing only the background description.
Tools used. save_image_to_canvas, create_canvas_workflow, run_canvas_workflow, get_canvas_workflow_run, open_canvas.
Requires. The full endpoint. Building needs canvas:read and canvas:write. Running additionally needs asset:read, image:generate, and job:manage.
run_canvas_workflow can incur provider cost. Building the workflow is free; running it is not. Get explicit approval in the current turn before the run, and never re-call it after a failure or a timeout.
Part 1 — Build it (free)
Save the fixed images first
Anything that stays the same on every run should be a durable node before the workflow exists. See Save a generated image.
{
"name": "save_image_to_canvas",
"arguments": {
"imageUrl": "https://example.com/product-hero.png",
"destination": "canvas:abc123:product-shots",
"title": "Product hero — locked",
"idempotencyKey": "product-hero-2026-08-04-01"
}
}Keep the returned handle — say node:img8q4 — and the canvas handle.
Getting the order wrong here is the mistake that costs the most later. A workflow built without pinned defaults makes every single run re-supply the product shot.
Create the workflow
{
"name": "create_canvas_workflow",
"arguments": {
"destination": "canvas:abc123:product-shots",
"name": "Product on seasonal backgrounds",
"description": "Same product shot, new background per run.",
"category": "Product Visualization",
"tags": ["product", "seasonal"],
"inputs": [
{
"key": "product",
"label": "Product shot",
"description": "Locked. Do not substitute.",
"type": "image",
"sourceNodeId": "node:img8q4",
"useAsDefault": true
},
{
"key": "background",
"label": "Background direction",
"description": "Surface, colour, and light for this run.",
"type": "text",
"value": "matte cream, soft window light",
"useAsDefault": false
}
],
"outputs": [
{
"key": "hero",
"label": "Hero image",
"type": "image",
"prompt": "Place the exact connected product on the described background. Preserve the product precisely — do not substitute, restyle, or redraw it.",
"inputKeys": ["product", "background"],
"size": "1024x1024"
}
],
"idempotencyKey": "seasonal-workflow-2026-08-04-01"
}
}Three things to notice:
useAsDefault: trueonproductis what lets later runs omit it entirely.- Image inputs use
sourceNodeId, nevervalue. The schema is strict — an image input carryingvalueis rejected outright. - The output prompt says “preserve the exact connected product”. Every product-variant workflow should say this explicitly; otherwise the model treats the product as a suggestion.
category must be one of Brand & Visual Design, Product Visualization, Marketing & Ads, or Content Package. Limits: 1–16 inputs, 1–8 outputs, and every inputKeys entry must name a key you actually declared.
Check: the response gives you a workflowNodeId and a browser url.
Keep workflowNodeId. It is the only value run_canvas_workflow accepts. And note what just happened: a workflow now exists and nothing has generated. That separation is the whole point — you can build this on request without spending anything.
Stop
Building is done. Unless the user asked to run it in this message, the turn ends here:
Created “Product on seasonal backgrounds” as
node:wf7k2moncanvas:abc123:product-shots. The product shot is pinned as a default, so a run only needs a background direction. I have not run it — running generates an image and uses your provider. Want me to?
Part 2 — Run it (paid)
Get explicit approval
The user must ask to run it, in the current message. Not “build me a workflow”, not an approval from three turns ago, not a node labelled “hero image” sitting empty.
Name what it will do before asking:
This will generate 1 image using your connected provider. Run it?
Run with only what changes
{
"name": "run_canvas_workflow",
"arguments": {
"destination": "canvas:abc123:product-shots",
"workflowNodeId": "node:wf7k2m",
"inputs": { "background": "matte terracotta, low autumn sun, long soft shadows" },
"idempotencyKey": "terracotta-run-2026-08-04-01",
"waitSeconds": 45
}
}product is absent because it is a stored default. inputs accepts up to 16 entries, each value 1–8,000 characters, keyed by stable input key, exact portNodeId, or input label. Image values must be node: or asset: handles.
destination must be the canvas the workflow card lives on.
Check the response for status. One of two things happened.
If it finished
You get the completed run with its outputs. Image outputs are resolved to their canvas nodes and come back with previews the model can inspect. Note the output node handles.
Skip to the last step.
If it timed out
You get the queued run back, with its status, the destination, and the workflowNodeId.
This is not a failure, and it is the single most dangerous moment in the recipe. Do not call run_canvas_workflow again. That starts a second paid run. The first one is still going.
Poll instead:
{ "name": "get_canvas_workflow_run", "arguments": { "runId": "run:9f3a2b" } }Polling is free and needs only canvas:read and job:manage. Call it again while the status is queued or running, with a sensible gap between calls.
waitSeconds accepts 1–50 and defaults to 45. Raising it does not make the work faster; it only changes how long the tool blocks before handing you a handle.
Verify the result is durable
A run reporting success is not the same as durable canvas content. Confirm the output landed:
{ "name": "canvas_get", "arguments": { "canvasId": "canvas:abc123:product-shots" } }Check: the output node exists, is of type image, and carries server-owned media rather than being an empty placeholder. Report what the server actually returned — do not describe a temporary handle as a finished canvas output.
Report and link
{ "name": "open_canvas", "arguments": { "canvasId": "canvas:abc123:product-shots" } }Run
run:9f3a2bsucceeded. Output image isnode:out5y6oncanvas:abc123:product-shots. Review it here: https://app.gavana.ai/…
Running it again
Each subsequent run needs a new idempotency key — different inputs, different intent, different key:
{
"name": "run_canvas_workflow",
"arguments": {
"destination": "canvas:abc123:product-shots",
"workflowNodeId": "node:wf7k2m",
"inputs": { "background": "wet slate, blue hour, hard rim light" },
"idempotencyKey": "slate-run-2026-08-04-02"
}
}And each run needs its own approval. One yes is one run.
When it fails
| Symptom | Cause | Fix |
|---|---|---|
400 “not a reusable workflow card on this canvas” | workflowNodeId is a plain node, or destination points at a different canvas | Point destination at the canvas holding the card. |
409 “missing its Recipe identity” | The card lost its Recipe provenance | Recreate the workflow with create_canvas_workflow. Do not retry the run. |
| Strict schema rejection on create | An unrecognised key, or an image input carrying value | Images use sourceNodeId. value is for text and sticky only. |
inputKeys references an unknown key | The output names an input you did not declare | Fix the workflow definition before building the card. |
Returns while still queued | Exceeded waitSeconds | Poll get_canvas_workflow_run. Never re-run. |
400 on get_canvas_workflow_run | The handle is not a workflow run — probably an image or video job | Use the run: handle the workflow returned. |
403 insufficient_scope | Running needs five scopes | Most often the missing one is job:manage, without which you can start work but not observe it. |
| Terminal provider failure | The provider rejected or failed the job | Report it and stop. Ask for fresh intent before any new attempt. |
Common mistakes
| Mistake | Why it hurts |
|---|---|
| Running because you just built it | Building is not authorisation. Two separate decisions. |
Re-calling run_canvas_workflow after a timeout | A second paid run. The most expensive mistake on this page. |
| Creating the workflow before saving the fixed images | No node handles to pin, so every run re-supplies the product. |
| Omitting “preserve the exact connected product” from the prompt | The model substitutes a plausible product. |
| Reusing one idempotency key across runs | One key, one intended payload. |
Raising waitSeconds to avoid polling | It caps at 50 and does not speed anything up. |
| Reporting a queued run as a finished image | Verify against the canvas before claiming durability. |