Error reference

Reference21 min read

Look up an error code you saw in a terminal, a log, or an agent's tool result.

Every Graft error has three parts: a code from this list, a message saying what happened, and a fix naming the next step. The fix in your terminal is specific to your failure. The entry on this page explains the general cause, so you can tell whether the fix applies or something else is wrong.

Agents can read the same entries without leaving their tools: the explain_error MCP tool returns them for any code.

There are 51 codes, listed alphabetically.

Codes

ALREADY_INITIALIZED

graft init was run in a directory that already has a graft.config.

Usually because

  • Re-running init in an existing project
  • Pointing init at the wrong directory

How to recover

This is already a Graft project — evolve it by editing the existing graft.config.ts (add collections or fields there), or run graft init <dir> against an empty directory.

APPROVAL_INVALID

An approval id was supplied (the approval argument over MCP, the x-graft-approval header over raw HTTP), but it cannot authorize this call — details.reason says why (pending, denied, already_consumed, mismatch, or not_found).

Usually because

  • Retrying before a human has decided (pending)
  • Reusing an approval — they are one-shot (already_consumed)
  • Changing the input or function between request and retry (mismatch)
  • A human refused the operation (denied)

How to recover

pending → wait for the human decision; denied → do not retry, ask the operator; already_consumed or not_found → call again without the approval id to file a fresh request; mismatch → retry with exactly the approved input.

APPROVAL_SELF_DECISION

The identity deciding an approval is the same identity that requested it. Separation of duties: a requester can never approve (or deny) their own destructive operation.

Usually because

  • An agent (or a wrapper acting for it) running graft approve on an approval it filed itself
  • Passing the requester's identity as the decider (e.g. reusing the same dev-token id)

How to recover

A DIFFERENT operator must review it: they run graft approve <id> (or graft deny <id>) under their own identity. Do not retry as the requester and do not switch identities to impersonate a reviewer — the gate exists so a second party sees the exact function + input before it runs.

APPROVAL_UNATTRIBUTED

The approval was filed by a caller with no stable identity, so there is nobody the request can be held to and no meaningful separation of duties to enforce. It cannot be decided by anyone.

Usually because

  • An unauthenticated caller reached a destructive function (an MCP mount or functions route serving anonymous actors)
  • A trusted issuer minted a token with no sub claim, so the actor authenticated but carried no id
  • A row left over from before approvals required an identified requester

How to recover

Fix the filing surface, not the row: require authentication where the approval was requested (an actor resolver, or a token that carries sub), then have the caller request the operation again so the approval records who wants it. A stale row can be resolved directly in the database.

ASSET_EXISTS

An asset upload targeted a key that already holds a binary. The store has no version history, so an overwrite would irreversibly replace it — uploads refuse unless overwrite is explicit.

Usually because

  • Re-uploading with the same key instead of picking a new one
  • Two documents' assets colliding on a generic key like assets/hero.png

How to recover

Pick a distinct key (e.g. prefix it with the document: pages/pricing/hero.png) — that is almost always right. Only pass overwrite: true (put_asset) or --overwrite (graft asset put) when replacing the existing binary is the actual intent; every document referencing that key will show the new bytes.

AUTHORITY_MISMATCH

An operation used the wrong interface for a collection's authority: files/write_content on a db-authoritative collection, or record helpers on a file-authoritative one.

Usually because

  • Authoring an MDX file for a collection whose rows live in Postgres
  • Calling insertRecord/listRecords on a collection whose documents are files

How to recover

Check the collection's authority with describe_schema. File-authoritative → author MDX (write_content / files + compile). Db-authoritative → go through its functions (POST /api/fn/<name>); the rows are operational data Postgres owns.

BRANCH_BACKEND_FAILED

The branch backend's control plane (the Neon API for neon branches) rejected or failed an operation, so the branch's physical state may not match the registry.

Usually because

  • An invalid or expired NEON_API_KEY, or one scoped to a different project
  • A wrong GRAFT_NEON_PROJECT_ID
  • Neon-side limits (max branches) or a transient API outage
  • The branch's compute endpoint never became reachable after create

How to recover

