Skip to main content
Global: -v/-vv/-vvv raises log verbosity (stderr); GRAPH_LOG accepts a tracing filter string. Exception: graph workbench logs to <data_dir>/workbench.log instead of stderr (the TUI owns the terminal; [workbench].log_path or GRAPH_WORKBENCH_LOG overrides the path) — see Plan workbench.

graph ask

One agent turn.
Put --thread after the message (it takes an optional value).

graph chat

Interactive REPL. --thread [<id>] as above. Slash commands: /thread, /state, /quit.

graph workbench

Full-screen TUI for drafting, inspecting, and test-running plans — see Plan workbench. Opens blank (draft via chat), or with a plan resolved like plan validate (file path or identifier). Requires an interactive terminal on stdin and stdout.

graph plan

validate checks structure (templates, references, ids) and resolves every step tool against the loadable catalog — packs, user tools, plans (composition followed transitively), and configured MCP servers (server-level; MCP tool names are only verifiable by connecting). Problems exit 1; a file whose requires_servers declares an unconfigured server validates with a note (portable, just not runnable here). run performs the same resolution and refuses to start when it fails. Naming a plan hidden by unconfigured requires_servers errors with the missing servers. run exit codes: 0 ok, 3 missing/invalid inputs (schema printed to stderr), 1 failure (including unresolvable tools). Solver output streams to stdout; output-mode plans print JSON; silent plans print nothing. Per the streams contract, stdout carries only deliverables — list’s rows, show’s YAML, and --json envelopes. Verdicts and hints go to stderr, so plan validate prints nothing to stdout on success and its exit code is the answer; plan list on an empty catalog leaves stdout empty rather than emitting a hint into it. show and validate are deliberately lenient about which plans they will open: a plan that is invalid, or hidden by unconfigured requires_servers, is exactly the plan you need to inspect, so both resolve it from disk rather than requiring it to be in the runnable catalog. list only shows the runnable catalog; list --json also reports what was skipped (failed to load, with the reason) and hidden (with the missing servers).

Authoring plans

The commands below create and edit plan files. They are stateless — each one resolves a plan, applies a single edit, and writes the YAML back — so there is no draft session and no undo (use version control). Only draft costs inference; everything else works with no provider credentials and no network.
new scaffolds a plan with no steps. It is intentionally invalid on creation (plan has no steps) — an edit is only refused if it makes things worse, so a scaffold stays editable and a plan can be built up step by step without ever invoking the planner. draft runs the planner over your tool catalog, one validated step at a time; if drafting exhausts its retries on a step, the valid prefix is saved and reported under salvaged so you can finish it with step add instead of redrafting. --stdout prints the YAML and writes nothing. Drafting happens once, at the start. It replaces every step, so it is not a way to correct a plan — redrafting to fix one thing discards everything that was already right. --from exists to draft into a plan’s identity (its identifier, name, description, and input schema), which is how you get a well-named plan without renaming afterwards:
Once a draft has steps worth keeping, every correction goes through set, step add, step update, step rename, and step rm. Each applies one intent, is validated atomically, and is refused if it would break the plan.

Attributes

One attribute per call, named exactly as it appears in the plan file. See Plan schema for what each one means. solver and output are the two finish modes and are mutually exclusive, so setting either clears the other; unset solver / unset output leaves a silent plan. unset refuses the required fields (identifier, name, description, and a step’s tool/input) — change those with set instead. step rename is separate from step update because it is not a plain field write: it also rewrites every downstream {{<old-id>.…}} reference in later steps, the output map, and the solver templates.

What gets rejected

Every edit runs through the same guard the plan workbench uses: an edit is rejected only if it introduces a validation problem that wasn’t already there, and a rejected edit leaves the file untouched. Problems that already existed never block an edit (otherwise repairing a broken plan would be impossible) — they come back as preExistingProblems on the success result. So graph plan step rm demo E1 fails when a later step still references {{E1.…}}, naming the template that would dangle, while graph plan set demo description '…' succeeds on a plan that is invalid for unrelated reasons. Exit codes: 0 applied, 1 rejected. With --json the envelope is printed to stdout before the non-zero exit, so a caller always gets the structured problem list. Without it, a one-line result goes to stderr and stdout stays empty — see the scripting contract. Changing identifier writes a new file (<plans dir>/<new>.yaml) and leaves the original in place, reporting renamedFrom; removing the old file is left to you, so a rename can never silently destroy a plan. An edit reserializes the whole file, so cosmetic YAML is normalized: flow mappings (input: { a: b }) become block mappings, folded scalars (>) become literal ones (|), and comments are dropped. The plan itself is unchanged — commentary belongs in description and each step’s reasoning, which are real fields the agent also reads.

graph tools

Operates on the full catalog: MCP (server__), user (user__), bundled packs (builtin__), plan (plan__), and plan_and_execute. list groups the catalog by source — names and one-line descriptions, plan_and_execute under (core). list --json emits the flat catalog instead, each entry carrying the namespaced name a plan step has to use, its source, the full description (untruncated — it is the routing signal), readOnly, and hasOutputSchema, plus a per-source roll-up. That is the surface for a script or an agent authoring a plan; per-tool schemas stay on tools show. show --json emits one tool’s whole definition — inputSchema included, which is what you need to write a step’s input object — with every key always present, null when absent. Unknown tool names stay ordinary errors (stderr, exit 1); there is no envelope for them. See the scripting contract. test --json wraps the call: {tool, isError, result}. A tool that reports an error still exits 0 — the call completed and its payload is the result worth reading, so isError carries the distinction the exit code does not. Without --json the tool’s own result is the deliverable, unwrapped.

graph threads

graph mcp

graph is an MCP client: each [mcp.<name>] entry becomes a set of <name>__* tools — see MCP servers. It is also an MCP server: graph mcp serve exposes every plan in the catalog as a callable tool, plus the plan authoring commands, so another agent can build and run graph plans. See graph as an MCP server.

graph shapes

Inspect the shape cache: list prints tool names and seen-counts, show prints one tool’s cached schema and example. The same data lives as plain files under <data_dir>/shapes/.

graph config

init refuses to overwrite an existing config; --force overwrites it. The starter ends with a [prompts] section carrying the built-in chat and workbench system prompts, written out verbatim so they can be tuned in place — delete a field to fall back to the shipped default.