Script the CLI

Run graft from a script or an agent and read exactly what happened.

Reference8 min read

Every graft command works the same way when a program runs it instead of a person. The data goes to stdout, messages go to stderr, and the exit code says whether it worked and, if not, what kind of failure it was. This page covers those rules. The CLI reference lists every command and flag.

Find a command and its flags

graft --help lists the commands by task. graft <command> --help shows one command's usage, flags and examples, even when the rest of the line is incomplete. graft help <command> prints the same thing.

graft help branch create
graft help exit-codes

The help topics are exit-codes, json, environment and auth.

An agent can learn the whole CLI in one call. graft commands --json prints every command with its usage, arguments, flags, whether it takes --json, and whether it needs a database.

graft commands --json

A command only takes the flags it declares. A misspelt or misplaced flag stops the command before it does anything, names the closest match, and exits 2:

graft: unknown option "--jsno" for graft compile.
Did you mean `--json`? Run `graft compile --help`.

Get JSON instead of text

Every command that prints data takes --json. It then prints one JSON object on stdout and nothing else. Progress and messages still print, on stderr, so a script can save stdout and a person can still read the log. The object is indented on a terminal and on one line when piped or redirected.

graft check --json
{"documents":1,"problems":[]}

Each command's entry in the CLI reference says whether it takes --json. Commands that run until you stop them (graft dev, graft serve, graft studio, graft mcp) do not.

Read and write content from the terminal

The graft content commands run the same tools an agent calls over the Model Context Protocol (MCP), the open standard that lets agents use tools. They run them in the CLI's own process, so they check the schema, compile, and file approvals exactly as the agent's call would, and need no database on the static engine.

Print a document as the file it is, change it, and write it back:

graft content get pages/home > home.mdx
graft content write pages/home --file home.mdx
Wrote pages/home.mdx on branch "main": +0 added, ~1 changed, -0 removed.

--file - reads the document from stdin. To write frontmatter alone, pass it as JSON with --data '{"title":"About"}', and the body with --body-file.

Find documents, and change one field on several at once:

graft content search pricing --collection pages
graft content set pages home about --field tagline --value '"Updated"'

graft content set writes each document the way graft content write does. One that fails is listed with its error and fix, the others are still saved, and the command exits 1.

Deleting needs a person. The first run files an approval and exits 3, and its fix is the exact command to repeat:

error DESTRUCTIVE_OP_REQUIRES_APPROVAL: "delete_content" is destructive and always requires human approval. An approval request has been filed.
  fix: Ask a person to run `graft approve f9cb72bb-4b39-48e7-913b-9b9979da310e`, then run `graft content delete pages/about --approval f9cb72bb-4b39-48e7-913b-9b9979da310e`. The approval works once, for this exact call.

Approvals live in Postgres, so on the static engine graft content delete answers NEEDS_DATABASE: delete the file and compile instead.

graft schema prints each collection's fields, graft fn run runs a typed function, graft drafts handles drafts where content is kept in the database, and graft explain <code> explains any error code. graft mcp call runs any other tool by name. The MCP tools page maps each tool to its command.

Work with a hosted site

The same commands work on a site you host with graft serve --studio, through its /api/mcp endpoint. Make a token in Studio under Developer, Agents, then save it once:

printf %s "$TOKEN" | graft login https://cms.example.com --with-token

graft login checks the token with the site, prints who it acts as and what it may do, and saves it in ~/.config/graft/hosts.json (under XDG_CONFIG_HOME when that is set), readable by you alone. Without --with-token, on a terminal, it asks for the token without echoing it. A token is never an argument, where it would land in your shell history.

Then add --remote to a content command:

graft content list --remote
graft content write posts/launch --file launch.mdx --remote

Each prints the site it is using on stderr. In CI, set GRAFT_URL and GRAFT_TOKEN in the environment instead: commands that take --remote then use the site without the flag, and --local brings them back to the folder's project. GRAFT_URL is never read from .env, so a file in some folder cannot point a command at a production site.

graft auth status checks every saved token against its site, and graft logout forgets one on this computer. A token keeps working until someone revokes it in Studio.

No token can approve an action. A delete over --remote exits 3, and a person approves it in the site's Studio before you repeat it with --remote --approval <id>.

Call the Studio API

graft api calls any Studio API route on the site with the saved token, so you never paste the token into a curl command:

graft api workspace
graft api entries -f collection=posts --paginate
graft api -X PUT entry --input entry.json

