Skip to Content
MCPRecipesRun a canvas workflow

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: true on product is what lets later runs omit it entirely.
  • Image inputs use sourceNodeId, never value. The schema is strict — an image input carrying value is 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:wf7k2m on canvas: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.

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

Run run:9f3a2b succeeded. Output image is node:out5y6 on canvas: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

SymptomCauseFix
400 “not a reusable workflow card on this canvas”workflowNodeId is a plain node, or destination points at a different canvasPoint destination at the canvas holding the card.
409 “missing its Recipe identity”The card lost its Recipe provenanceRecreate the workflow with create_canvas_workflow. Do not retry the run.
Strict schema rejection on createAn unrecognised key, or an image input carrying valueImages use sourceNodeId. value is for text and sticky only.
inputKeys references an unknown keyThe output names an input you did not declareFix the workflow definition before building the card.
Returns while still queuedExceeded waitSecondsPoll get_canvas_workflow_run. Never re-run.
400 on get_canvas_workflow_runThe handle is not a workflow run — probably an image or video jobUse the run: handle the workflow returned.
403 insufficient_scopeRunning needs five scopesMost often the missing one is job:manage, without which you can start work but not observe it.
Terminal provider failureThe provider rejected or failed the jobReport it and stop. Ask for fresh intent before any new attempt.

Common mistakes

MistakeWhy it hurts
Running because you just built itBuilding is not authorisation. Two separate decisions.
Re-calling run_canvas_workflow after a timeoutA second paid run. The most expensive mistake on this page.
Creating the workflow before saving the fixed imagesNo node handles to pin, so every run re-supplies the product.
Omitting “preserve the exact connected product” from the promptThe model substitutes a plausible product.
Reusing one idempotency key across runsOne key, one intended payload.
Raising waitSeconds to avoid pollingIt caps at 50 and does not speed anything up.
Reporting a queued run as a finished imageVerify against the canvas before claiming durability.
Last updated on