Troubleshooting
Gavana’s failures are designed to be readable. Start with the exit code, then the error code, then this page.
First: run doctor
gavana doctor --output humanFour checks, exit 0 only if all pass:
| Check | Fails when | Fix |
|---|---|---|
node | Node.js major version is below 20 | Install Node.js 20 or newer |
credentials | No token in the config file, environment, or command line | gavana auth login, or set GAVANA_AGENT_TOKEN |
base_url | Not HTTPS, and not HTTP on a loopback address | Use https://app.gavana.ai, or http://localhost:3000 for local work |
authentication | Gavana rejected the credential | Read the reported code and requestId, then see below |
Then confirm what the credential can actually do:
gavana auth status --jq '.scopes[]' -rHalf the “the agent cannot do X” reports are a missing scope, visible in one command.
Exit codes
| Exit | Meaning | First thing to check |
|---|---|---|
0 | Success | — |
1 | Any other failure, including a failed doctor | The stderr error object |
2 | Usage, configuration, or validation error | The fields array — it names what to fix |
3 | Authentication or authorization error | Token validity, then scopes |
4 | Revision or idempotency conflict | Re-read the canvas |
5 | Not found | The handle, and whether a Run or Job record expired |
7 | Network error | Connectivity and the base URL |
8 | Wait timeout — the work is still running | Resume with run wait or job wait |
9 | A waited Run or Job ended failed, canceled, or expired | The failure.code on stdout |
Exit 8 and exit 9 are different failures. 8 means you stopped waiting — the work continues and the handle is still good. 9 means the work itself reached a terminal failure. Never treat 8 as a reason to start a second paid run.
Error codes
Errors are JSON on stderr. The code is stable; match on it rather than on the message.
| Code | Source | Meaning |
|---|---|---|
usage | Local | The command is malformed. Nothing was sent |
configuration | Local | Missing token, missing or invalid base URL, or a runtime without fetch |
validation / input_validation_error | Server 400/422 | The request was understood and rejected. Read fields |
unauthorized | Server 401 | The credential is not valid |
forbidden | Server 403 | Valid credential, insufficient scope |
not_found | Server 404 | No such handle, or the record expired |
conflict | Server 409 | Revision or idempotency conflict |
rate_limited | Server 429 | Too many requests |
upstream | Server 5xx | A Gavana or provider-side failure |
network | Local | Gavana could not be reached |
timeout | Local | The request or the wait timed out |
canceled | Local | The wait was aborted |
invalid_response | Local | Gavana returned something unparseable |
internal | Local | An unexpected client-side failure |
Authentication and permissions
unauthorized — “Gavana could not verify this agent token”
The token does not exist, was revoked, or was mistyped. Create a new one and reconfigure the client.
“This agent token expired. Create a new token in Gavana.”
Tokens default to Never expiry. This message applies only when a token was created with a custom 1–90 day expiry; check Personal Access Tokens and create a replacement.
“The delegated Firebase session was revoked. Create a new agent token.”
The Gavana account’s own session was invalidated — usually by signing out everywhere or a password change. Every token created from that session stops working. Sign in again and create new tokens.
forbidden — the token is valid but lacks the scope
gavana auth status --jq '.scopes[]' -rMatch what you are calling against what the operation needs:
| Operation | Scopes |
|---|---|
canvas list/get/render, node get, recipe search/get, action list/get | canvas:read |
canvas apply with validateOnly | canvas:read |
canvas apply, node and connection writes, canvas create | canvas:read, canvas:write |
asset list/get | asset:read |
element list/get/history, element collection-list | element:read |
| Element and Element-collection mutations | element:read, element:write |
asset upload | asset:read, plus image:generate or video:generate |
model list/get, provider list | image:generate or video:generate |
recipe fork | canvas:read, canvas:write |
action run | canvas:read, canvas:write, asset:read |
image generate/edit/variations, recipe run | canvas:read, canvas:write, asset:read, image:generate; add element:read when applying Elements |
video generate | canvas:read, asset:read, video:generate |
run get/wait/cancel, job get/wait/cancel, video download | job:manage |
Two that surprise people:
- Listing models and providers needs a generation scope. A read-only token cannot browse the model catalog.
asset uploadneeds a generation scope, andaction runwith a local file input needs it too — because the local file is privately uploaded before the Action runs. Existingnode:andasset:handles do not.
Scopes cannot be added to an existing token. Create a new one.
“Waiting for the Run always fails”
The token lacks job:manage. Either create a token that includes it, or switch to --no-wait plus a signed webhook. See Webhooks.
Conflicts
conflict on a canvas write
Someone changed the canvas between your read and your write. The response includes the current revision.
Do not re-send the same operations with the newer revision — that is the overwrite the check exists to prevent. Read again, look at what changed, rebase your addition around the newer graph, and use a new idempotency key if the payload changed.
“Automatic image targets are incomplete”
You reused an idempotency key whose earlier attempt created only some of the automatic target nodes. Use a new key, or pass explicit --target node:NODE_ID values.
The same command created a second canvas
With --destination new-canvas, the idempotency key determines the canvas identity — the new canvas id is derived from a hash of the key. A different key means a different canvas. Reuse the key to reuse the canvas.
Not found
A run: or job: handle stopped resolving
Image and Action observation records expire: 24 hours while unacknowledged, 15 minutes after the first successful server finalization, and seven days for the expired tombstone. Recipe Run records currently do not use that temporary TTL.
The node: and asset: handles from the result stay durable. If you kept only the run: handle, read the destination canvas to recover the outputs.
A handle that “should” exist does not
If it was constructed rather than copied from a response, that is the bug. Handles come from Gavana. For a shared asset, use the owner-qualified asset:OWNER_UID:ASSET_ID form exactly as returned.
Usage errors
| Message pattern | Fix |
|---|---|
recipe run requires --destination … | Supply agent-canvas, new-canvas, or a canvas handle |
… with --destination new-canvas requires --canvas-title | Add --canvas-title |
Unknown Recipe input KEY | Run recipe get and use the exact keys |
Recipe input X was provided more than once | One --input per declared input |
<Action> requires exactly N --input values | Match the count and order from action get |
--last-frame requires --first-frame | Supply a first frame |
--canvas canvas:<id> is required when a video frame or reference uses node:<id> | Add --canvas |
Video --reference values must be unique | Remove the duplicate |
--download cannot be combined with --no-wait | Queue, then video download |
--duration must be a whole number from 1 through 120 | Use an integer the model also supports |
<Model> does not support --X | Check model get for the model’s real parameters |
<Model> does not support --X Y. Supported values: … | Use one of the listed values |
Video model X exists on more than one connection | Use the model: handle from model list |
Output file already exists: … | Add --yes to overwrite, or choose another path |
Reference image must be a PNG, JPEG, WebP, or GIF | Convert it. The CLI checks magic bytes, not the extension |
Reference image is too large | Maximum 50 MB per image |
An image job can use up to 16 references | Reduce the count; video allows nine |
--jq must be a jq-style path | Start with .; only property paths, non-negative indexes, and [] |
-r/--raw cannot be combined with --output … | -r works only with json or raw |
node update needs --json/--file or a title, content, prompt, position, or size option | An empty patch is refused rather than written |
… Re-run with --yes | The command is destructive and wants confirmation |
The retired campaign commands
The retired campaign command surface is disabled. Use Recipe Library forks and explicit canvas/image commands instead.Expected. The campaign group is retired. Set GAVANA_ENABLE_LEGACY_CAMPAIGN_COMMANDS=true only to migrate off it. Build on Recipes instead.
Output problems
“My script cannot find .result any more”
You added --jq. With --jq the CLI prints only the selected value, without the ok/result envelope. Pick one shape and keep it.
“The output is empty”
With -r/--output raw, an empty array prints nothing at all. With --output jsonl, an empty result set prints nothing. Both are success, not failure — check the exit code.
“Progress output is polluting my pipeline”
It is not. Progress and errors go to stderr; only the result goes to stdout. If your pipeline sees them, something is merging the streams.
Connection problems
network — “Could not reach Gavana at …”
Check connectivity and the base URL:
gavana config get --jq '.baseUrl' -rSafe GET requests are retried up to three times on transient errors and on 429, 502, 503, 504, respecting Retry-After. Writes are never retried automatically. A network error on a write means you must decide, with the user, whether to try again.
configuration — “Gavana base URL must use HTTPS, except on local loopback”
Remote origins require HTTPS. Plain HTTP is accepted only for localhost, 127.0.0.1, and ::1. The base URL also cannot contain credentials, a query string, or a fragment.
The wrong environment or account
gavana config list
gavana config use workPrecedence, highest first: --profile, then GAVANA_PROFILE, then CRAFTBOARD_PROFILE, then the stored active profile, then default.
Browser login never completes
It times out after three minutes. On a remote or headless machine use --no-browser and open the printed URL yourself, or use a Personal Access Token via --token-stdin.
Configuration problems
“The CLI cannot find my token”
Resolution order for the config path:
GAVANA_AGENT_CONFIG_FILECRAFTBOARD_AGENT_CONFIG_FILE$XDG_CONFIG_HOME/gavana/agent.json~/.config/gavana/agent.json
A legacy ~/.config/craftboard/agent.json is read as a fallback when no explicit config file is set.
macOS Keychain
On macOS the token is stored in Keychain under the service ai.gavana.cli, keyed by profile name, and the config file records credentialStore: "macos-keychain" instead of a token. Keychain is skipped when GAVANA_CLI_KEYCHAIN=false, when a custom config file path is set, or when XDG_CONFIG_HOME is set — in those cases the token goes into the mode-0600 file.
If Keychain access is denied, the token is unreadable and credentials fails in doctor. Log in again, or set GAVANA_CLI_KEYCHAIN=false and log in to use the file instead.
“I cannot log out”
auth logout revokes an OAuth token at Gavana before removing the profile, so it needs network access. A token-based profile has nothing to revoke remotely — revoke it in Personal Access Tokens.
MCP problems
| Symptom | Cause | Fix |
|---|---|---|
| The server never appears in the client | Registered at the wrong scope, or the config was not reloaded | Re-register at user scope; restart the client |
| The local server will not start | Node.js older than 20, or npx unavailable | Install Node.js 20+ |
| Every call is unauthorized | Token was revoked, mistyped, or created with a custom expiry that has passed | Create a new token and update the client configuration |
| Tools are listed but everything is refused | Read-only mode, or a read-only token | Remove GAVANA_MCP_READ_ONLY, or use a token with the scopes you need |
| No webhook option anywhere | By design | MCP omits webhook secrets. Use the CLI or the API |
gavana mcp install codex fails immediately | codex is not on PATH | Install it, or use gavana mcp config codex and register by hand |
More: MCP troubleshooting.
Agent behaviour problems
These are not error codes. They are the agent going off contract.
| Symptom | Cause | Fix |
|---|---|---|
| The agent reorganised a canvas when asked to add to it | Ignored the preservation contract | Point it at the safety contract. “Add” never means “reorganise” |
| The agent generated something nobody approved | Inferred authorisation from preparation language | Preparing is not running. Approval is per-turn |
| Two charges for one request | Automatic retry after a failure | Never auto-retry paid work. A retry is a new intent |
| The agent reported a canvas node that does not exist | Claimed durability it never observed | Read back before reporting. Report only what the server returned |
The agent invented a model: handle | Guessed instead of listing | model list then model get. Handles are opaque |
What to send support
Send the requestId, the operation by name, and the error code.
gavana canvas get canvas:OWNER_UID:CANVAS_ID 2>&1 >/dev/null | jq '.error | {code, status, requestId}'Every Gavana response carries an X-Request-ID, surfaced as requestId on errors and — where the server did not already supply one — on successful results too. It identifies the exact server-side trace without exposing anything else.
Never send a token, a full request body, a config file, or a terminal screenshot. The requestId is enough, and everything else is a leak. If a credential has already been shared, revoke it now.