A path is under /api/studio/v1 unless it starts with /api/fn or /api/content/v1. -f key=value sends a string and -F key=value sends true, false, null or a number as JSON. Fields make the request a POST with a JSON body; with -X GET or --paginate they become the query string. --paginate follows nextCursor and prints every page. Add --slurp to get every page as one JSON array once the last one arrives. graft api with no path lists the routes.

The answer prints on stdout. A refused request also prints its error and fix on stderr and exits 1, or 4 when the token lacks the access. -X DELETE asks before it sends anything on a terminal, and needs --yes everywhere else. graft api never follows a redirect, and never sends the token to another site.

Read a failure

When a command run with --json fails, stderr gets one line of JSON with the same fields every Graft error has: the error code, a message, the fix that names the next step, and details when there are any. Stdout stays empty.

{"error":"USAGE_INVALID","message":"unknown option \"--jsno\" for graft compile.","fix":"Did you mean `--json`? Run `graft compile --help`.","details":{"command":"compile","flag":"--jsno","suggestion":"--json"}}

Without --json, the same error prints for a person:

error CONFIG_NOT_FOUND: No graft.config.{ts,js} found in /tmp or any parent directory.
  fix: cd into a Graft project, or scaffold one here with `graft init`.

The error reference explains every code.

Act on the exit code

Exit codeWhat it meansWhat to do
0It worked.Nothing.
1It failed.Read the error and its fix.
2The command was called wrong. Nothing ran.Fix the command; retrying it as it is fails the same way.
3A person has to approve the action first.A person runs graft approve <id>, then you repeat the action.
4The credential is missing, wrong, or lacks the access this needs.Fix the credential before retrying.

graft doctor, graft check and graft ci skip-build use their own codes, listed in their reference entries. graft ci skip-build, for example, exits 0 when the deploy can be skipped and 1 when it has to build.

graft compile --json prints the branch, the commit and what changed. The summary line goes to stderr. It works on both engines.

{
  "branch": "main",
  "gitSha": "b8eb719a468be404bae9adca4fbea4d90c642411",
  "changes": {
    "added": ["posts/launch"],
    "changed": ["pages/home"],
    "removed": [],
    "unchanged": 42
  }
}

added, changed, and removed are lists of collection/slug ids, and unchanged is a count. gitSha is null when the content is not in a git repository.

On the static engine, a content change ships with a new deploy, so there is nothing to refresh. On the Postgres engine, writes from agents, Studio, graft merge and graft migrate call your app's revalidate route when GRAFT_REVALIDATE_URL and GRAFT_WEBHOOK_SECRET are both set. With the URL alone, these commands refuse to start. graft compile does not, because a deploy compiles before the new version is live. Send its output to the route yourself so the app refreshes only the pages that changed. A shell does not read .env, so export GRAFT_WEBHOOK_SECRET in the script's environment first:

graft compile --json > changes.json || exit 1
curl --fail -X POST https://example.com/api/revalidate \
  -H "Authorization: Bearer $GRAFT_WEBHOOK_SECRET" \
  -H "content-type: application/json" \
  --data-binary @changes.json

See Refresh pages when content changes for the route.

Stop the CLI from asking questions

Only graft create and graft db claim ask questions, and only on a terminal. Pass --no-input, or set GRAFT_PROMPT_DISABLED=1 for every command at once, and they never ask. graft create then takes the default for each question a flag did not answer. graft db claim says to run it again with --url.

Turn color off or on

graft prints color only to a terminal. NO_COLOR turns it off everywhere. TERM=dumb and CLICOLOR=0 turn it off too. FORCE_COLOR or CLICOLOR_FORCE turn it on when the output is not a terminal. Set GRAFT_DEBUG=1 to print the stack trace of an unexpected error.

Update notice

When a newer Graft is on npm, graft prints a notice on stderr after the command finishes. graft dev, graft serve and graft studio print it while they run. It names the new version, the version you have, and the upgrade guide. On a beta, the notice is yellow, because beta releases often include security fixes. On a stable release it is cyan.

The CLI asks the registry at most once a day and caches the answer in ~/.cache/graft/update-check.json (or under XDG_CACHE_HOME). It uses the registry in npm_config_registry when you set one. A slow or unreachable registry never fails or delays a command by more than 1.5 seconds.

The check is off when GRAFT_NO_UPDATE_CHECK=1 or CI is set, under --json, and for graft mcp, graft help, graft commands, --help and --version. GRAFT_NO_UPDATE_CHECK=1 works in the environment or in the project's .env.

Next steps