Script the CLI
Run graft from a script or an agent and read exactly what happened.
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 code | What it means | What to do |
|---|---|---|
0 | It worked. | Nothing. |
1 | It failed. | Read the error and its fix. |
2 | The command was called wrong. Nothing ran. | Fix the command; retrying it as it is fails the same way. |
3 | A person has to approve the action first. | A person runs graft approve <id>, then you repeat the action. |
4 | The 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.
Print what a compile changed
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
- CLI reference: every command and flag.
- Error reference: what each error code means and how to recover.
- Connecting agents: give an agent the same operations over MCP.