Check NEON_API_KEY and GRAFT_NEON_PROJECT_ID in the environment, then retry. If a create failed partway, the error says whether a Neon branch was left behind — delete it in the Neon console (or retry the drop) before recreating. Overlay branches never hit this: they need no external API.

BRANCH_EXISTS

A branch with that name is already registered.

Usually because

  • Re-running a create for a branch that already exists
  • Two previews competing for the same name

How to recover

Use the existing branch, pick a different name, or drop the existing one first. Registering a branch is idempotent only if you check first — names are unique.

BRANCH_INVALID

A branch operation was rejected because the name or the topology change is not allowed.

Usually because

  • A branch name that is not URL-safe (uppercase, spaces, punctuation)
  • Making a branch its own parent, or dropping main
  • Dropping a branch that still has child branches

How to recover

Use lowercase kebab names with optional "/" segments (e.g. "preview/checkout"). Fork previews from main; drop child branches before their parent; main is the root and cannot be dropped.

BRANCH_NOT_FOUND

An operation referenced a branch that is not registered in the topology.

Usually because

  • A typo in the branch name
  • Forking from or dropping a branch that was never created
  • Expecting a branch to exist that a teammate has not created yet

How to recover

List the registered branches (graft branch) to see valid names. "main" is always seeded; create previews off it before forking from or merging them.

COLLECTION_NOT_FOUND

Content referenced a collection that is not registered in the schema.

Usually because

  • A typo in the collection name
  • A document placed in a folder that has no matching defineCollection
  • The collection exists in code but was not passed to compile()/the MCP server

How to recover

Call list_collections to see what is registered. Either use one of those names, move the file into a registered collection folder, or add a defineCollection for it in graft.config.ts.

COMMIT_FAILED

git refused to record the commit. The working tree is untouched by the refusal.

Usually because

  • No committer identity configured (user.name / user.email)
  • A pre-commit or commit-msg hook rejected the change
  • Nothing to commit — the selected paths match the last commit already
  • The repository is mid-merge or mid-rebase

How to recover

The error's details.stderr carries git's own words. For an unset identity, run git config user.name "…" and git config user.email "…". Selected files may already be staged; git status shows the current state, and committing from a terminal always remains available.

CONFIG_INVALID

graft.config exists but could not be loaded, or it does not export a valid collections record.

Usually because

  • A syntax or import error in graft.config.ts
  • Missing export const collections = { … }
  • A collections entry that was not created with defineCollection

How to recover

Fix graft.config.ts so it imports defineCollection/field from @usegraft/core and exports collections as a record of defineCollection results — the error message names exactly what failed to load or validate.

CONFIG_NOT_FOUND

No graft.config.{ts,js} was found in the working directory or any parent.

Usually because

  • The command was run outside a Graft project
  • The project has not been initialized yet

How to recover

cd into the project root (the directory holding graft.config.ts) and retry, or scaffold a new project with graft init.

CONTENT_CONFLICT

A write or a publish was refused because the document changed underneath it: someone (a person in Studio, an agent over MCP, a developer's push) saved a newer version after this one was read. Nothing was written.

Usually because

  • Two people, or two browser tabs, editing the same document
  • An agent called write_content on a document a person had open in Studio
  • A publish where the production branch changed the same document since the draft began

How to recover

Read the document again (details.path) and reapply the change on the current version. For a publish, details.conflicts lists each document; publish again choosing, per document, to keep the draft version or take the published one.

CONTENT_DIR_NOT_FOUND

The configured content directory does not exist on disk.

Usually because

  • The project has no content/ directory yet
  • compile() was pointed at the wrong path

How to recover

Create the directory (documents live at <contentDir>/<collection>/<slug>.mdx) or correct the contentDir passed to compile()/the MCP server. write_content creates folders automatically.

CONTENT_REFERENCE_NOT_FOUND

A reference field names a document that does not exist. graft compile stops on it, and a database-sourced publish refuses it, including the deletion of a document that live documents still point at.

Usually because

  • The referenced document was deleted, renamed or given a new slug
  • A slug typo in the reference
  • The reference and its target were published separately, target last

How to recover

Read details.references: each names the file, the field and the missing collection/slug. Create the missing document, or point the field at one that exists (list_content shows what exists). To delete a document others point at, change them in the same publish. graft check lists every broken reference at once.

CONTENT_SOURCE_MISMATCH

The operation works on one source of content, and this project uses the other. A database-sourced project keeps its content in Postgres, so operations that read or rewrite the files in git (reverting to a commit, merging a branch's files, running a content migration over the files) have nothing to act on.

Usually because

  • graft.config.ts exports content = { source: "database" } and the call reached for files
  • A script written for a git-sourced project ran against a database-sourced one

How to recover

Use the database-sourced equivalent the error's fix names. To change a document, write it with write_content and publish it with publish_drafts. Do not edit the files under the content directory and expect the site to change: they are a checkout, not the source.

CONTENT_TREE_READ_ONLY

A write reached the content tree, but the filesystem refused it. Authored content is files, so writing requires a writable checkout — serverless platforms deploy a read-only filesystem.

Usually because

  • Studio or an MCP write served from a serverless deployment (Vercel, Netlify, Cloudflare)
  • A container with the project mounted read-only
  • File permissions on the content directory

How to recover

Run the writing surface where the checkout is writable — local graft studio / graft mcp, or a self-hosted container with the project mounted read-write — and let the deployment serve reads only. Writes then arrive as git commits, which is the model: git is authoritative for authored content.

DESTRUCTIVE_OP_REQUIRES_APPROVAL

The operation is human-gated: a pending approval request was filed (details.approvalId) and the call will not run until a human approves it. Destructive functions are always gated; under the 'human' approval policy every mutation is.

Usually because

  • Calling a function marked destructive: true (deletes or irreversibly overwrites data)
  • Calling any mutation on a deployment whose approvalPolicy is 'human'
  • Calling a destructive function over MCP at all — the 'unattended' policy that lifts this gate for a headless deployment is not part of the MCP surface, so there is no server setting that makes this tool call stop asking. That is deliberate, and not something to route around

How to recover

Ask a human operator to run graft approve <approvalId> (they can also graft deny it). Once approved, retry the EXACT same call carrying the approval id — over MCP pass it as the approval tool argument; over raw HTTP send the x-graft-approval: <approvalId> header. Approvals are one-shot and bound to the exact input — never work around the gate. If you are seeing this over MCP, the server did not ask your client directly: either the mount has not opted into elicited approvals (approvalElicitation, off by default and intended for a local server whose operator is present) or your client did not declare the elicitation capability. Both are ordinary — the gate is the same either way, only the way the human is reached differs.

DOCUMENT_NOT_FOUND

No document with the requested slug exists in that collection's files.

Usually because

  • A slug typo
  • The document lives in a different collection
  • The document was deleted (git is authoritative — the files are the truth)

How to recover

Call list_content for the collection to see every slug that exists, then retry with a real one — or author the document with write_content.

ENV_VAR_MISSING

A required environment variable is not set.

Usually because

  • The project has no .env file yet
  • The variable exists in a different environment (e.g. CI but not local)

How to recover

Add the variable named in details.variable to the project's .env (the CLI walks parent directories to find one) or export it in the environment, then retry.

FRAMEWORK_VERSION_UNSUPPORTED

An adapter API needs a newer version of the framework than the app has installed. The rest of the adapter still works.

Usually because

  • Calling updateContent from @usegraft/sdk-next on Next.js 15. It needs updateTag, which Next.js added in 16
  • Mounting graftHandler from @usegraft/sdk-nitro on an h3 older than 1.15. It reads the request from event.req (h3 2) or event.node.req (h3 1)

How to recover

Use the fallback the error's fix names. For updateContent that is revalidateContent(branch, changes), which works on Next.js 15 and 16. For graftHandler outside Nitro, call the Graft handler with a Web Request directly. Or upgrade the framework.

FUNCTION_EXECUTION_FAILED

The function's handler threw an unexpected (non-Graft) error — a bug in the handler or its environment, not in the caller's input.

Usually because

  • A runtime error inside the handler code
  • The database is unreachable or a required env var is missing on the server

How to recover

Retrying with the same input will fail again. Find the invocation in the server logs via details.correlationId, fix the handler code or the server environment, then retry.

FUNCTION_NOT_FOUND

The invoked function name is not registered in the functions runtime.

Usually because

  • A typo in the function name (the last path segment of the POST URL)
  • The function exists in code but was not included in the exported functions record

How to recover

The error's details.available lists every registered function — call one of those, or add a defineFunction to graft.config.ts and include it in the exported functions.

GIT_UNAVAILABLE

An operation needed git and could not reach it: either the git binary is not on PATH, or the content directory is not inside a git work tree.

Usually because

  • The project was scaffolded but git init was never run
  • Studio or the CLI running in a container image that ships no git binary
  • The content directory lives outside the repository (a mount, a symlink target)

How to recover

Run git init at the project root and make a first commit, or install git. Nothing else in Graft requires it — content still compiles and serves — but the change history, graft compile's recorded SHA, and reverting all depend on it.

INDEX_OWNERSHIP

A projection would soft-delete every document in collections this project's schema doesn't know — the signature of two projects pointing at one DATABASE_URL. Nothing was written; the transaction rolled back.

Usually because

  • DATABASE_URL points at another project's database (e.g. a shared repo-root .env)
  • A collection was renamed or deleted in graft.config.ts, so its old rows look foreign

How to recover

Each project needs its own database or branch: set DATABASE_URL in a .env next to graft.config.ts (it overrides parent .envs). If the schema really did drop or rename a collection, a human runs graft compile --prune-unknown once — the override is CLI-only by design.

INPUT_VALIDATION_FAILED

A function invocation's input does not satisfy the function's Zod input schema.

Usually because

  • The request body is not a JSON object
  • A required input field is missing or has the wrong type

How to recover

Fix the fields listed in details.issues and retry. describe_schema shows each function's exact input fields; an empty body is treated as {}.

INTERNAL_ERROR

The graft CLI hit an error it does not recognize. It is a bug in Graft or in code the command loaded, not a mistake in how the command was called.

Usually because

  • A bug in Graft
  • graft.config.ts or a module it imports throwing an ordinary Error

How to recover

Run the command again with GRAFT_DEBUG=1 to print the stack trace. If it points into graft.config.ts or your own code, fix it there; otherwise report it with the trace.

INVALID_SLUG

A slug is not URL-safe kebab-case.

Usually because

  • Uppercase letters, spaces, or punctuation in the slug or filename
  • Leading/trailing/double hyphens

How to recover

Use lowercase letters, digits, and single hyphens only (e.g. "getting-started"). Set a valid slug in frontmatter or rename the file.

METHOD_NOT_ALLOWED

The endpoint was called with an HTTP method it does not serve.

Usually because

  • GETting a function endpoint (functions are RPC over POST)
  • A client following a redirect that downgraded the method

How to recover

Use the method named in the Allow response header — for Graft functions, POST a JSON object body to the same URL.

MIGRATION_FAILED

A content or data migration could not be applied; nothing was written (runs are all-or-nothing).

Usually because

  • The transform's output does not satisfy the collection's current schema
  • The transform threw for some documents/rows
  • A file's frontmatter is not parseable YAML

How to recover

Read details.failures — each entry names the file/row and why it failed. Fix the transform (or the listed files) in migrations/<id>.ts, then re-run graft migrate --apply. Dry-run first with graft migrate to see what would change.

MIGRATION_REQUIRED

The schema and the stored content/data have drifted; a migration must run first.

Usually because

  • graft.config.ts changed shape while documents still use the old shape
  • A database schema change is pending

How to recover

Run the pending migration (content migrations update the files; DB migrations via @usegraft/db), then retry the operation.

NEEDS_DATABASE

This project runs on the static storage engine (index = "static" in graft.config), and the requested feature needs the Postgres engine: db-authoritative collections, typed functions, or database branching.

Usually because

  • A db-authoritative collection or a defineFunction was added to a static-mode project
  • graft compile --branch <name> was run in static mode (branches are git branches there)

How to recover

Either stay static (content-only: use git branches for previews) or upgrade: set DATABASE_URL in .env and switch graft.config to export const index = "postgres", then re-run graft compile.

NOT_IMPLEMENTED

The capability is planned but not built yet.

Usually because

  • Calling a placeholder API from a later phase

How to recover

Use the documented alternative (the error's fix names it if one exists), or accomplish the task by editing files directly — git is always a valid interface.

PAYLOAD_TOO_LARGE

The request body is larger than the endpoint accepts. It was refused before Graft read the rest of it, so the function or tool never ran. A refused function call is still recorded against the caller's rate limit.

Usually because

  • A function call or Studio save carrying more than 1 MiB of JSON, the default for those routes (Studio asset uploads allow 14 MiB)
  • An MCP put_asset with a file over about 24 MB, which is 32 MiB once base64-encoded
  • A client sending an unbounded stream to an endpoint

How to recover

Send a smaller body. For a large asset, upload it with graft asset put <file>, or pass path to put_asset on a local stdio server; over HTTP, put_asset takes base64, which counts against the limit. Each endpoint has its own limit: a direct function call or Studio request raises maxBodyBytes on that handler, and an MCP message, including run_function arguments and HTTP put_asset, raises it on the MCP handler. --max-body-bytes on graft serve sets all of them.

RATE_LIMITED

This caller has invoked the function more times than its per-window limit allows. Every attempt counts, including rejected ones.

Usually because

  • A retry loop hammering a function after failures
  • Many calls from one actor (or one IP, for anonymous callers) in a short window

How to recover

Wait out the window (the Retry-After header says how long) before retrying, and fix whatever caused the burst — the limit is per caller per function, so backing off actually works.

REGISTRY_FILE_EXISTS

graft add would overwrite a file that already exists in the project, so it wrote nothing (adds are all-or-nothing).

Usually because

  • The item (or one of its dependencies) was already added
  • A project file happens to share a target path with the item

How to recover

Inspect the listed file(s). If replacing them is intended, re-run with --overwrite; otherwise move/rename your file first. graft add --dry-run <name> previews every path an item would write.

REGISTRY_ITEM_INVALID

A registry item's manifest is malformed, or the item requires a different @usegraft/core version than the one installed.

Usually because

  • registry.item.json does not match the manifest schema
  • The item's graftVersion range does not include the installed core version
  • A file the manifest lists is missing from the item directory

How to recover

The error names what failed (a manifest field or the version mismatch). For a version mismatch, move @usegraft/core to the range the item needs; a malformed manifest is a registry bug — fix the item or report it.

REGISTRY_ITEM_NOT_FOUND

graft add was asked for a registry item (a copy-in primitive) that does not exist.

Usually because

  • A typo in the item name
  • Expecting a community/remote item — only the bundled core registry ships today
  • The item was renamed or removed

How to recover

The error's details.available lists every item you can add. Run graft add <name> with one of those, or build the primitive by hand as owned code under graft/.

REMOTE_STORE_FAILED

The content store that writes to a remote repository (GitHub) refused or failed a request. When GitHub refused (details.status is set), that request was not applied, but an operation made of several requests may have finished earlier steps: a publish can move the production branch and then be refused while tidying the draft. When a request failed before an answer arrived (a network error), GitHub may still have applied it. Either way, check the draft and the production branch before retrying.

Usually because

  • The token or GitHub App lacks Contents or Pull requests write access to the repository
  • GRAFT_GITHUB_REPO or GRAFT_GITHUB_BRANCH names a repository or branch that does not exist, or the credential cannot see a private repository (GitHub answers 404 for both)
  • A branch protection rule forbids direct pushes to the production branch
  • GitHub rate limiting or an outage

How to recover

Read the error message and details. A GitHub refusal carries details.status and details.message, GitHub's own answer; a failure before GitHub answered (network, a malformed response) has the message only. A 401 or 403 is credentials or permissions; a 404 is the repository or branch name, or a private repository the token or GitHub App is not installed on; a protected branch needs GRAFT_STUDIO_PUBLISH=pull-request. Retry once the cause is fixed: drafts already saved stay on their branch. After a network failure, reload the entry or the list of changes first, since the request may have gone through.

REMOTE_UNREACHABLE

The graft CLI could not reach the hosted site it was pointed at, or the site answered with something other than Graft.

Usually because

  • A typo in the URL given to graft login or in GRAFT_URL
  • The site is down, or this computer has no network
  • The site does not run graft serve, so it has no /api/mcp or /api/studio/v1

How to recover

Check the address with graft auth status, open it in a browser, and log in again with graft login <url> if it changed. Studio is served by graft serve --studio.

REVALIDATE_FAILED

The content was written and the index is up to date, but telling the app to refresh its cache failed. The app may serve the old copy until its next refresh.

Usually because

  • GRAFT_WEBHOOK_SECRET differs from the secret the app's revalidate route checks (the route answers 401)
  • GRAFT_REVALIDATE_URL is unreachable from the server, redirects, or points at the wrong path
  • An onContentChange listener passed to an in-app mount threw

How to recover

Do not retry the write: it succeeded. Fix the cause the error's fix names. Then POST the write's { branch, gitSha, changes } to the revalidate route: an agent's result carries changes, and the server logs the whole body with the error. Do not recompile instead. The index already holds the change, so a later compile reports these documents as unchanged and refreshes none of them.

ROUTE_NOT_FOUND

The Graft server (graft serve) has nothing mounted at the requested path — the request reached the right process but the wrong URL.

Usually because

  • A typo in the endpoint path (e.g. /api/fns instead of /api/fn/<name>)
  • Expecting the frontend app's routes on the headless runtime — graft serve hosts functions, MCP, authored-content reads, and health

How to recover

Use POST /api/fn/<name> for typed functions, POST /api/mcp for the MCP Streamable HTTP surface, GET /api/content/v1/documents or GET /api/content/v1/search for authored content, or GET /healthz for liveness. The error's details carry the path that missed.

SCHEMA_VALIDATION_FAILED

A document's frontmatter does not satisfy its collection's Zod schema.

Usually because

  • A required field is missing from the frontmatter
  • A field has the wrong type (e.g. a string where a number is expected)
  • The schema changed in graft.config.ts and existing documents were not updated

How to recover

Call describe_schema to see the exact fields the collection expects, fix the frontmatter (the error's details.issues lists every violation), then retry the write or re-run compile.

SLUG_NOT_UNIQUE

Two documents in the same collection resolve to the same slug.

Usually because

  • Two files share a filename-derived slug
  • A frontmatter slug duplicates another file's slug

How to recover

The error's details list both files. Give one of them a unique slug (frontmatter slug: wins over the filename) and re-run compile.

STATIC_INDEX_NOT_FOUND

A read tried to open the static index artifact (.graft/index.db by default), but the file does not exist — the project has not been compiled yet.

Usually because

  • graft compile has never run in this checkout
  • The artifact path in graft.config's index setting does not match where compile wrote it
  • A deploy shipped the app without running graft compile in the build step

How to recover

Run graft compile (or add it before the framework build in the deploy's build command). The artifact must be deployed with the app.

STATIC_INDEX_UNSUPPORTED

Static index mode needs the node:sqlite built-in with FTS5, which this Node runtime does not provide (FTS5 ships in the bundled SQLite only from Node 22.16).

Usually because

  • Node older than 22.16 running the CLI or the app server

How to recover

Upgrade Node to 22.16+ (24 LTS recommended), or switch the project to the Postgres index (DATABASE_URL + export const index = "postgres").

TOKEN_INVALID

A bearer token was sent but could not be verified — different from sending no token (which makes the caller anonymous).

Usually because

  • An expired or not-yet-valid JWT
  • A token issued by an issuer this deployment does not trust
  • A wrong audience claim, a bad signature, or a malformed token

How to recover

Mint a fresh token from a trusted issuer (details.reason states what failed). Do not strip the Authorization header to fall back to anonymous — fix the token instead.

UNAUTHORIZED

The caller's token does not permit this operation.

Usually because

  • A missing or expired agent token
  • A token scoped to reads used for a write
  • An anonymous call to a mutation that is not marked public: true (the secure default)

How to recover

Obtain a token with the required scope from the project owner; do not retry with the same credentials. If the function is meant to accept anonymous callers, its definition needs public: true.

USAGE_INVALID

A graft command was called with an unknown command, flag or subcommand, a missing argument, or a flag without its value. Nothing ran.

Usually because

  • A typo in a command or flag name
  • A flag that belongs to a different command
  • A required argument left out, such as the branch name for graft merge

How to recover

Read the fix, which names the closest match when there is one, then run graft <command> --help for the exact usage. graft commands --json lists every command and flag. The CLI exits 2 for this error.