The nodetool CLI is the TypeScript command-line interface for the NodeTool platform. It manages servers, workflows, jobs, assets, and secrets. Run nodetool --help to see the top-level command list. Every sub-command exposes its own --help flag with detailed usage.
Installation
Install globally from npm to get the nodetool and nodetool-chat commands:
npm install -g @nodetool-ai/cli
Or run a single command without installing:
npx --package=@nodetool-ai/cli nodetool --help
npx --package=@nodetool-ai/cli nodetool-chat
Requires Node.js 22 or later (engines in packages/cli/package.json, and the repository pins 24 in .nvmrc). Check with node --version, and install via nvm if needed.
Getting Help
nodetool --help— list all top-level commands.nodetool <command> --help— show command-specific options (e.g.nodetool serve --help).nodetool <group> --help— list sub-commands for grouped tooling (e.g.nodetool workflows --help).nodetool help [command]— the same output as--help, as a command (e.g.nodetool help serve). With no argument it lists the top-level commands.
Authoring tools and guides
Use the installed tool catalog and guides to start a document from the terminal. No agent session or MCP client is needed. These commands run locally against the same tool implementations as CLI agent sessions.
nodetool tools list timeline
nodetool tools info create_timeline
nodetool skills list motion
nodetool skills read motion-graphics
nodetool tools call create_timeline --args @arguments.json
tools list [query] prints names and descriptions. tools info <name> prints
its current input schema. tools call <name> --args <json|@file> executes it
with a JSON object. Results are JSON. Tool failures exit nonzero. Calls can save
work, execute jobs, or incur provider charges. The local database, secret store,
installed packs, current working directory, and agent budget settings are used
as in CLI agent sessions. Generation costs are recorded in the local ledger.
skills list [query] searches installed guides and user skills.
skills read <name> reads the full guide, including its examples and creation instructions.
Start with these guides and then inspect the named tools:
| Document | Guide | Tool discovery |
|---|---|---|
| Timeline or storyboard | storyboard-core |
tools list timeline |
| Animated text and shapes | motion-graphics |
tools list timeline |
| Reusable workflow | nodetool-workflow-builder |
tools list workflow |
| Interactive mini app | nodetool-app-builder |
tools list app |
| Playable game | native-game |
tools list game |
Call nodetool tools call list_projects to obtain an owned project id for
project-scoped documents. Local tool calls initialize the Personal project
(personal:1) on a fresh installation.
For a deterministic mini app build without provider charges, run:
nodetool packs add audio --yes
nodetool app build "greeting fixture" --provider fake --model fake-app-greeting --no-judge
The audio pack supplies the required nodetool.output.Output node.
fake-app-greeting builds a fixed greeting app and ignores the prompt. It tests
the build pipeline and interaction checks, not general app generation.
For games, list_example_games and get_example_game expose shipped starting
documents. Follow the guide’s installation instructions when a bundled example
uses packaged assets. For other documents, read the guide’s minimal example and
inspect the creation tool’s schema before saving it.
Global Options
These flags work on any nodetool command and control OpenTelemetry tracing:
--trace-file <path>— append every LLM/agent/workflow span to<path>as JSONL (analyzer-friendly).--trace-stdout [format]— stream spans to stdout:pretty(default) orjson.--no-trace-stdout— disable stdout span output (overridesNODETOOL_TRACE_STDOUT).--version— print the CLI version.
nodetool --trace-file trace.jsonl run workflow.ts
nodetool --trace-stdout pretty workflows run <id>
Core Commands
nodetool info
Display the CLI version, Node.js version, platform, which provider API keys are set (configured or not set, never the value), and the ENV, LOG_LEVEL and PORT environment values.
Options:
--json— output as JSON.
Example:
nodetool info
nodetool info --json
nodetool serve
Starts the TypeScript WebSocket + HTTP backend server. This serves the REST API, WebSocket endpoints, and static assets.
Options:
--host(default127.0.0.1) — bind address (use0.0.0.0for all interfaces).--port(default7777) — listen port.
Examples:
# Start the server on the default port
nodetool serve
# Bind to all interfaces on a custom port
nodetool serve --host 0.0.0.0 --port 8080
serve passes the two flags to the server as HOST and PORT, overwriting whatever those variables already held — use the flags, not the environment, to move the server.
nodetool workflows run <workflow_id_or_file>
Executes a workflow by ID (from the local database), JSON file, or TypeScript DSL file.
Arguments:
<workflow_id_or_file>— workflow ID, path to a.jsonworkflow file, or path to a.tsDSL file.
Options:
--params <json>— JSON string of workflow parameters.--json— output result as JSON.--asset-output-mode <mode>— how media outputs are materialized:native(default),raw,data_uri,workspace,storage_url, ortemp_url.--workspace <dir>— workspace directory forworkspacemode output (default./nodetool-output).--trigger-event <json>— wake a trigger node the way the scheduler or webhook adapter would, for example'{"node_id":"watch","payload":{"path":"/tmp/a.csv","event":"created"}}'. Without it a webhook, file-watch or manual trigger has no event to emit and the run stalls until its timeout.--superviseand its bounds — see Supervised runs.
Examples:
# Run workflow by ID
nodetool workflows run workflow_abc123
# Run workflow from JSON file
nodetool workflows run ./my_workflow.json
# Run workflow from TypeScript DSL
nodetool workflows run ./my_workflow.ts
# Run with parameters as JSON
nodetool workflows run workflow_abc123 --params '{"input": "hello"}'
# JSON output for automation
nodetool workflows run ./my_workflow.json --json
--json never expands a large binary output into the JSON text. An image,
audio, or video payload over 64 KiB is written to
nodetool-output/<job_id>/payload-N.<ext> and appears as
{"$file": "…", "bytes": N, "mimeType": "…"} in its place; the path is
reported on stderr. Image nodes emit raw RGBA as their in-flight format, so a
raw ref is PNG-encoded on the way out. nodetool run and nodetool node run
do the same, spilling into nodetool-output/.
nodetool workflows export-dsl <workflow_id_or_file>
Exports a workflow as a TypeScript DSL file.
Arguments:
<workflow_id_or_file>— workflow ID or path to a.jsonworkflow file.
Options:
--api-url <url>— fetch the workflow from a remote server instead of the local database (defaultNODETOOL_API_URL).-o, --output <file>— write to file instead of stdout.
Examples:
# Print DSL to stdout
nodetool workflows export-dsl workflow_abc123
# Write to file
nodetool workflows export-dsl workflow_abc123 -o workflow.ts
# Export from JSON file
nodetool workflows export-dsl ./my_workflow.json
nodetool workflows export-script <workflow_id_or_file>
Converts a workflow into a JavaScript script that calls each node in
dependency order, one awaited call per node. The editor’s workflow menu and
the workflows.exportScript tRPC call produce the same script.
Options:
-t, --target <target>—sandbox(default) writes a body for a JS script document or a Code node, calling nodes through@nodetool-ai/sandbox-nodetool/flow.nodewrites a standalone.mjsthat calls nodes in process through@nodetool-ai/dsl/flow, takes the workflow inputs as a JSON argument, reads provider keys from the environment, and prints the outputs as JSON.-o, --output <file>— write to file instead of stdout.
A node a script cannot express (a node that runs once per streamed item, a
control edge, a live input stream) is listed on stderr, and the script throws
before running any node. A graph with a cycle, such as a Loop, is refused.
nodetool workflows export-script workflow_abc123 --target node -o flow.mjs
npm install @nodetool-ai/dsl
node flow.mjs '{"prompt": "a red fox"}'
nodetool run <dsl-file>
Shorthand for running a TypeScript DSL workflow file directly.
Options:
--json— output results as JSON.--superviseand its bounds — see Supervised runs.
Examples:
nodetool run workflow.ts
nodetool run workflow.ts --json
Supervised runs
--supervise puts an agent on the failure path: when a node invocation throws
after its own error handling is exhausted, the agent sees the failure and
answers with one verdict — retry, repair the output, skip the item, or fail.
Without the flag nothing changes: no escalation is constructed and the run is
the run it was before.
Available on nodetool run, nodetool workflows run, and nodetool debug
(server surface).
Options:
--supervise— supervise this run. Off unless passed.--max-decisions <n>— decisions allowed in the run (default 10).--max-retries <n>— retries per node invocation (default 2).--supervisor-cost-cap <usd>— ceiling on supervisor spend (default 0.50), enforced by reservation before each model turn, not after.--supervisor-model <provider/model>— who supervises (defaultanthropic/claude-sonnet-4-6, orNODETOOL_SUPERVISOR_MODEL). The leading segment must be a registered provider; the rest is the model id, slashes and all (openrouter/openai/gpt-5.4-mini).
Passing a bound without --supervise is an error rather than a silent
unsupervised run.
Output. Each decision prints a ⛨ line as it happens, and the run ends
with a supervised summary:
⛨ fetch-item [3] skipped — HTTP 404 (agent, $0.0041)
⛨ supervised: 2 skipped, 1 retried, 3 decisions, +$0.0200
With --json, the decisions are in interventions — alongside the outputs in
workflows run, and under {results, interventions} in nodetool run, whose
bare results shape is left alone for unsupervised runs. nodetool debug puts
them in server.summary.interventions with a server.supervised rollup. Each
record carries the
escalation the agent saw (node, item lineage, redacted inputs, allowed
actions), the verdict, who decided it (agent, sticky, bounds, default,
kernel), and its cost.
Cost. Supervisor spend lands in the same ledger nodetool costs reads, one
row per billable decision, attributed to the run and tagged supervisor in
node_type — so supervision is separable from the workflow’s own spend:
nodetool costs list --limit 20 # supervisor rows show node_type=supervisor
Bounds are the guarantee. Every supervisor failure (timeout, an unparseable
verdict, an exhausted budget, a cancelled run) resolves as fail, which is
what would have happened without it. What each verdict means and why
retry is opt-in per node: workflow-supervisor-design.md.
Database Migrations
nodetool db migrate
Applies NodeTool migrations to a PostgreSQL/Supabase database. For Supabase, use the direct connection URL from Settings → Database (port 5432), not the transaction pooler URL.
Options:
--direct-url <url>— Supabase/PostgreSQL direct connection URL.--database-url <url>— connection URL; defaults toDIRECT_URLorDATABASE_URL.--target <version>— stop after a specific migration version.--dry-run— show pending migrations without applying them.--skip-checksums— skip checksum validation.--json— output as JSON.
Examples:
DIRECT_URL="postgresql://postgres:[password]@db.[project].supabase.co:5432/postgres" \
nodetool db migrate
nodetool db status --direct-url "$DIRECT_URL"
nodetool db migrate --direct-url "$DIRECT_URL" --dry-run
Other migration commands:
nodetool db status --direct-url "$DIRECT_URL"
nodetool db baseline --direct-url "$DIRECT_URL" # for existing DBs
nodetool db rollback --direct-url "$DIRECT_URL" --steps 1
status, baseline, and rollback take --direct-url, --database-url, and --json like migrate. baseline also takes --force to clear and recreate the migration tracking records. rollback --steps <n> defaults to 1.
Chat
nodetool chat
Starts fullscreen terminal chat with a scrollable transcript, multiline composer, tool details, task progress, and approval prompts. The previous terminal screen is restored on exit. Piped input keeps its line-based interface.
Options:
-p, --provider <provider>— LLM provider (e.g.,anthropic,openai,ollama).-m, --model <model>— model ID.-a, --agent— deprecated, no-op. Every chat session runs the unified agent loop; this flag has no effect.-u, --url <url>— WebSocket server URL (default: uses a local provider). With piped input, the command exits 1 when the server reports an error for any turn.-w, --workspace <path>— workspace directory for file operations (default: current directory).--no-read-only-search— disable the read-onlyrun_searchfan-out primitive (on by default).--tools <tools>— comma-separated tool names that narrow the belt. Without it, a local session offers the same belt as a server chat turn.--permission-mode <default|auto|plan>— how tool calls are gated (see Permission mode). Interactive chat defaults todefault, with approval prompts. Piped input defaults toauto.--resume [id]— resume a saved conversation in this workspace. Without an ID, resume the most recently saved session.--cost-cap <usd>— ceiling on provider spend for one turn;0lifts it. Default: theNODETOOL_AGENT_TURN_COST_CAP_USDsetting.--timeout <s>— wall-clock bound on one turn, in seconds;0leaves it no time at all. Default: theNODETOOL_AGENT_TURN_DEADLINE_MSsetting.
Both flags override two of the five NODETOOL_AGENT_* settings; the other
three — concurrency, total turns, unpriced-token ceiling — come from the
settings alone. The budget is one object per turn, shared by every loop the
turn starts, so a ceiling bounds the turn rather than each loop separately. A
turn a ceiling refuses prints the reason: [stopped] turn budget of $5 reached
piped, or a Stopped: line in the TUI. The terminal also displays the local
turn’s provider spend. Connected servers manage their own run budgets.
Fullscreen chat reserves stdout for its screen. Use --trace-file <path> for
tracing and --no-trace-stdout when stdout tracing is enabled in the environment.
Enter sends a message. Alt+Enter, Shift+Enter in supported terminals, or Ctrl+J inserts a newline. Pasted lines stay in the composer. Up/Down recalls prompts, Tab completes commands. Page Up/Page Down and the mouse wheel scroll the transcript. Drag across transcript text to select and copy it to the clipboard on release. Ctrl+G returns to the latest output. Ctrl+O toggles tool details and edit diffs. Escape stops a running turn or dismisses input. Ctrl+C stops a turn, clears a draft, or saves and quits when idle with an empty composer.
Use /sessions and /resume <id> to continue a conversation, /export <path.md>
to save a transcript, and /mode to change permissions. /clear clears the
screen while retaining context. /new starts a fresh conversation. Local
/compact [instructions] retains a summary. Remote context is managed by the
server. /help lists all commands.
Sessions are saved under ~/.nodetool/chat-sessions/ after turns and on exit.
They include conversation context and provider continuation state. The session
picker filters by workspace and server. Model and provider selections persist
in ~/.nodetool/chat-settings.json. Remote /model <id> accepts a server model
ID without requiring a local API key.
Examples:
# Start interactive chat
nodetool chat
# Chat with a specific provider and model
nodetool chat --provider anthropic --model claude-sonnet-5
# Connect to a running server
nodetool chat --url ws://localhost:7777/ws
Workflow Management
nodetool workflows
Manage workflows. Reads the local database by default; --api-url targets a remote server.
Subcommands: list and get (both take --api-url <url> and --json, and list takes --limit <n>, default 100), run, export-dsl, export-script, export-example, export-bundle, import-bundle,
migrate-code-inputs
# List all workflows
nodetool workflows list
nodetool workflows list --api-url http://localhost:7777 --json
# Get a workflow by ID
nodetool workflows get <workflow_id>
# Run a workflow (see above for full options)
nodetool workflows run <workflow_id_or_file>
# Export as a TypeScript DSL file (see above)
nodetool workflows export-dsl <workflow_id_or_file>
nodetool workflows export-example <workflow_id_or_file>
Export a workflow as a shipped template: materialize its referenced assets into the package’s constant asset directory
(rewriting refs to package://<pkg>/<file>) and write the example JSON.
Options:
--api-url <url>— fetch the workflow and asset bytes from a remote server instead of the local database (defaultNODETOOL_API_URL).--package <name>— owning package (defaultnodetool-base).--assets-dir <dir>— root holding<package>/<file>constant assets (default: the monorepo base-nodes assets directory).--examples-dir <dir>— directory the example JSON is written under, as<dir>/<package>/<name>.json.-o, --output <file>— write the example JSON to this exact path.--include-remote— also materialize http(s) and local-file refs.
nodetool workflows export-example <workflow_id>
nodetool workflows export-example <id> --package nodetool-base
nodetool workflows export-example workflow.json -o example.json
nodetool workflows export-bundle <workflow_id_or_file...>
Export one or more workflows as a portable .nodetool bundle (a zip containing the graphs plus the bytes of every asset
they reference), sharable as a single file.
Options:
--api-url <url>— fetch the workflows and asset bytes from a remote server instead of the local database (defaultNODETOOL_API_URL).-o, --output <file>— output path (default<name>.nodetool).--include-remote— also embed http(s) and local-file refs.
nodetool workflows export-bundle <id> [<id2> ...] -o my-pack.nodetool
nodetool workflows import-bundle <bundle_file>
Import a .nodetool bundle into the local library: store its assets and create the workflows with refs rewritten to the
imported assets.
Options:
--json— print the created workflows as JSON.
nodetool workflows import-bundle my-pack.nodetool
nodetool workflows migrate-code-inputs
A one-time repair for saved nodetool.code.Code bodies. A Code node’s declared
inputs used to arrive as globals of their own name; they now arrive on one
inputs object, so a body written the old way throws a ReferenceError on its
first input read. This walks the saved workflows and rewrites name to
inputs.name for every name the node can read — its declared slots, its inline
dynamic properties, and any handle an edge feeds it.
The rewrite is done on the AST, so a name inside a string, a comment, an object
key, or a local binding is left alone. It is safe to re-run: a body already
reading inputs.* has nothing to rewrite.
Options:
--dry-run— report what would change and write nothing.--user-id <id>— migrate this user’s workflows instead of the local user’s (1).--json— print the report as JSON rather than the per-node lines.
# See what would change first
nodetool workflows migrate-code-inputs --dry-run
# Then do it
nodetool workflows migrate-code-inputs
Each rewritten node prints one line naming the inputs it moved, followed by a count:
4 Code node(s) in 3 workflow(s) were rewritten (11 scanned across 7 workflows).
A body that fails to parse is counted under a trailing N failed. line and left
untouched, so one bad node does not stop the pass.
nodetool validate <workflow_id_or_file>
Check a workflow against the node registry without running it: unknown node
types, missing required properties, dangling or mis-typed edges, model
properties naming a provider or model id that does not exist,
and nodetool.code.Code bodies that never return or leave an output unset. On
a workflow-id target, where the secret store is reachable, it also warns about
declared credentials the install cannot resolve. It finishes in well under a
second, which makes it the cheap pre-flight before a run that costs money.
The target is a workflow id, a workflow JSON file, or a TypeScript DSL file. File targets need no database.
Options:
--json— print the full validation report as JSON.--warnings-as-errors— exit non-zero on warnings, not just errors.
Examples:
nodetool validate <workflow_id>
nodetool validate workflow.json
nodetool validate workflow.ts --json
nodetool validate <workflow_id> --warnings-as-errors
A clean graph prints a one-line verdict and exits 0:
✅ Workflow is valid — 3 node(s), 2 edge(s).
A broken one names the node and the rule it broke, then exits 1:
❌ Workflow has 1 error(s).
error Unknown node type "nodetool.text.NoSuchNode" (not in the registry; Python-only nodes are not validated statically) [nodetool.text.NoSuchNode 1] (unknown_node)
Codes:
unknown_node — node type is not in the registry
nodetool debug <workflow_id_or_file>
Run a workflow end to end and collect everything it emitted into a
self-contained bundle — every message, log line, node input/output, and error —
then print a verdict. Reach for it when validate passes but the run still does
the wrong thing.
The headless server run is on by default. The expensive surfaces are opt-in:
--browser starts Playwright and Chromium, --trace adds OpenTelemetry
spans, and --stages screenshots the canvas at each run stage.
Options:
--no-server— skip the headless server surface.--browser— also run the workflow in a real browser (Playwright).--trace— capture an OpenTelemetry trace of the server run (timing, tokens, cost).--stages— screenshot the canvas at every browser run stage (implies--browser).--params <json>— JSON params keyed by input-node name.--out <dir>— bundle output directory (defaultnodetool-debug/<id>-<timestamp>).--timeout <ms>— per-surface run timeout.--json— print the fullDebugReportas JSON to stdout.--watch— re-run on file change and print a diff of the verdict (file targets only).--superviseand its four bounds — see Supervised runs above.
Examples:
# Server surface only
nodetool debug <workflow_id>
nodetool debug workflow.json --params '{"prompt":"hi"}'
# Opt into the expensive surfaces
nodetool debug <workflow_id> --trace
nodetool debug <workflow_id> --browser --stages
# Tight edit→verify loop on a file target
nodetool debug workflow.json --watch
The bundle holds report.json and report.md, the resolved workflow.json,
and server/messages.jsonl. --trace adds server/trace.jsonl; --browser
adds browser/record.json, a canvas screenshot, and browser/console-errors.log.
nodetool node list [query], nodetool node info <node_type>
List executable TypeScript nodes from installed packs, optionally filtering by
node type or title. --json returns complete metadata. info prints the exact
node type’s properties, defaults, inputs and outputs as JSON. These commands
use the same installed pack registry as node run and work outside a checkout.
Catalog entries for missing packs are excluded.
nodetool node list text
nodetool node list --json
nodetool node info nodetool.text.Concat --json
Use nodetool packs list to inspect pack installation state. Python package
metadata is inspected separately through nodetool package list.
nodetool node run <node_type>
Instantiate one node, hand it a property bag, and print what it emits — no
workflow, no graph. Use it when debug says a run went wrong and you want to
know whether one node is the reason.
Options:
--props <json>— JSON object of property values keyed by@propname (default{}).--no-secrets— skip secret and asset resolution, so the run touches no database.--json— print the full run result as JSON.
Examples:
nodetool node run nodetool.text.Concat --props '{"a":"hi ","b":"there"}'
nodetool node run nodetool.text.Concat --props '{"a":"hi ","b":"there"}' --no-secrets
nodetool node run nodetool.text.Concat --props '{"a":"hi ","b":"there"}' --json
The verdict names the node type, its title, and how long it took, then lists every record it emitted:
✅ nodetool.text.Concat (Concat) — 0ms
Emitted 1 record(s):
{"output":"hi there"}
Mini App Management
nodetool apps
Move mini apps between installs. Reads and writes the local database directly — no running server.
Subcommands: list, export-bundle, export-project, import-bundle
# List applications with their operation count
nodetool apps list
nodetool apps list --json
list prints id, name, operations, and updated_at.
nodetool apps export-bundle <application_id>
Export an application as a portable bundle: the app document plus the full graph of every workflow its operations bind,
in a single JSON file. Inside the file an operation’s workflowId is a bundle-local key, not a real workflow id.
Options:
-o, --output <file>— output path (default<name>.app.json, with unsafe characters replaced by_).--released— export the released snapshot and the graphs it pinned, not the draft. Fails when the app has no released version.
The bundle path goes to stdout and the summary line to stderr, so $(nodetool apps export-bundle <id>) captures the
path alone. An operation binding a workflow that no longer exists is left unresolved in the bundle and reported as a
warning.
nodetool apps export-bundle <application_id>
nodetool apps export-bundle <application_id> -o my.app.json --released
nodetool apps export-project <application_id|bundle_file>
Export an application as a standalone JavaScript project that runs without NodeTool: a Vite + React UI and a Node
server that runs the app’s workflows and JS scripts in process, with no database. Takes an application id from the local
database or an exported .app.json bundle. See @nodetool-ai/app-export for what
the project contains.
Options:
-o, --output <dir>— output folder (default./<app-name>).--released— export the released snapshot and the graphs it pinned, not the draft.--force— write into an output folder that is not empty.
.env.example lists the credentials the graphs and scripts need. When a graph leaves a model for the person to pick, it also lists
every provider key, commented out. Sample inputs referenced as package:// are copied under assets/. What the
exported app cannot do (resource documents, editor-only widgets) is printed as warnings and listed
in the project’s README.
nodetool apps export-project <application_id> -o ./my-app
nodetool apps export-project my.app.json
cd my-app && npm install && cp .env.example .env && npm run dev
nodetool apps import-bundle <bundle_file>
Import an application bundle into the local library: create its workflows and the app, with operations rewired to the new ids.
Options:
--project <id>— project to create the app in (defaultdefault).--json— output the created application as JSON.
A bundled workflow carrying a sourceId gets a row id derived from it, so importing two bundles that ship the same
workflow — two example apps binding one template — reuses the existing row instead of duplicating it. Without a
sourceId every import creates fresh workflows.
nodetool apps import-bundle my.app.json
nodetool apps import-bundle my.app.json --project my-project --json
nodetool app
Where nodetool apps moves finished apps around, nodetool app runs one and
builds one. Both subcommands are headless — no browser, no editor — and both
write a debug bundle under nodetool-debug/.
nodetool app debug <application_id_or_file>
Run a mini app the way the web runtime would: validate every widget binding against the workflows it binds, seed input defaults, apply params, replay a scripted interaction sequence, execute the workflows on the kernel, fold the streamed messages into the app’s reactive values, and report what each widget ends up showing.
The target is an application id (read from the applications table), an
ApplicationBundle JSON file (self-contained — operations reference bundle
keys, so it runs without a database), or a workflow id/file carrying a legacy
app_doc.
Options:
--params <json>— reactive values applied before interactions, keyed by input name. Aresource:<binding id>key seeds that collection with an array of items.--interact <json>— scripted steps:set,change,click,run(by operation id),cancel,seedResource.--no-run— static wiring check only; never execute a workflow.--out <dir>— bundle directory (defaultnodetool-debug/app-<id>-<timestamp>).--timeout <ms>— per-run timeout.--json— print the fullAppDebugReportto stdout.
Examples:
# Static wiring check — no provider calls, no workflow execution
nodetool app debug my.app.json --no-run
nodetool app debug <application_id> --params '{"prompt":"hi"}'
nodetool app debug <application_id> --interact \
'[{"set":{"key":"prompt","value":"hi"}},{"run":"draft"}]'
The verdict catches what a workflow-only run cannot: a widget bound to a token
the workflow has no input, output, node, or variable for, one naming an
operation the document never declares, an unknown widget type, a display widget
that never received a value, an app no interaction ever ran, an operation that
overran its timeout, and a click or change on a widget its own visibleWhen /
disabledWhen hides or disables — that step fails and names the condition.
Every declared operation runs, not just the first, and state is keyed per
operation.
The report lists what it did not simulate under notSimulated: layout, styling,
focus, and scroll, since nothing renders a DOM; the stored resource collections,
since a run reads the seeded in-memory provider rather than the database; and
reactive subgraph runs, where the browser reruns one input’s downstream subgraph
and the harness runs the whole workflow.
nodetool app build <prompt_or_spec_file>
Turn a prompt, or a hand-written spec.json, into a verified
ApplicationBundle. Six stages run in order: spec pins what the app must
do, plan builds one workflow per operation, author places and wires the
widgets through the real ui_app_* tool contract, check validates the
wiring, run replays every interaction on the kernel, and judge asks a
model whether each interaction achieved what was asked. Everything still wrong
at the end of a pass becomes one complaint, and the next round edits the
document rather than rebuilding it.
Plan, author, and judge each call a model, and run executes real workflows, so a build spends money and takes minutes.
Options:
-p, --provider <name>/-m, --model <id>— the builder’s provider and model.--judge-model <provider/model>— model that judges each interaction (envNODETOOL_APP_JUDGE_MODEL). Defaults to a configured model other than the builder’s, because a model grading its own work is the weakest reviewer available.--workflow <id>— pin an existing workflow instead of planning one (repeatable, in operation order).--max-repairs <n>— repair rounds after the first pass (default3).--cost-cap <usd>— ceiling on build spend (default2).--timeout <ms>— wall clock for the whole build (default600000).--out <dir>— bundle directory (defaultnodetool-debug/app-build-<slug>-<timestamp>).--json— print the fullBuildReportto stdout.--no-judge— skip the judge stage. The verdict’snotSimulatedthen says nothing scored the app.--watch— re-build on spec-file change and print a diff of the verdict (spec-file targets only). Every save spends money.--supervise,--max-decisions <n>,--max-retries <n>,--supervisor-cost-cap <usd>,--supervisor-model <provider/model>— the same flags described under Supervised runs. They apply to the Run stage, whose interactions execute on the kernel;buildAppitself is never supervised.
Examples:
nodetool app build "an app that drafts a note from a prompt" \
-p anthropic -m claude-sonnet-5
nodetool app build spec.json -p openai -m gpt-5.4-mini --json
# Bind an existing workflow instead of planning one
nodetool app build "..." -p anthropic -m claude-sonnet-5 --workflow <id>
# Structural check only, one repair round, tight cap
nodetool app build spec.json -p anthropic -m claude-sonnet-5 \
--no-judge --max-repairs 1 --cost-cap 1.00
The bundle holds report.json, report.md, spec.json,
interactions/<name>/run-N.messages.jsonl per replayed run, and — only for a
green build — app.bundle.json, the deliverable. The loop fails closed: an
exhausted budget, an issue that reappears after being fixed, or a cancelled
signal ends the build as failed with the reason named, and there is no bundle
behind a failed verdict. Exit code is 0 only when verdict.ok.
Editor Documents
Timelines, sketches, and JS scripts each get the same three commands:
validate checks the document without rendering or running it, debug replays
a scripted edit session against it, and versions reads and writes its snapshot
history. All of them take a JSON file or a row id — a path that exists on disk
wins over an id, and file targets need no database.
nodetool timeline
Subcommands: validate, debug, render, score, versions
nodetool timeline validate <timeline_id_or_file>
Check a timeline sequence without rendering it: clips on tracks the document
does not have, fields a schema round trip would strip, animation presets that
do not exist, and timings nothing can render. The target is a timeline JSON
file — a bare TimelineDocument or anything carrying one under document, so
a timeline.get response works as-is — or a timeline_sequences row id.
Options:
--json— print the fullTimelineValidationas JSON.--warnings-as-errors— exit non-zero on warnings, not just errors.
nodetool timeline validate sequence.json
nodetool timeline validate <timeline_id> --json
nodetool timeline validate <timeline_id> --warnings-as-errors
✅ 0 error(s), 0 warning(s)
Every finding carries a stable code. An error is a document no reading of
which produces the scene its author described; a warning is a picture that
renders and is not the one that was written — usually because this build could
not read something a newer one wrote (forward compatibility), or because the
authored motion did not fit the clip.
| Code | Severity | What it caught |
|---|---|---|
schema_invalid |
error | The document does not parse; every structural check is skipped |
duplicate_id |
error | Two tracks, clips or markers share an id |
clip_track_missing |
error | A clip sits on a track the document does not declare |
negative_timing |
error | A clip or marker starts before the origin, or a clip lasts zero |
fade_exceeds_duration |
error | The in and out audio fades overlap |
in_out_points_invalid |
error | The source span is negative or empty |
speed_multiplier_invalid |
error | A non-positive playback rate |
unknown_animation_preset |
error | A preset this build does not ship; nothing animates |
custom_animation_invalid |
error | Baked curves the one gate refuses; re-bake from the script |
animation_link_source_missing |
error | A clip links animation to a clip the document lacks |
animation_style_invalid |
error | An animation uses glyph tracks without a stagger unit |
text_path_invalid |
error | A text clip’s path cannot be read |
shape_path_invalid |
error | A path shape this build cannot draw, so it draws nothing |
midi_clip_off_midi_track |
error | A clip carrying notes sits on a non-midi track, so it makes no sound |
non_midi_clip_on_midi_track |
error | A picture or audio clip sits on a midi track, so it is neither heard nor seen |
midi_clip_retimed |
error | A midi clip carries speed or time remap, which retime samples a midi clip does not have |
midi_notes_invalid |
error | A midi clip holds a note that fails its checks |
parent_cycle |
error | A parentId chain loops, so the group cannot be resolved |
matte_source_missing |
error | A matte names a clip the document lacks, or itself — the layer draws unmatted, showing everything the matte was hiding |
time_remap_not_monotonic |
error | timeRemap keyframe t repeats or goes backwards (sourceMs may descend — that is a reverse) |
field_stripped |
warning | A field the schema drops, lost on the next save |
in_out_duration_mismatch |
warning | Source span and timeline duration disagree at the clip’s rate |
transition_exceeds_duration |
warning | The cut is longer than the clip carrying it |
unknown_transition |
warning | A transition type or direction this build cannot draw; it cross-fades left |
unknown_easing |
warning | An easing outside the grammar; it eases linearly |
generated_matte_stale |
warning | A generatedMatte cut from an asset the clip no longer plays, or from less source than its window now shows; regenerate it |
unknown_effect |
warning | A clip effect type this build cannot apply; the layer draws ungraded |
mask_path_invalid |
warning | A mask kind or path d that cannot rasterize; the layer draws unmasked |
unknown_shape_kind |
warning | A shapeStyle.kind this build has no geometry for; the shape draws nothing |
font_not_portable |
warning | A font family NodeTool does not ship; every host resolves it against its own installed fonts, so the editor preview and the render can differ |
matte_source_invalid |
warning | A clip is matted by an adjustment clip, which has no pixels, so the layer draws unmatted |
crop_degenerate |
warning | A crop whose left and right, or top and bottom, sum to 1 or more, so the clip draws its whole source |
bake_stale |
warning | A bake rendered from a style the clip no longer has, so the live 3D layer draws instead |
reframe_stale |
warning | Automatic framing that no longer matches the active source, so rendering falls back to centred framing |
source_curve_outside_window |
warning | Animation keyframes cut from source outside the span the clip plays |
adjustment_no_effects, adjustment_treats_nothing, adjustment_group_empty |
warning | An adjustment clip with no enabled effect, with no clip beneath it in its window, or in a group where nothing else overlaps it |
adjustment_field_ignored |
warning | An adjustment clip carries fields it never draws |
layout_on_non_group, unknown_layout_display, flex_item_without_flex_parent |
warning | A layout on a clip that is not a group, a layout.display other than flex, or a flexItem whose parent is not a flex container. Each is ignored |
midi_clip_silent |
warning | A midi clip with no notes, or none starting inside its window |
midi_tempo_missing |
warning | The document has midi but no tempo, so notes read at the default BPM |
text_backing_unproven |
warning | Text over picture with no scrim, plate or outline, so nothing decides whether it is readable |
parent_missing, parent_not_group |
warning | A parentId naming nothing, or naming a clip that is not a group; the child renders unparented |
layer_cap_exceeded |
warning | More video clips overlap at an instant than the compositor draws |
animation_exceeds_clip |
warning | The window does not fit the clip after its delay, so the motion is clamped — or never runs |
stagger_compressed |
warning | The stagger span did not fit, so the per-unit offset was shrunk and the units overlap more than authored. At 0ms every unit moves in sync |
effect_animate_ignored |
warning | animate on a generator or stylize mode whose shader never reads the clock, so it stays still, or on a stylize gradientWipe, whose map it switches |
effect_washes_out |
warning | A color effect’s brightness (an offset, not a multiplier) drives the picture to white or black, or its contrast removes nearly all of it |
animation_link_overrides |
warning | A link sets its channel outright and discards the clip’s own animation there, or a wiggle discards the clip’s placement |
typewriter_not_staggered |
warning | A typewriter is stored with a plain durationMs and no stagger, so the whole text appears at once after that time, with no caret |
animation_holds_rest_before_window |
warning | A custom out curve starts away from the channel’s rest value after clip start, so the frames before its window show the rest value and then jump |
replace_curves_overlap |
warning | Two animations drive one absolute channel (positionX/Y, anchorX/Y, trimStart/End) at the same time; the last in document order wins and the other is discarded |
text_illegible |
warning | Type under 2.5% of frame height, or under a 3:1 contrast ratio against its own background plate or a full-frame shape clip behind it |
clips_overlap, clip_shorter_than_frame, caption_out_of_range, binding_incomplete, duplicate_track_index, transcript_clip_missing, link_partner_missing |
warning | Structural smells that still render |
text_illegible refuses to guess: a colour notation it cannot read, a
translucent plate, gradient-filled type, or a backdrop it cannot prove sits
behind the text ends the contrast check rather than producing a finding.
nodetool timeline debug <timeline_id_or_file>
Run the same check, then execute each scripted step against the headless
ui_timeline_* bridge and validate the document the session left behind. A
step names a tool with or without the ui_timeline_ prefix. A failing step is
recorded and the script continues, so one bad target does not hide everything
after it. Rendering, playback, decode, and generation are not simulated; the
report lists that under notSimulated.
Options:
--interact <json>— the scripted steps, e.g.[{"tool":"add_track","input":{"type":"audio"}}].--out <dir>— bundle output directory (defaultnodetool-debug/timeline-<id>-<timestamp>).--json— print the fullTimelineDebugReportas JSON to stdout.
nodetool timeline debug sequence.json \
--interact '[{"tool":"add_track","input":{"type":"audio","name":"Music"}}]'
✓ ui_timeline_add_track
✅ Timeline is sound — 1 interaction(s) ran clean.
timeline: 2 track(s), 1 clip(s), 4000ms @ 30fps
session: 1 step(s), 0 failed
The bundle holds report.json, report.md, and timeline.json (the input
document). The command exits 0 only when the verdict is ok.
nodetool timeline render <timeline_id_or_file>
Render a timeline through the GPU compositor that the Render Timeline
node uses. The target is a timeline JSON file (a bare document, or a wrapper
with document, fps, width, height and durationMs, such as an example
bundle) or a timeline_sequences row id. Clip media resolves through the
local asset store. Video exports mix audio-track clips, MIDI clips, and embedded
video audio. Stills, contact sheets, PNG sequences, and video frame selections
carry no audio.
Use --stills to check a render frame by frame. The Canvas 2D frame preview
is a second renderer, so only this command shows the exported pixels.
Options:
--out <path>— output file, or an output directory with loose--stillsor--sheet(defaultnodetool-render/<name>.<ext>,<name>-frames/, or<name>-sheet.png).--format <format>—mp4(default),webm,movorpng_sequence.--frames <spec>— render only these frames: indices and inclusive ranges, e.g.0-89or175,290,380-390. Usecoverage:<file>to read theFramecolumn of a Markdown table in a builder file’s leading comment. Non-integer rows are skipped and reported.--stills— write each selected frame asframe_<index>.pnginstead of encoding a video.--sheet [columns]— write one PNG contact sheet instead of loose stills. It labels each frame and defaults to three columns. Select at most 60 frames.--sheetimplies still-image output.--only <clips>— render clips selected by comma-separated names or ids. Selected groups include their children; other selections retain their ancestor groups and matte sources.--scale <n>— render at this fraction of the sequence size. The layout is the same as at full size.--bitrate <bps>— video bitrate in bits per second.--json— print the result as JSON.
nodetool timeline render sequence.json --out ad.mp4 --bitrate 12000000
nodetool timeline render <timeline_id> --stills --frames 175,290,376-384 --scale 0.5
nodetool timeline render sequence.json --frames coverage:build-sequence.ts --sheet --out review.png
nodetool timeline render sequence.json --only title,logo --frames 60,120 --sheet 2
The command needs ffmpeg for video output and a WebGPU adapter. On a headless machine, install a Vulkan ICD such as lavapipe (headless WebGPU setup). The first frame compiles the GPU pipelines, so progress starts after a delay.
nodetool timeline score <timeline_id_or_file>
Score a timeline document’s craft against the shipped showcase examples
(cadence, kite, prism, serein, tidewater, voltra —
packages/base-nodes/nodetool/examples/timelines/*.timeline.json,
excluding the footage-based t-minus-30) — a deterministic, no-LLM
scorecard, not a judge call. It measures motion density (animations and
keyframes per visible second), breadth (distinct animated properties, effect
types, showcase-only feature flags), and structural craft (style tracks, text
animators, authored transitions, scene count), each against the reference
examples’ median, plus a fixed penalty per distinct showcase-tier warning
code present (the showcase_* codes that the showcase validation tier adds, which timeline validate does not run) — four findings of the
same code cost as much as one, so no single noisy check can dominate the
score; instance counts stay visible in showcaseWarnings and
showcaseWarningCodeCounts. The target is a timeline JSON file or
a timeline_sequences row id, same resolution as timeline validate.
Options:
--json— print the full score result (every metric, its reference median and credit, the reference band, and the underlying validation) as JSON.--min-score <n>— exit non-zero when the score is below this threshold. Without it the command always exits 0 — the score is information, not a gate.--sheet <out.png>— also render a GPU contact sheet of 12 evenly spaced frames, through the same renderertimeline render --sheetuses.
nodetool timeline score <timeline_id>
nodetool timeline score sequence.json --json
nodetool timeline score <timeline_id> --min-score 70
nodetool timeline score <timeline_id> --sheet review-sheet.png
See Harness Reference
for the score formula and scripts/motion-craft-eval.mjs, the eval loop that
runs an agent brief and scores what it produced.
nodetool timeline versions
Read and write a sequence’s snapshot history against the local database:
manual saves, the autosaves timeline.update writes at most every five
minutes, and the pre-restore snapshot that makes a restore undoable.
Subcommands: list, show, create, restore, delete. All five take --json.
list <timeline_id>— newest first;--save-type <manual|autosave|restore>,--limit <n>(default 100).show <timeline_id> <version>— one version’s metadata and the document it stored.create <timeline_id>— snapshot the sequence as it stands now;--name <name>labels it.restore <timeline_id> <version>— snapshot the current state first, write the old document back, then re-validate it.delete <timeline_id> <version>—-y, --yesskips the confirmation prompt.
nodetool timeline versions list <timeline_id> --save-type manual --limit 10
nodetool timeline versions show <timeline_id> 3 --json
nodetool timeline versions create <timeline_id> --name "before the recut"
nodetool timeline versions restore <timeline_id> 3
nodetool timeline versions delete <timeline_id> 3 --yes
An old document is restored against today’s schema, so what it used to pass is not what it passes now: a restore whose document no longer validates prints the issues and exits non-zero.
nodetool sketch
Subcommands: validate, debug, render, versions
nodetool sketch validate <sketch_id_or_file>
Check a sketch (image document) without opening an editor: a duplicate layer
id, an activeLayerId or binding pointing at a layer the document lacks,
opacity or a blend mode no compositor ships, a binding with no workflow or
prompt behind it, and fields a schema round trip would strip. The target is a
{sketch, layerBindings} JSON file — or anything carrying one, so a
sketch.get response works as-is — or an image_documents row id. Layer
bitmaps stay opaque to the check.
Options:
--json— print the fullSketchValidationas JSON.--warnings-as-errors— exit non-zero on warnings, not just errors.
nodetool sketch validate sketch.json
nodetool sketch validate <image_document_id> --json
nodetool sketch debug <sketch_id_or_file>
Run the same check, then execute each scripted step against the headless
ui_sketch_* bridge (the prefix is optional) and validate the document the
session left behind. Pixels, painting, rendering, generation, and asset I/O are
not simulated; the report lists that under notSimulated.
Options:
--interact <json>— the scripted steps, e.g.[{"tool":"add_layer","input":{"name":"Glow"}}].--out <dir>— bundle output directory (defaultnodetool-debug/sketch-<id>-<timestamp>).--json— print the fullSketchDebugReportas JSON to stdout.
nodetool sketch debug sketch.json \
--interact '[{"tool":"add_layer","input":{"name":"Shadow"}},
{"tool":"set_layer_props","input":{"target":"Shadow","opacity":0.4,"blendMode":"multiply"}}]'
✓ ui_sketch_add_layer
✓ ui_sketch_set_layer_props
✅ Sketch is sound — 2 interaction(s) ran clean.
sketch: 2 layer(s), 0 binding(s), 1024x768
session: 2 step(s), 0 failed
The bundle holds report.json, report.md, and sketch.json.
nodetool sketch render <sketch_id_or_file>
Render a sketch with the editor’s compositing rules and write a PNG, or a
layered PSD. The target is the same as validate. Layer images stored as
asset:// resolve through the local asset store. The command exits non-zero
when a layer’s image could not be read.
Options:
-o, --out <file>— output file (default: the sketch’s name, slugged, with.pngor.psd).--format <png|psd>—png(default) or a layeredpsdat the canvas size.--width <px>,--height <px>— output size. One alone keeps the aspect ratio. PNG only.--background <color>— CSS color under the image. Default transparent. PNG only.--layer <layer_id>— one layer alone, without its opacity or blend mode. PNG only.--mask— the inpainting mask layer. PNG only.--json— print the render report (size, output path, per-layerdrawnandissue).
nodetool sketch render <image_document_id> -o poster.png --width 1024
nodetool sketch render sketch.json --format psd -o poster.psd
nodetool sketch versions
The same five subcommands as timeline versions, against an image document’s
history. The per-layer generation takes in the editor are a different thing:
those record one generated image on one layer, these snapshot the whole
document.
nodetool sketch versions list <image_document_id> --save-type manual --limit 10
nodetool sketch versions show <image_document_id> 3 --json
nodetool sketch versions create <image_document_id> --name "before the repaint"
nodetool sketch versions restore <image_document_id> 3
nodetool sketch versions delete <image_document_id> 3 --yes
nodetool jsscript
A JS script is a named, versioned script document: a body plus declared ports,
secrets, a timeout, and saved test cases. Commands that take a script target
accept a script JSON file (a bare JsScriptDocument or anything carrying one
under document) or a js_scripts row id.
Subcommands: schema, example, validate, run, test, debug, versions
nodetool jsscript schema and example
Print the document JSON Schema or a runnable document with a saved test. Both write JSON to stdout and need no database or provider credentials.
nodetool jsscript schema
nodetool jsscript example > script.json
nodetool jsscript validate script.json --warnings-as-errors
nodetool jsscript test script.json
Saved cases use expect for final output values and expectedStreamed for
ordered {name, value} emissions. Unknown case fields, including expected,
are errors so a misspelled assertion cannot pass without being graded.
nodetool jsscript validate <script_id_or_file>
Check the body’s syntax, its imports against the installed pack catalog,
undefined names, undeclared inputs.* reads, outputs no emit/output call
reaches, duplicate or non-identifier port names, and tests naming ports the
script does not declare. A body that declares outputs and returns them instead
of emitting them is an error — a script has no legacy return contract. Zero
saved tests, and a declared secret this install lacks, are warnings.
Options:
--json— print the fullJsScriptValidationas JSON.--warnings-as-errors— exit non-zero on warnings, not just errors.
nodetool jsscript validate script.json
nodetool jsscript validate <js_script_id> --warnings-as-errors
nodetool jsscript run <script_id_or_file>
Validate the document, then execute the body once in the QuickJS sandbox and
print its outputs, streamed emits, logs, and error. Declared outputs must use
await output(name, value) or await emit(name, value). A body that reads its
inputs with stream is fed with --input-streams instead of --inputs. A
staged handle the script does not declare is refused.
Options:
--inputs <json>— input values, e.g.'{"a":1}'.--input-streams <json>— items staged per handle, e.g.'{"nums":[1,2,3]}'.--json— print the run result as JSON.
nodetool jsscript run script.json --inputs '{"numbers":[1,2,3]}'
nodetool jsscript run script.json --input-streams '{"numbers":[1,2,3]}'
✅ ran in 1032ms
outputs: {"total":6}
streamed: [{"name":"running","value":1},{"name":"running","value":3},{"name":"running","value":6}]
nodetool jsscript test <script_id_or_file>
Validate the document, then run its saved test cases and grade each one. Exit non-zero when validation or any case fails.
Options:
--json— print the grade report as JSON.
nodetool jsscript test script.json
nodetool jsscript test <js_script_id> --json
✅ 2 passed, 0 failed
✓ sums three numbers
✓ sums an empty list to zero
nodetool jsscript debug <script_id_or_file>
Replay each scripted step against the headless ui_jsscript_* bridge (the
prefix is optional) and validate the document the session left behind. A failed
run or test result fails the interaction, report verdict, and command exit status.
The editor, persistence of a debug session, and secret values are not simulated.
Options:
--interact <json>— the scripted steps, e.g.[{"tool":"set_code","input":{"code":"..."}}].--out <dir>— bundle output directory (defaultnodetool-debug/jsscript-<id>-<timestamp>).--json— print the fullJsScriptDebugReportas JSON to stdout.
nodetool jsscript debug script.json \
--interact '[{"tool":"set_code","input":{"code":"await output(\"n\", 1);"}}]'
nodetool jsscript versions
The same five subcommands as timeline versions, against a script’s history.
nodetool jsscript versions list <js_script_id> --limit 10
nodetool jsscript versions create <js_script_id> --name "before the rewrite"
nodetool jsscript versions restore <js_script_id> 3
nodetool game
Validate, replay, capture, and export a native game document from a JSON file.
Simulation advances exactly the requested number of 60 Hz ticks. The input
recording is a JSON array of { "pressed": ["right"] } frames, one per tick.
nodetool game validate game.json
nodetool game simulate game.json --ticks 120 --inputs inputs.json --expect-score 1 --expect-win
nodetool game simulate game.json --ticks 120 --inputs inputs.json --assertions assertions.json --verify-replay
nodetool game capture game.json --ticks 120 --inputs inputs.json --out frame.png
nodetool game capture game.json --ticks 120 --out frame.png --backend webgpu
nodetool game build game.json --out game-build --assets-dir game-assets
nodetool game build game.json --out game-desktop --assets-dir game-assets --target tauri --name "Cave Run" --compile
nodetool game smoke game-build --json
Subcommands and options:
game validate <game_file>—--json.game simulate <game_file>—--ticks <count>(required),--seed <integer>(default1),--inputs <file>,--assets-dir <directory>(prepared 3D collider files named<full-asset-id>.json),--expect-score <score>,--expect-win,--assertions <file>,--verify-replay,--saves <file>,--json.game capture <game_file>—--ticks <count>and--out <file>(both required),--seed <integer>,--inputs <file>,--scale <factor>(default1),--backend <canvas2d|webgpu|webgl2>,--assets-dir <directory>,--json. A schema version 3 (3D) document defaults towebgl2. A 2D document acceptscanvas2d(default) orwebgpu.game build <game_file>—--out <directory>(required),--assets-dir <directory>,--target <web|tauri>(defaultweb),--json.--target tauriwrites a Tauri desktop app project: the web player indist/and a native shell insrc-tauri/. It accepts--name <name>,--identifier <reverse-dns>,--app-version <MAJOR.MINOR.PATCH>,--icon <png|jpeg|webp>, and--compile, which runscargo build --releaseand reports the executable path. Runnpm install && npm run buildin the project for installers.game smoke <directory>—--json. Serves an exported build in headless Chromium with outside network access blocked, renders 300 scripted frames, and checks tick progress. Console errors, page errors, failed requests, and stalled ticks return a nonzero exit status. Install Chromium withnpx playwright install chromium.
Schema version 2 adds sprite visualAnimation tracks and scene backgrounds.
Tracks use scene or spawn age in ticks and animate rotation, scale, opacity, and
tint. Backgrounds use per-axis parallax (0 screen-fixed, 1 world-fixed),
scroll rate in world units per second, and none, repeat, or mirror tiling.
game capture derives both at the selected tick, so a resumed simulation and
an arbitrary-tick capture use the same positions.
Scene lighting adds ambient color and up to 32 colored point lights. Each
point has world position, radius, intensity, and falloff. Set sprite.unlit
for emissive art. Lighting applies to the world before effects and leaves HUD
text unchanged.
game capture uses Canvas2D by default. It reports omitted optional GPU
effects and fails if an effect is required. Choose --backend webgpu to run
bloom, brightness/contrast, and LUT grading through the GPU modules used by
the browser. The command fails when a required effect cannot run. The JSON
report includes effect diagnostics. GPU capture requires the local Dawn WebGPU
adapter.
A 3D capture checks the frame against the document’s performance.budgets,
with player defaults for any budget left out. The JSON report has
budget.limits and budget.overruns, and stats.culledEntities counts the
entities that distance culling hid. Text mode prints each overrun to stderr.
--assertions checks selected ticks. Tick 0 is the initial state; tick 1 is
after the first input frame. Each listed tick may check sceneId, entity
x/y/active, and the complete ordered events array for that tick. Omitted
fields are not checked. A missing entity fails the assertion. The default
position tolerance is 1e-9; set tolerance at the top level to change it.
Assertions beyond --ticks and duplicate tick entries are errors.
{
"tolerance": 0.000001,
"ticks": [
{
"tick": 0,
"sceneId": "room",
"entities": [{ "id": "player", "x": 0, "y": 0, "active": true }],
"events": []
},
{ "tick": 1, "events": [] }
]
}
--verify-replay saves the midpoint state, resumes it in a second session,
and compares snapshots and ordered events after each remaining tick. The JSON
report includes replay.resumeTick, replay.verified, and the first divergent
tick and path when it fails. This check excludes script execution timing.
Script save and load commands use in-memory storage during simulate.
--saves <file> seeds it from {progress?, settings?}, each mapping a slot name
to its data object. A load result reaches scripts on the next tick’s input frame,
and the replay session steps the same frame, so --verify-replay covers loads.
When a script saved or loaded, the JSON report lists each slot’s data under
saves with any storage errors.
--assets-dir supplies media files named <full-asset-id>.<extension> for
capture and export. The build copies media into a content-addressed folder and
includes a standalone browser player. Built-in sample art needs no media files.
Schema version 2 permits up to eight ordered renderEffects. brightnessContrast
sets brightness from -1 to 1 and contrast from 0 to 4.
bloom sets threshold, softness, radius in pixels, and intensity.
lut references an opaque image asset laid out as a packed 2D cube with
dimensions size² × size. hudEffectOrder chooses beforeEffects or
afterEffects; bloom defaults to HUD after effects, while a legacy one-effect
document keeps its earlier output. Each effect has required: a missing GPU
capability fails for required effects and reports omitted optional effects.
Canvas capture reports the same omission policy; GPU capture applies the chain.
HUD labels may refer to an embedded font asset by fontId. Capture loads the
font bytes before drawing; a missing required font fails and an optional font
reports its fallback. To compare browser and headless text metrics, run
NODETOOL_BROWSER_FONT_TEST=1 npm run test --workspace=packages/game-renderer -- fonts.test.ts.
Schema version 2 scenes can set music: {"assetId":"music","volume":0.5,
"fadeInTicks":30,"fadeOutTicks":30} to loop an installed audio asset. Audio
sources on entities remain one-shot effects. Their volume defaults to 1,
and triggered effects can overlap up to the browser player’s 32-voice cap.
Music position is saved as a scene start tick; loading seeks within the decoded
loop. Audio starts after a browser gesture. The CLI simulation and image capture
record logical audio events and state without producing audio samples.
Project Files
nodetool projects
List projects and move whole projects between installs as .nodetool-project
files. Runs against the local database, asset store, and workspaces. The
server serves the same format at GET /api/projects/:id/export and
POST /api/projects/import, so a file from either side imports on the other.
Subcommands:
list [--archived] [--json]— projects, newest first.export <project_id> [-o <file>] [--json]— write the project to a file. The default name is<project name>.nodetool-project.import <file> [--name <name>] [--json]— create a new project from a file and print its id.
A project file holds every row filed under the project: workflows and their versions, storyboards, scripts, timelines, sketches, mini apps, JavaScript scripts, and games, each with its version history. It also holds chats with their messages, assets with their stored bytes, and every file in the project’s workspaces. Jobs, run traces, and spend records stay behind.
Import never overwrites. It creates a new project and gives every row a new
id, then rewrites each reference to an exported id: id columns, document JSON,
asset:// locators, workspace file paths, and JSON files in workspaces.
Imported workflows are private. Workspaces become managed folders, because the
exporting machine’s paths mean nothing on another one. The Personal project
imports as an ordinary named project.
Examples:
nodetool projects list
nodetool projects export 6f1c2e... -o launch-spot.nodetool-project
nodetool projects import launch-spot.nodetool-project --name "Launch spot (copy)"
Job Management
nodetool jobs
Query job status and results. Reads the local database by default.
Subcommands: list, get
Options:
--api-url <url>— query a remote server instead of the local database.--workflow-id <id>— filter by workflow ID (forlist).--limit <n>— max results (default:100).--json— output as JSON.
--json returns the same declared row shape whether the command read the local
database or a server. The local read used to dump every column, including a
job’s whole graph and logs.
Examples:
# List all jobs
nodetool jobs list
# Filter by workflow
nodetool jobs list --workflow-id workflow_abc123
# Get a specific job
nodetool jobs get <job_id>
nodetool runs
Inspect durable app runs, workflow jobs and chat turns. Reads the local
database as the local account by default. --api-url <url> or
NODETOOL_API_URL selects a server. Every query stays within the caller’s account.
Local reads apply pending SQLite migrations before opening the database.
Run IDs accept their full value or an exact 12-character resource prefix.
Ambiguous prefixes fail. Trace and span IDs always retain their full OTel
values. show, trace and logs also accept a workflow job or chat message
ID when it identifies a recorded run.
| Command | Output and filters |
|---|---|
runs list |
Newest runs. --kind app\|workflow\|chat, --app-id, --instance-id, --workflow-id, --thread-id, --status running\|completed\|failed\|cancelled, --origin ui\|agent\|cli\|debug\|public, --since <ISO time>, --until <ISO time>, --limit (default 20, maximum 100), --cursor |
runs show <id> |
Record and bounded summary: failed span and root path, provider costs, slow spans, span counts, generations, documents and content flags. --include-content adds capped app run inputs and outputs |
runs trace <id> |
Span tree. The table has a detail column: <node id> (<node type>) on node spans, <provider>/<model> on LLM spans. --depth (default 4, maximum 64), --focus <full span ID>, --name, --errors-only, --limit (default 100, maximum 500) |
runs logs <id> |
Events, oldest first. With --include-content the table adds message and arguments columns. Without it, the table ends with a hint that messages are excluded. --level, --source, --span <full span ID>, --since-ms, --until-ms, --limit (default 100, maximum 500), --cursor |
runs tail <id> |
Follow until terminal. --cursor (default 0), --limit (default 100, maximum 500), --poll-interval <ms> (default 250, range 10–5000), --timeout <seconds> |
Each command accepts --json. Single reads return the shared runs service’s
JSON result. Lists and logs return an opaque next_cursor for the next page.
Content is excluded by default. show --include-content adds the app record’s
capped inputs and outputs. trace --include-content requires --focus.
logs --include-content and tail --include-content return capped owner
content. Results report expired, absent, visitor or suppressed content and
trace truncation or incompleteness.
tail --json emits NDJSON, one object per line. Its frame types are
resnapshot, span, event, reconnecting, terminal, interrupted,
gap and error. The store retains each span’s latest complete snapshot, so a
resnapshot frame warns that intermediate updates may have been coalesced.
Replace cached spans by their full snapshot. Event frames are deduplicated by
span ID and stable event ID during the process. After a connection loss, tail
retries from its durable cursor. Save the final cursor to resume in another
process. A terminal run with all spans ended drains its remaining snapshots and
returns immediately. If final spans are still arriving, tail waits until they
end or until one second passes without a new cursor, with a five-second maximum
drain. An unfinished trace produces a gap frame and a terminal frame with
trace_settled: false. Reader limits appear on resnapshot frames. Deduplication
memory is bounded by the store’s span and event caps. A dedup_limit gap warns
if later records exceed those bounds.
Ctrl-C stops following, removes the signal handler and exits 130. It does not
cancel the run. A timeout or read error exits 1. Following a failed run to its
terminal record exits 0.
nodetool runs list --kind app --status failed --json
nodetool runs show <run_id> --json
nodetool runs trace <run_id> --focus <span_id> --include-content --json
nodetool runs logs <run_id> --level error --include-content --json
nodetool runs tail <run_id> --cursor 42 --json
Asset Management
nodetool assets
Manage uploaded files and workflow assets. Reads the local database by default.
Subcommands: list, get
Options:
--api-url <url>— query a remote server instead of the local database.--query <q>— search query (forlist).--content-type <type>— filter by content type (forlist).--limit <n>— max results (default:100).--json— output as JSON.
Examples:
# List assets
nodetool assets list
# Search assets
nodetool assets list --query "landscape"
# Get a specific asset
nodetool assets get <asset_id>
Storage Maintenance
nodetool storage migrate-keys
Move asset objects under their owner’s prefix, so an object’s key is
<userId>/<assetId>.<ext> and the owner is the leading path segment — the
boundary a Supabase RLS policy or an S3 bucket policy can enforce on the object
itself.
Run it once when upgrading an S3 or Supabase install that still holds objects written under the older flat layout. The local file backend needs no migration: it falls back to the flat key on a miss.
Options:
--dry-run— report what would move without writing anything.--user-id <id>— migrate only this user’s objects.--json— output the report as JSON.
Examples:
# See what would move first
nodetool storage migrate-keys --dry-run
# Move them
nodetool storage migrate-keys
# One user, machine-readable
nodetool storage migrate-keys --user-id 1 --json
A dry run reports the tally and writes nothing:
scanned 0, would move 0, already migrated 0, absent 0, failed 0
Dry run — nothing was written. Re-run without --dry-run.
Vector Collections
nodetool collections
Manage the RAG vector-store collections behind semantic search. Runs in-process
against the default vector provider — sqlite-vec unless
NODETOOL_VECTOR_PROVIDER points elsewhere — so no server has to be running.
Subcommands: list, get, create, delete, query, index
Options:
--embedding-model <model>/--embedding-provider <provider>— record the embedding model and provider on a new collection (forcreate).-n, --n-results <n>— number of matches to return (forquery, default10).-y, --yes— skip the confirmation prompt (fordelete).--json— output as JSON. Available on every subcommand.
Examples:
# Create a collection and record what embeds it
nodetool collections create my_docs --embedding-model text-embedding-3-small
# Chunk and index files, with the same splitter the server uses
nodetool collections index my_docs notes.md report.txt
# Semantic search
nodetool collections query my_docs "how does the runner cancel a job" -n 5
# Inspect and remove
nodetool collections list
nodetool collections get my_docs
nodetool collections delete my_docs --yes
Re-indexing the same file replaces its chunks rather than adding a second copy,
so an index run is safe to repeat after a document changes.
Spend Tracking
nodetool costs
Report what NodeTool has spent on LLM and provider calls. Every call writes a row carrying its cost and token counts; these subcommands aggregate them straight from the local database, so no server has to be running.
Reach for it to answer “what did that run cost” after the fact, or to find which model is eating a budget.
Subcommands: summary, list, by-provider, by-model
Options:
--provider <name>— filter by provider (forlistandby-model).--model <id>— filter by model (forlist).--limit <n>— max results (forlist, default50).--json— output as JSON. Available on every subcommand.
Examples:
# Overall spend plus per-provider and per-model breakdowns
nodetool costs summary
# Recent calls, most recent first
nodetool costs list --limit 20
nodetool costs list --provider anthropic
# Grouped totals
nodetool costs by-provider
nodetool costs by-model --provider openai
summary prints the overall total and both breakdowns:
Overall
key │ value
──────────────┼─────────
total_cost │ $0.0000
total_tokens │ 0
calls │ 0
By provider
(no results)
By model
(no results)
Supervised runs and nodetool app build write to the same ledger, tagged
supervisor and app-build in node_type, so costs list shows what the
harnesses spent alongside the workflow’s own calls.
nodetool generations
Inspect the record of every media generation — image, video, audio, 3D. One
row per provider call, opened before the call as running and closed with its
outcome (completed, failed, cancelled, or interrupted when a restart
orphaned it), its cost, the provider’s request id, and the asset ids it
produced. Like costs, it reads the local database, so no server has to be
running.
Where costs aggregates LLM spend, this answers the per-call questions: what a
render cost, which asset it wrote, and whether it is still going.
Subcommands: list, get, await, cancel, reconcile, sweep, provider-list, provider-get
Options:
--status <status>—pending,running,recovering,completed,failed,cancelled,needs_attention, orinterrupted(forlist).--provider <name>/--capability <name>— filter by provider, or by capability such astext_to_video(forlist).--thread-id <id>/--job-id <id>— only what a chat thread or a workflow run asked for (forlist).--since <iso>— only generations created at or after this time (forlist).--limit <n>— max results (forlist, default50).--timeout <seconds>— how longawaitwaits before giving up (default300).--provider <name>is required, with--model <id>, forprovider-listandprovider-get(see below).--json— output as JSON. Available on every subcommand.
Examples:
# What ran, newest first
nodetool generations list
nodetool generations list --status running
nodetool generations list --provider fal --capability text_to_video
nodetool generations list --since 2026-09-01T00:00:00Z --limit 20
# One row in full — cost, assets, origin, reconcile state
nodetool generations get <generation_id> --json
# Block until it settles, then act on the exit code
nodetool generations await <generation_id> --timeout 600
# Close a running row, or ask the provider what it actually billed
nodetool generations cancel <generation_id>
nodetool generations reconcile <generation_id>
# Close orphaned rows and drain the reconcile queue once
nodetool generations sweep
With nothing recorded, list prints No generations recorded.
Exit codes are the verdict, so these compose in a script:
await—0once the row reaches a terminal status,1if it is still running when the timeout expires.cancel—0if it was running and is now cancelled,1if it had already settled or does not exist. The provider call itself finishes on its own in whatever server process started it; only the record is closed.reconcile—0when the provider answered and the row was updated,1otherwise, with areasonnaming which:no request id, orno reconciler for <provider>when that provider has no billing API, which also marks the rowunavailableso it leaves the queue.
get prints the whole record: status, provider, model, capability,
cost with its currency, billing_unit, quantity and unit_price,
asset_ids, started_at/completed_at/duration_seconds, an origin block
(surface, thread_id, tool_call_id, job_id, node_id, workflow_id),
the provider_request_id, and a reconcile block (reconciled_at,
attempts, error, next_at).
A model in no price catalog still gets a row, with cost null — list shows
it as unpriced rather than as free.
sweep is the startup sweep by hand: it closes rows still marked running as
interrupted, then drains the reconcile queue once. The server runs it at
every start, so reach for it when working against the database directly. Rows
awaiting reconciliation retry with backoff at 1, 5, 30, 120, and 720 minutes.
provider-list and provider-get <request_id> ask the provider instead of the
local database: its own record of what this account ran, from any machine, at
the price it billed. Both require --provider <name> (for example fal_ai) and
accept --model <id>, the endpoint id (comma-separated for provider-list).
provider-list also takes --status <running|completed|failed|cancelled|unknown>,
--since <iso>, --until <iso> (exclusive), --limit <n> (default 50) and
--cursor <cursor>, the next_cursor that a previous page printed. A provider
without a generations API reports that instead of an empty list.
nodetool generations provider-list --provider fal_ai --status running
nodetool generations provider-get <request_id> --provider fal_ai --model fal-ai/flux/dev
Agents reach the same record through list_generations, get_generation,
await_generation, cancel_generation, and reconcile_generation. Design:
Media generation tracking.
Secrets Management
nodetool secrets
Manage encrypted secrets stored in the local database with per-user encryption.
Subcommands: list, store, get
Examples:
# List stored secret keys
nodetool secrets list
# Store a secret (prompts for value)
nodetool secrets store OPENAI_API_KEY
# Retrieve a secret value
nodetool secrets get OPENAI_API_KEY
Provider Sign-In
nodetool auth
Sign in to providers that use an account instead of an API key. Today that is
Claude: auth claude runs the same OAuth flow the claude CLI does and writes
the tokens to the Claude Agent SDK’s credential file
(~/.claude/.credentials.json), so a NodeTool login and a claude login are
interchangeable. The Claude Agent provider picks the file up with no further
configuration. No database and no server are involved.
Reach for it when you want to run agents on a Claude subscription rather than
store an ANTHROPIC_API_KEY.
Subcommands: claude login, claude status, claude refresh, claude logout
Options:
--console— sign in with a Console (API-billed) account instead of a subscription (forlogin).--manual— skip the loopback listener and paste the code the browser shows. This is the flow for a headless or remote machine (forlogin).--no-browser— print the URL instead of opening a browser (forlogin).--force— refresh even when the current token is still valid (forrefresh).--json— output as JSON. Available onlogin,status, andrefresh.
Examples:
# Browser opens, loopback callback completes the flow
nodetool auth claude login
# Headless box: paste the code yourself
nodetool auth claude login --manual
# Check and refresh
nodetool auth claude status
nodetool auth claude refresh --force
nodetool auth claude logout
The same flow is available over HTTP at
/api/oauth/claude/{start,complete,tokens,disconnect} and as a sign-in card on
the Models & Providers settings page.
Settings
nodetool settings show
Display the current values of a fixed list of environment variables: ENV, LOG_LEVEL, PORT, HOST, DB_PATH,
NODETOOL_API_URL, the provider keys (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, MISTRAL_API_KEY,
GROQ_API_KEY, SERPAPI_API_KEY), OLLAMA_API_URL, HF_TOKEN, VECTORSTORE_DB_PATH, ASSET_BUCKET, and
S3_ENDPOINT_URL. A variable whose name ends in KEY or TOKEN prints as *** when set. Unset variables print empty.
Options:
--json— output as JSON.
Example:
nodetool settings show
nodetool settings show --json
nodetool telemetry
Control anonymous usage analytics for the CLI. Telemetry is off until you run
enable, and the CLI never asks. NODETOOL_TELEMETRY=0 and DO_NOT_TRACK=1
turn it off whatever was stored. The decision lives in
~/.nodetool/telemetry.json and applies to the CLI only. What is sent and what
is never sent is in Product Analytics.
enable— start sending anonymous usage events.disable— stop, and delete the anonymous ID and unsent events.status [--json]— show whether telemetry is on, why, and where it is stored.inspect [--json]— print the events waiting to be sent, exactly as they would be sent.
nodetool telemetry status
nodetool telemetry enable
nodetool telemetry inspect --json
Model Management
nodetool models
List models and providers. Queries local providers and caches by default. providers, list and by-provider mlx start the local Python worker for MLX when the Python environment exists, and stop it when the command ends. Without that environment the other providers still list. --api-url <url> (default NODETOOL_API_URL) targets a remote server on list, ollama, and huggingface. Every subcommand takes --json.
Subcommands:
list— list all models (recommended + provider + HuggingFace cached).providers— list configured providers and their capabilities.recommended— list recommended models.ollama— list Ollama models.huggingface— list HuggingFace cached models (--query,--typeto filter).by-provider <provider>— list models for a provider;--kindone ofllm,image,tts,asr,video,embedding(defaultllm).
Examples:
nodetool models list
nodetool models providers
nodetool models ollama
nodetool models by-provider openai --kind image
nodetool models recommended
The curated shortlist — what to reach for when you want a model id that is known
to work, rather than everything a provider will sell you. It reads the shipped
RECOMMENDED_MODELS table in-process, so it needs no server, no API key, and no
network.
--category <category>— one ofall(default),image,image-text-to-image,image-image-to-image,language,language-text-generation,language-embedding,asr,tts,video-text-to-video,video-image-to-video. An unknown value exits1listing the valid ones.--system <darwin|linux|windows>— keep only models that run on that platform. A model that declares no platforms is kept for all of them.--limit <n>— cap the results. Applied after both filters; a value that is not a positive integer exits1.--check-servers— fetch the list from a running server instead, which also probes whether the local Ollama and llama.cpp servers can serve each entry. This is the only mode that uses--api-url(defaulthttp://localhost:7777, orNODETOOL_API_URL).--json— the full model records rather than the five-column table.
# The top of the curated list, no network
nodetool models recommended --limit 3
# What to embed with
nodetool models recommended --category language-embedding
# What a Mac can actually run, machine-readable
nodetool models recommended --system darwin --json
# Which of them a local Ollama has pulled
nodetool models recommended --check-servers
nodetool models — HuggingFace Hub and cache
Five subcommands search the Hub and manage the local HuggingFace cache. They talk to the Hub’s HTTP API and the cache on disk, so they need no running NodeTool server. Reach for them when you want to find a repo id for a local model node, or pull the weights before a workflow needs them.
hf-types— print the nodetool HF model types, one per line, followed by aGeneric types (require --task):block. The types in that block need a pipeline tag when you search them; the ones above it do not.--jsonprints{types, generic}instead.list-hf <model_type>— search the Hub for models matching a nodetool model type.--task <task>supplies the HF pipeline tag (required for the generic types);--limit <n>caps the results;--jsonprints the raw entries. Without--task, a generic type fails withModel type 'hf.model' requires --task (e.g. 'text-to-image').list-hf-all— search across every nodetool HF model type at once.--limit <n>caps the total;--repo-onlydrops file-level entries and keeps one row per repo.hf-cache— list the local cache with disk detail: repo id, path, type, whether the repo is fully downloaded, size on disk, pipeline tag.--downloaded-onlykeeps only complete repos;--limit <n>and--jsonbehave as elsewhere.huggingface(above) reads the same cache but prints id/name/provider/type/repo_id and can search it (--query,--type) or target a remote server (--api-url); usehf-cachewhen you care about what is on disk and how big it is.download-hf --repo-id <owner/name>— download a repo into the local HuggingFace cache, printing progress to stderr.--file-path <path>fetches a single file instead of the whole repo;-a, --allow-patterns <glob>and-i, --ignore-patterns <glob>are repeatable globs that narrow the file set. Passing--cache-dir <dir>switches the download to the flat llama.cpp cache layout (~/.cache/llama.cppon Linux,~/Library/Caches/llama.cppon macOS) — it does not redirect the files into<dir>.
Examples:
# Which model types can be searched, and which need --task
nodetool models hf-types
# Search one type, then every type at once
nodetool models list-hf qwen3 --limit 5
nodetool models list-hf hf.text_to_image --task text-to-image --limit 5
nodetool models list-hf-all --repo-only --limit 20
# What is already on disk
nodetool models hf-cache --downloaded-only
# Pull weights ahead of a run — whole repo, then just the GGUF files
nodetool models download-hf --repo-id Qwen/Qwen3-0.6B
nodetool models download-hf --repo-id Qwen/Qwen3-0.6B-GGUF -a "*.gguf"
Media Generation
nodetool generate <provider> <model> <prompt...>
Local and self-hosted installs expose fake-image and fake-video on provider
fake for no-cost tests without credentials. They produce a fixed PNG or a
two-second MP4. Select fake explicitly with find_model({provider_hint: "fake", ...}).
Automatic model selection excludes fake media. generate accepts image models.
For a video fixture, use the video tool:
nodetool tools info generate_video
nodetool tools call generate_video --args '{"provider":"fake","model":"fake-video","prompt":"test fixture"}'
nodetool skills read storyboard-core
Storyboards can use these refs as image_model and video_model board defaults.
Generate an image from any registered provider straight to a file, with no workflow in between. Use it to try a model, check that a provider’s key resolves, or produce one image from a script.
Provider and model names are matched leniently against the registry and the
provider’s own model manifest, so fal-ai finds fal_ai and flux-schnell
finds fal-ai/flux/schnell. The API key comes from the secret store or the
environment; when it is missing the error names the variable to set.
Options:
-o, --output <path>— output file or directory. A directory (or a path ending in a separator) keeps the generated name; a file path is used as the base name. Default is anodetool-<model>-<timestamp>.<ext>file in the working directory.--width <n>/--height <n>/--aspect-ratio <ratio>— output size, e.g.--aspect-ratio 16:9.--negative-prompt <text>— what to avoid in the image.--seed <n>— random seed, for reproducible output.-n, --num-images <n>— number of images (default1). Extra images get a-2,-3, … suffix.--steps <n>/--guidance <n>— inference steps and guidance scale.--image <path...>— input image(s); switches to image-to-image.--strength <n>— image-to-image strength, with--image.--list-models— list the provider’s image models and exit.--json— print the result as JSON.
Examples:
# Discover model ids for a provider
nodetool generate fake --list-models
nodetool generate fake fake-image "test fixture" -o fixture.png
nodetool generate fal-ai --list-models
# Text to image
nodetool generate fal-ai flux-schnell "a red fox in snow" -o fox.png
# Four square variants into a directory
nodetool generate fal-ai flux-schnell "a logo" --aspect-ratio 1:1 -n 4 -o ./out/
# Image to image
nodetool generate fal-ai flux-dev "restyle this" --image in.png --strength 0.6
MCP Integration
nodetool mcp
Register NodeTool as an MCP server in Claude Code, Codex, and OpenCode, check the result, or run the server. See NodeTool as an MCP Server for the setup guide.
Subcommands: install, uninstall, status, config, serve
Examples:
# Register the stdio server with every client found on this machine.
# Without a global install:
npx -y --package=@nodetool-ai/cli nodetool mcp install
# Register for Claude Code only
nodetool mcp install --claude
# Connect to a running server over HTTP instead of stdio
nodetool mcp install --http
nodetool mcp install --url http://127.0.0.1:8000/mcp
# Show the registered entries, then start each one and list its tools
nodetool mcp status
nodetool mcp status --check
# Print an mcpServers JSON block for Cursor, Claude Desktop, or another client
nodetool mcp config
# Remove from all clients
nodetool mcp uninstall
| Option | Applies to | Effect |
|---|---|---|
--claude, --codex, --opencode |
install, uninstall |
Select clients. install without one selects every client found. |
--npx |
install, config |
Launch through npx even when nodetool is on PATH. |
--http, --url <url> |
install, config |
Write an HTTP entry. The default URL is http://127.0.0.1:7777/mcp. |
--no-verify |
install |
Write the config without a test start of the server. |
--check |
status |
Start each registered server and list its tools. |
mcp status --check exits 1 when a registered server fails to start, and
mcp uninstall exits 1 when a client config cannot be written.
install starts the server once before it writes a config, so a broken
command never reaches a client config. The stdio entry runs nodetool mcp serve
when the Node CLI is first on PATH, else
npx -y --package=@nodetool-ai/cli nodetool mcp serve.
HTTP MCP (/mcp) needs the API server (nodetool serve). nodetool mcp serve
uses stdio for the client and forwards to the local API server when its MCP
endpoint is available on port 7777 (or PORT when set). Actions then run in the API process, so
connected UI clients receive live updates. When the endpoint is unavailable,
the command runs its own local MCP server.
What the MCP server exposes
The MCP mount offers these direct tools alongside the promoted discovery and browser tools:
execute_code— the CodeAct action tool, built by the samecreateChatCodeActSessionthe in-app chat agent runs on, so the two surfaces cannot drift. Everything NodeTool can do is reached from inside an action: workflow building and debugging (create_workflow,validate_workflow,debug_workflow,debug_app, theui_*graph editing tools), media generation (generate_image,generate_video,generate_speech,transcribe_audio, …), files (read_file,write_file,edit_file,glob,grep), web (web_search,browser,http_request), collections, documents, code execution, image critique, and memory — asnodetool.<namespace>.<method>(), by importing them, or found withawait nodetool.searchTools("query"). Google Workspace tools appear only on deployments with a Google login.view_image— direct, because image content cannot ride a sandbox action’s JSON observation envelope.upload_asset— save base64 bytes in the user’s asset library, or read an absolute path on the local NodeTool server. It returns the asset ID and URI.download_asset— return an owned asset’s bytes as base64, or write them to an absolute path on the local NodeTool server. Remote MCP sessions use base64.
Inside execute_code, import_asset accepts an HTTP(S) URL or an absolute
server-local path on a local MCP connection. It returns an asset:// URI,
byte size, and media duration when the source can be probed. Find its import
line with nodetool.searchTools("import asset").
render_demo_surface renders a storyboard, script, sketch, timeline, graph, or 3D
surface from a source checkout with Remotion installed. It returns a silent MP4
asset, with the baked corner label hidden by default. The tool also accepts a
document cast ID. With both from_ms and to_ms, the exported clip plays that
source range at normal speed and has its duration. Without a range, it renders
the designed six-second loop. The 3D surface accepts ranges within its six-second
camera orbit.
MCP has no system prompt. The guest contract (QuickJS, not Node; nodetool.*;
no finish()) is the server instructions string and the first lines of the
execute_code description. The rest of the description is the CodeAct catalog.
Two resources carry the machine-readable form: nodetool://capabilities (tools
and modules) and nodetool://sandbox (blocked globals, unavailable bridges,
worked examples). Prompts sandbox-action and sandbox-asset are complete
execute_code bodies. Each skill the user can load is also a prompt with the
skill’s name, and it returns the skill body.
The session needs a user to run as — its tools touch that user’s secrets, assets, and files — so
createMcpServer refuses one that is not bound to a user (nodetool mcp serve, the local /mcp
mount, or an authenticated session). File tools read and write under a per-user workspace at
<data-dir>/mcp-workspaces/<user-id>, not the host filesystem.
Agents
nodetool agent
Run one CodeAct turn from the command line, over the default toolbelt. The
objective becomes the user message and the model acts by writing sandboxed
JavaScript — the same loop the chat runs. create_plan and execute_plan are
on the belt, so an objective that wants decomposing gets a task DAG and the
planning_update / task_update events stream with the rest of the trace.
Subcommands: run, diagnose
# Run an agent with an objective
nodetool agent run -p anthropic -m claude-sonnet-5 --objective "Research AI trends"
# Or pipe the objective
echo "Research AI trends" | nodetool agent run -p anthropic -m claude-sonnet-5
# Bound the tool-calling rounds
nodetool agent run -p openai -m gpt-5.4-mini -o "Summarize the README" \
--max-iterations 8
# Ask before every write, execute or external call
nodetool agent run -p anthropic -m claude-sonnet-5 -o "Tidy the workflows" \
--permission-mode default
# Bound what the run may spend and how long it may take
nodetool agent run -p openai -m gpt-5.4-mini -o "Research AI trends" \
--cost-cap 0.50 --timeout 120
# Aggregate a failed run into one report
nodetool agent diagnose <job_id>
--max-steps is gone with the planner→compiler pipeline the command used to
run, along with plan approval, checkpoints, the planning/reasoning model split,
and skill auto-select. See Agent CLI.
Run budget
--cost-cap <usd> and --timeout <s> bound one run. They override two of the
five NODETOOL_AGENT_* settings a chat turn reads
(NODETOOL_AGENT_TURN_COST_CAP_USD, NODETOOL_AGENT_TURN_DEADLINE_MS); the
other three — NODETOOL_AGENT_MAX_CONCURRENCY, NODETOOL_AGENT_MAX_TURNS,
NODETOOL_AGENT_UNPRICED_TOKEN_CEILING — come from the settings alone.
--cost-cap 0 lifts the dollar cap, which is what a local-only install wants;
--timeout 0 leaves the run no time and stops it before its first model turn.
One budget covers the whole run, not each loop: a sub-agent, an execute_plan
DAG, and an AgentNode reached through run_node all reserve against it. A
cap is admission, so a turn whose worst case would cross it is refused before
the call rather than noticed after the money is spent — a model the price
catalog does not cover has no worst case and is admitted against a prompt-token
ceiling instead, never as free.
A run a ceiling stops prints the reason and exits non-zero:
agent stopped: turn budget of $0.01 reached
With --json the same reason arrives as an error event. Every run ends with
what it committed against the cap, as spent $0.0123 on stderr or a
log_update in --json.
Permission mode
Every tool call the model makes goes through one ladder, on every host. What a mode decides:
| Mode | Read | Write, execute, external |
|---|---|---|
default |
runs | asks first |
auto |
runs | runs |
plan |
runs | blocked, with a message telling the model to switch out |
--permission-mode sets it on nodetool agent run and on nodetool-chat; an
unrecognized value is refused rather than falling back to a default.
In a terminal, default is what an unset flag means. Each write, execute or
external call waits for y (this call), n (refuse), or a (this tool for the
rest of the session). Chat displays the request in its fullscreen interface.
nodetool agent run prints it on stderr, leaving stdout for the run’s result.
Behind a pipe, nobody is there to answer, so the run takes the headless gate:
auto, printing once up front that escalated calls are denied. An explicit mode
still applies — a piped --permission-mode plan blocks what plan mode blocks —
but the answer to anything the ladder escalates is deny, never a silent allow.
See the Agent CLI reference for full details.
The
nodetool dbgroup (migrate,status,baseline,rollback) is documented under Database Migrations above.
Deployment and Workers
Two different things, and the CLI keeps them apart. A server is long-lived
and people connect in to it — nodetool deploy manages one over Docker. A
worker is a rented GPU that a NodeTool instance connects out to, bills by
the minute, and is meant to be torn down — nodetool worker provisions it. See
Deployment for which one you want.
nodetool deploy
Manage a Docker self-host server target described by a deployment.yaml.
init scaffolds the file, add fills in one target, and every other
subcommand names that target.
The file lives in the user config directory, not the working directory:
~/Library/Application Support/nodetool/deployment.yaml on macOS,
%APPDATA%\nodetool\deployment.yaml on Windows, and
$XDG_CONFIG_HOME/nodetool/deployment.yaml — falling back to
~/.config/nodetool/ — elsewhere. Run nodetool deploy edit to open it rather
than guessing the path.
Subcommands: init, add, edit, list, show, plan, apply,
status, logs, destroy, plus the remote group workflows and the
users-* verbs below.
Options:
--type <type>— required onadd.dockeris the only supported value.--dry-run— print whatapplywould do without executing it.-f, --follow/--tail <n>/--service <service>— forlogs;--taildefaults to100.--force— skip the confirmation ondestroy,workflows delete, andusers-remove.--json— machine-readable output onlist,workflows list, andusers-list.
# Scaffold, describe, and review before touching the remote host
nodetool deploy init
nodetool deploy add my-server --type docker
nodetool deploy plan my-server
# Deploy and watch it
nodetool deploy apply my-server
nodetool deploy logs my-server --follow --tail 200
# Open deployment.yaml in $EDITOR
nodetool deploy edit
list and show answer the two questions you have before naming a target in
any of the other verbs, and they are the pair to reach for when you have
inherited a deployment.yaml you did not write.
list queries each configured target for its live state and prints one row per
deployment — name, type, status, last_deployed, host, container,
pod_id, project, region, service. With none configured it prints
(no deployments configured); --json gives the same rows for a script.
show <name> reads the file only, printing that one target’s config as YAML —
no remote call, so it works while the host is down.
nodetool deploy list
nodetool deploy list --json
nodetool deploy show my-server
Both need the file to exist. Without it they exit non-zero with
Deployment configuration not found at <path>. Run 'nodetool deploy init' to
create it.
The full server walkthrough is Deployment and Self-Hosted Deployment.
Remote workflows
Once a target is up, workflows acts on the deployment’s /api/workflows
routes rather than on the local database:
# Push a local workflow to the deployment, then run it there
nodetool deploy workflows sync my-server <workflow_id>
nodetool deploy workflows list my-server
nodetool deploy workflows run my-server <workflow_id> -p prompt="a red fox"
nodetool deploy workflows delete my-server <workflow_id>
workflows run takes -p, --param <k=v>, repeatable. sync pushes the
workflow row itself; the assets and models its nodes reference must already be
present on the target.
API users on the deployment
For a multi-user server, mint and rotate the tokens its API clients
authenticate with. --role is user unless you pass admin.
nodetool deploy users-add my-server alice --role admin
nodetool deploy users-list my-server
nodetool deploy users-reset-token my-server alice
nodetool deploy users-remove my-server alice
Every subcommand that touches a live deployment — the users-* verbs and the
workflows group above — sends an admin bearer token. --token <token>
passes it explicitly and wins over NODETOOL_ADMIN_TOKEN; with neither, an
interactive shell prompts and a non-interactive one exits 1.
nodetool worker
Provision, attach to, and tear down a rented GPU worker on RunPod or Vast. A
worker bills by the minute, so stop is part of the workflow, not cleanup you
get to postpone.
Subcommands: profile (add, list, rm), create, list, status,
token, stop, models
Options:
--target <runpod|vast>,--image <image>,--gpu <gpu>,--vcpu <n>— what to rent, onprofile addor inline oncreate.--idle-timeout <minutes>/--max-lifetime <minutes>— auto-stop when idle, and a hard TTL. Both are the guard against a forgotten worker.--ssh-key <path>— OpenSSH public key file. It exposes22/tcpso the worker can be debugged.--disk <gb>— persistent volume size, which holds the model cache (default100).--token-policy <generate|fixed>— defaultgenerate.--profile <name>/--attach— forcreate.--all— forstop, stops every non-stopped worker.--json— forlistandprofile list.
# Save a preset once, then rent from it
nodetool worker profile add hf-a40 --target runpod \
--image ghcr.io/nodetool-ai/nodetool-worker:latest \
--gpu "NVIDIA A40" --idle-timeout 15
nodetool worker create --profile hf-a40 --attach
# Inspect and tear down
nodetool worker list
nodetool worker status <instance-id>
nodetool worker stop --all
token <id> prints one worker’s bearer token and nothing else — no table, no
label — so it pipes into the variable the bridge reads:
export NODETOOL_WORKER_TOKEN=$(nodetool worker token <instance-id>)
list withholds tokens on purpose; this is the one command that decrypts one,
and only for the worker you name. Reach for it when a second shell, a CI step,
or a --api-url client has to talk to a worker --attach did not configure. A
worker that carries no token is an error rather than empty output:
Worker instance '<id>' has no token (open worker).
The full walkthrough, including what --attach changes locally, is
Worker Deployment.
nodetool worker models
Manage the HuggingFace cache on the worker itself, over the WebSocket bridge — no NodeTool server has to be running. Pre-pulling weights here is what keeps the first run of a graph from paying the download on rented GPU time.
The [worker-id] argument is optional: omit it and the command uses the
currently attached worker, or errors telling you to attach one.
Options:
--repo-id <id>— HuggingFace repo,owner/name. Required fordownloadanddelete.--file-path <path>— download a single file instead of the repo.-a, --allow-patterns <pattern>/-i, --ignore-patterns <pattern>— glob filters ondownload, both repeatable.--json— forlist.
# What is already cached on the attached worker
nodetool worker models list
# Pull weights ahead of a run — whole repo, then just the safetensors
nodetool worker models download --repo-id stabilityai/sdxl-turbo
nodetool worker models download --repo-id stabilityai/sdxl-turbo -a "*.safetensors"
# Reclaim disk on a named worker
nodetool worker models delete <worker-id> --repo-id stabilityai/sdxl-turbo
Messaging Bridges
nodetool telegram
Run the Telegram bridge: it turns private-chat messages into turns on the agent
loop of a running NodeTool server, and streams the answers back. The bridge
holds no conversation state and no user credentials — the agent loop, tools,
permissions, threads, and cost tracking all stay on the server, which the
bridge reaches over /ws as the linked user.
Reach for it when you want NodeTool answering from a phone without opening the
web UI. The server it talks to must have NODETOOL_INTEGRATION_TOKEN set, or
the linking routes the bridge calls do not exist.
Subcommands: serve, register-commands
Options:
--config <path>— path totelegram-bot.json, relative to the working directory. Both subcommands take it. Without the flag,telegram-bot.jsonis read if it is there and skipped if it is not; a path you asked for explicitly and that does not exist is an error.
Configuration is environment variables plus that optional file. The env vars are listed in the Environment Variables Index; the file carries only the tuning knobs:
// telegram-bot.json
{
"allowUsers": [], // Telegram user ids allowed to link; empty = anyone
"editThrottleMs": 1500, // minimum gap between edits of one streamed reply
"maxQueuedTurns": 3 // messages queued behind an in-flight turn
}
# Publish the bot's command list — a deploy step, not a boot step
export TELEGRAM_BOT_TOKEN=123456:AA...
export NODETOOL_INTEGRATION_TOKEN=the-same-value-the-server-has
nodetool telegram register-commands
# Long-poll the Bot API and serve turns
nodetool telegram serve --config ./telegram-bot.json
Both subcommands validate the whole configuration before doing anything and print the fields that failed rather than a stack trace:
Invalid Telegram bridge environment:
TELEGRAM_BOT_TOKEN: TELEGRAM_BOT_TOKEN is required
NODETOOL_INTEGRATION_TOKEN: NODETOOL_INTEGRATION_TOKEN is required
serve polls with getUpdates. Webhook mode is not implemented yet, and
setting TELEGRAM_WEBHOOK_URL makes serve refuse to start rather than
silently polling. Design notes: telegram-bot-design.md.
Browser Extension
nodetool extension
Set up and check the bridge between NodeTool and the Chrome extension. The bridge needs no server: Chrome starts the native messaging host when the extension connects, and local clients reach it over a unix socket.
Subcommands: install, status
Neither subcommand takes options.
installregisters the native messaging host with Chrome, Chromium, Brave, Edge and Arc. It prints the wrapper path, each manifest it registered, and the extension ID.statusprintsconnectedornot connected, followed by onesocket: <path>line for each bridge socket it finds. It exits with code 1 when the extension is not connected.
# Register the native host, then load or reload the extension in chrome://extensions
nodetool extension install
# Check the connection
nodetool extension status
Development Tooling
nodetool affected [files...]
Map changed files — or the git working tree — to the smallest set of workspaces
that must be rebuilt and tested: the workspace that owns each file plus its
downstream dependents, and a build:packages only when a package that loads
from dist/ is in the set. It saves reaching for the full build after a
one-file change.
Options:
--base <ref>— compare against a git ref instead of the working tree.--json— print the result as JSON.
Examples:
nodetool affected
nodetool affected --base main
nodetool affected packages/cli/src/nodetool.ts
nodetool affected --json
The output names each affected workspace and the commands to run:
1 affected workspace(s) from 1 changed file(s):
changed @nodetool-ai/cli
Suggested commands:
npm run test --workspace=packages/cli
nodetool harness
The machine-readable inventory behind harness-first engineering: every headless harness in the repo, every product surface with the code paths it owns, and which harnesses cover which surface. Shipping a new surface means adding it to the registry with its harness — or with its debt written down.
Subcommands: list, audit, capabilities, gate
nodetool harness list
List every harness and its capabilities. Each command is marked as available
in the installed CLI or requiring a NodeTool checkout. --json adds a
requiresCheckout flag to each registry entry. Repository checks need the
checkout’s scripts and workspace dependencies.
nodetool harness list
nodetool harness list --json
Each entry names the harness, its kind, what it covers, and how to invoke it:
affected meta Changed-file → workspace mapping
nodetool affected [--base main] [json, no-db]
packs-compile static Sandbox npm module compiler (bundle, scan, probe, cache) and the shipped bridge packs
nodetool packs compile [--json] [--force] [json, no-db]
nodetool harness audit
Audit surface coverage. A surface with no harness must carry a written gap note; one without it fails the audit.
Options: --json, and --strict to exit non-zero while any gap remains.
nodetool harness audit
nodetool harness audit --strict
Covered surfaces list their harnesses; gaps print the note that justifies them:
ok sandbox-packages packs-compile, validate
GAP electron-shell Electron shell (windows, IPC, menus, auto-update)
Documented gaps:
electron-shell: Covered by Jest unit tests only. A harness would boot the
packaged shell headlessly (Playwright's Electron driver), assert the IPC
surface, and reuse backend-smoke for the server half.
nodetool harness capabilities
The same invariant one rung down, over the agent capabilities rather than the
product surfaces: every exported capability must name the checked-in suites that
exercise it, the eval cases that drive a model through it, or a written gap note.
Reach for it after adding a capability — a new one with neither a check nor a
note fails here, and in npm run capabilities:check.
Options: --json, and --strict to exit non-zero while any gap remains.
nodetool harness capabilities
nodetool harness capabilities --strict
The header counts covered capabilities, then each gap prints the note that justifies it:
Capability coverage: 216/224 covered, 8 gap(s)
GAP ui_get_graph ui
GAP get_apify_actor apify
Documented gaps:
get_apify_actor: The Apify suites cover search, schema, run, abort, and the
dataset reader; fetching one actor's record has no case. A read-only case
against the recorded actor fixture would close it.
A trailing Contract drift: line means a capability’s declared description,
schema, category, or needsToolCallId moved without its coverage mapping being
touched. Refresh the table with npm run capabilities:sync and say which case
covers the new contract.
nodetool harness gate [files...]
Run from a NodeTool Git checkout. Outside a checkout, the command exits with an
explanation and supports --json errors. For local documents in an installed
CLI, use validate, jsscript test, app debug, or timeline debug.
Map a diff onto surfaces by path and run the selfcheck of every harness covering a touched surface. The diff selects the checks, not the author. Harnesses that need a target or a key are printed as manual work rather than silently skipped.
Options:
--base <ref>— diff against a git ref instead of the working tree.--all— ignore the diff and run every selfcheck.--expensive— include the expensive selfchecks (bundle staging and friends).--dry-run— print the plan without running anything.--json— print the plan, and the results unless--dry-run.--strict— exit non-zero when the diff touches a surface no harness covers.--deps— also touch surfaces whose paths sit in a workspace downstream of a changed one.--include-suites— also runsuiteOnlyselfchecks, which only re-run suitesnpm run test:affectedruns for the same diff, and the workspace test steps dropped becausetest:affectedruns their whole suite.--jobs <n>— run cheap selfchecksnat a time (default 1). Each check’s output prints as one block when it finishes. Expensive checks run serially afterwards.--timeout <seconds>— kill a selfcheck that runs longer (default 900).
A changed root manifest, lockfile, root tsconfig*.json, turbo.json,
.nvmrc, or shared scripts/run-*.mjs launcher selects every selfcheck. The
plan step loads no built package, so --dry-run works before
npm run build:packages.
A workspace test step (npm run test --workspace=<pkg> [-- filters]) is
dropped from a selfcheck’s && chain when test:affected runs that
workspace’s whole suite for the diff: any workspace on a global-file diff,
otherwise an affected backend package. The JSON output lists the dropped
steps as prunedSuites. Steps with other work, shell syntax, or unknown
workspaces still run. This assumes test:affected runs for the same diff.
--all alone does not enable these skips.
CI adds --include-suites when its skip-tests input disables package tests.
Check starts and completions print to stderr, including with --json.
Each result includes durationMs, while stdout keeps the JSON output.
CI uses --jobs 1 and NODETOOL_TEST_WORKERS=1 to run selfchecks and their
test files serially. This prevents competing selfchecks from exhausting
the game runtime’s wall-clock script batch budgets. Timeouts still apply.
nodetool harness gate --base main
nodetool harness gate --dry-run packages/base-nodes/src/index.ts
nodetool harness gate --all --expensive
The plan names the surfaces the diff touched before it runs anything:
1 changed file(s) touch 1 surface(s):
workflow-execution (1 file(s))
Manual harnesses (need a target/key — run yourself):
debug nodetool debug <id|file> [--browser --trace --watch --supervise]
3 selfcheck(s) to run
nodetool package
Scaffold TypeScript node packages and inspect local Python package metadata.
init scaffolds a TypeScript package. list, docs and node-docs read
Python package metadata files. workflow-docs reads workflow JSON files.
nodetool packs installs, removes and diagnoses published
node packs and runtime libraries, and compiles sandbox packs. Use
nodetool node list and nodetool node info <type> to discover the TypeScript
nodes available to this CLI.
Subcommands: list, init, docs, node-docs, workflow-docs
nodetool package list
List Python package metadata in the current directory and its workspaces,
with node counts. -a, --available lists the shipped Python pack catalog.
--json prints the rows as JSON. For installed TypeScript runtime packs use
nodetool packs list. For executable node types use nodetool node list.
nodetool package list
nodetool package list --available
nodetool package list --json
name │ version │ description │ nodes
───────────────┼─────────────┼─────────────────────┼───────
nodetool-base │ 0.6.3-rc.41 │ Nodetool Base nodes │ 0
--available prints the Python packs in PYTHON_NODE_PACKS (packages/protocol/src/python-packs.ts). See
Node Packs for what a pack is and how one is installed.
nodetool package init
Scaffold a new TypeScript node package in the current directory. Takes no options — it prompts for what it needs.
mkdir nodetool-my-nodes && cd nodetool-my-nodes
nodetool package init
nodetool package docs, node-docs, workflow-docs
Generate Markdown from local Python package metadata: docs writes one
overview and node-docs writes a page per node. workflow-docs documents
workflow JSON files. Each takes -v, --verbose. An empty node-docs result
points to nodetool node list and nodetool node info <type> for installed
TypeScript node metadata, including properties and outputs.
Options:
docs—-o, --output-dir <dir>(defaultdocs),-c, --compactfor a shorter overview.node-docs—-o, --output-dir <dir>(defaultdocs/nodes),-p, --package-name <name>to emit only nodes under a namespace prefix.workflow-docs—-o, --output-dir <dir>(defaultdocs/workflows),-e, --examples-dir <dir>(required) to point at the workflow JSONs, and-p, --package-name <name>to keep only those whosepackage_namematches.
nodetool package docs --compact
nodetool package node-docs --package-name nodetool.text
nodetool packs
The npm CLI includes core, text, data, LLM, and code nodes. Install other node
packs and npm runtime libraries with nodetool packs in a global CLI
installation or an npm project. Temporary npx installations and development
checkouts support diagnostics but refuse installation changes.
| Command | Behavior |
|---|---|
packs compile [--json] [--force] |
Compile installed sandbox pack modules. See sandbox compilation. |
packs list [--json] |
Show available packs, installation state and size estimates. |
packs add <ids...> [--yes] |
Install the pinned packs or runtime libraries and their requirements. |
packs add --restore [--yes] |
Restore the desired set recorded for this install root. |
packs add --for <workflow-or-file> [--yes] |
Install missing requirements of a saved workflow or an explicitly named local file. |
packs remove <ids...> |
Remove packages owned exclusively by the selection while preserving retained requirements. |
packs doctor [--json] [--installed] [--pack <ids...>] |
Check the mode, versions, integrity and installation state. Registration checks use a fresh child process. |
packs doctor --repair |
Repair a dirty installation after verifying its pinned cohort. |
packs why <node_type-or-id> |
Explain ownership, requirements and installation state. |
Add commands prompt with package versions, size estimates, approved scripts
and local manifest changes. Use --yes in a non-interactive terminal. Local
installation updates package.json and package-lock.json. Restart running
servers after adding or removing packs. After upgrading the CLI, use
nodetool packs add --restore to reinstall the recorded pack and runtime set
for that installation root.
run, debug, node run and workflows run accept --install-missing.
NODETOOL_AUTO_INSTALL=1 also authorizes those commands. They install missing
npm requirements and re-execute once, forwarding the child exit code. Serve,
MCP and agent tools do not install packages automatically. External runtimes
such as ffmpeg and Python require their own setup.
See the pack-system design for installation boundaries and the platform matrix.
nodetool packs compile
Compile every npm-backed sandbox module of every installed pack. A pack can
declare a guest module by npm dependency name instead of authoring code
({"name": ".", "kind": "js", "npm": "js-yaml"}); this builds it — esbuild
bundles the dependency with no externals, a scope-aware scan rejects free
references to globals the guest lacks, and a capability-free QuickJS probe
imports the bundle to prove it initializes. Results are cached by content
digest, never by version.
The server compiles during its own catalog refresh and Electron compiles after an install, so reach for this to warm the cache or to diagnose one pack.
Options:
--json— print the compile report as JSON.--force— recompile and re-probe even when the cache has an answer.--pack-search-path <dir>— search thisnode_modulesroot (repeatable).
nodetool packs compile
nodetool packs compile --json
nodetool packs compile --force
Each module reports its source dependency and bundled size:
ok @nodetool-ai/sandbox-markdown ← marked — 66101 bytes
ok @nodetool-ai/sandbox-yaml ← js-yaml — 103863 bytes
10 compiled, 0 skipped.
Everything that stops a module short of admission is a named skip, not an
error: npm-module-builtin-import (the dependency needs node:*),
npm-module-unresolved, npm-module-too-large (1 MB cap),
npm-module-forbidden-global, and npm-module-probe-failed.
nodetool reliability
Run a reliability journey on more than one execution surface and diff each one against the kernel oracle. A journey is a small workflow plus the invariants its run must hold — lifecycle pairing, one terminal message, no leaked cleanup — and the point is that the kernel runner and the WebSocket server must produce the same stream for it. Reach for it when a change touches execution and you need to know whether the surfaces still agree.
Journeys live in reliability/journeys/.
Working in the monorepo, run this one from the built CLI (npm run nodetool --)
rather than from source: the journey fixtures use decorators, and the
dev:nodetool transform rejects them with Decorators are not valid here.
Subcommands: list, run, update-goldens
nodetool reliability list
List the journeys, each with its surfaces and the invariants it asserts. Takes
--json for the full summaries.
nodetool reliability list
nodetool reliability list --json
linear-text-pipeline
Journey #1: input -> transform -> output.
The baseline — if this diverges across surfaces, everything is suspect.
surfaces: kernel, ws-server
invariants: lifecycle-pairing, terminal-uniqueness
nodetool reliability run <journey>
Run one journey on the surfaces it declares and diff every non-oracle surface against the kernel oracle.
Options:
--surface <name>— run only this surface, repeatable, overriding the journey’s own list. The kernel oracle is always included.--faults <name>— inject this fault, repeatable, replacing the journey’s declared fault matrix. The provider-seam faults are implemented (provider-429,provider-500,provider-timeout,truncated-stream,malformed-sse,slow-drip,cost-omission); thews/bridge/host/clientnames parse but report as unimplemented.--diff— print the full per-channel stream diff for a diverging surface.--json— print the wholeCompareReportinstead of the summary.
nodetool reliability run linear-text-pipeline
nodetool reliability run linear-text-pipeline --surface kernel
nodetool reliability run mid-run-cancel-node --faults provider-429 --diff
nodetool reliability update-goldens <journey>
Rewrite a journey’s expected/ fixtures from a fresh unfaulted kernel run. For
a golden that legitimately moved — read the diff before committing it, since
this command cannot tell a fixed bug from a new one.
nodetool reliability update-goldens linear-text-pipeline
nodetool eval <suite>
Run one of the agent evaluation suites against a provider and model, and report its metrics — success rate, expectation score, tool calls, duration, and cost.
Suites: graph-planner, graph-e2e, code-gen, task-planner, subtask,
codeact, app-build, tool-loop, workflow-escalation, script-tools,
jsscript-tools, sketch-tools, timeline-tools, storyboard-tools,
model3d-tools, app-tools, memory-tools, creative-pipeline.
Options (the same on every suite):
-p, --provider <id>— provider id (anthropic,openai,claude_agent_sdk,ollama, …).-m, --model <id>— model id for that provider.--cases <ids>— comma-separated case ids to run (default: all).--list— list the suite’s cases and exit.--json— print the full report as JSON.--out <path>— write the JSON report to a file.--max-iterations <n>— turn cap per case for the loop-style suites (default 12).--timeout <ms>— per-case execution timeout for the suites that run what they plan (default 300000).--system-prompt <text>— the caller preamble the planner suites plan behind (default: the AgentNode’s own prompt).--no-agent-prompt— plan with no caller preamble, which scores the bare planner contract.--judge-model <provider/model>— the model that judges outputs in the self-judging suites (graph-e2e,app-build). Defaults to the run’s own provider and model, which grades its own work.--min-success <rate>— exit non-zero when the success rate falls below this threshold (0..1).--min-score <rate>— exit non-zero when the mean expectation score falls below this threshold (0..1).--min-cases <n>— exit non-zero when fewer thanncases ran. Skipped cases do not count.--max-cost-usd <usd>— exit non-zero when the run’s total provider spend exceeds this.--max-p95-duration-ms <ms>— exit non-zero when the p95 case duration, skipped cases excluded, exceeds this.--keyless— run only the cases that need no API key and no network (see--list).--no-find-model— run without configured model providers, skipping the model-dependent cases.
Examples:
# See what a suite covers before spending on it — needs no provider
nodetool eval graph-planner --list
nodetool eval graph-planner -p anthropic -m claude-sonnet-5
nodetool eval timeline-tools -p openai -m gpt-5.4-mini --cases cut-and-trim,titles-with-motion
nodetool eval graph-e2e -p anthropic -m claude-sonnet-5 --timeout 600000
nodetool eval codeact -p anthropic -m claude-sonnet-5 --json --out report.json
# Gate a run in CI
nodetool eval sketch-tools -p ollama -m qwen-3.5:4b --min-success 0.8
--list prints each case id with what it checks:
summarize Single LLM step with one wired input and one output
branch-both-paths Conditional with both If branches wired
deterministic-over-llm Pure string mechanics must not be solved with an LLM step
nodetool jtbd
An eval suite scores a model. A job asks whether the product let the agent finish something, and keeps the transcript so you can say what to change.
A job is one objective taken end to end across whatever surfaces it needs,
stated the way a user would state it, handed to the agent in the user’s own
words, and graded on the world it left behind. No job names a tool — which tool
the agent reaches for, and in what order, is what is under test. The worlds are
the same headless bridges the tool-loop eval suites drive.
Subcommands: list, run, optimize.
# What the jobs are, before spending anything — needs no provider
nodetool jtbd list
nodetool jtbd list --json
Each entry prints the job id, its difficulty tier, the surfaces it crosses, the statement, and the outcomes it grades:
storyboard-a-scene [standard] storyboard
When I have a scene to shoot, I want it broken into shots with the action
described, so I can see the coverage before I spend on renders.
outcomes: five-shots, action-written, savable
nodetool jtbd run
Drives a model through the jobs and writes one bundle per job.
-p, --provider <id>— provider id (anthropic,openai,ollama, …). Required.-m, --model <id>— model id for that provider. Required.--jobs <ids>— comma-separated job ids (default: all; seelist).--out <dir>— bundle directory (defaultnodetool-debug/jtbd).--max-iterations <n>— turn cap per job, for jobs that declare none.--min-achieved <rate>— exit non-zero when the achievement rate falls below this threshold (0..1).--no-find-model— run without configured model providers.--json— print the full report as JSON.
nodetool jtbd run -p anthropic -m claude-sonnet-5
nodetool jtbd run --jobs workflow-from-prompt,timeline-assemble-cut -p openai -m gpt-5.4-mini
# Gate a run in CI
nodetool jtbd run -p anthropic -m claude-sonnet-5 --min-achieved 0.8
run writes nodetool-debug/jtbd/<job>/ holding report.json (transcript,
tool calls, outcomes, friction) and review.md, the same run rendered for a
person to read. The transcript is the point: it records what the model was told
and what it said between calls, not only which tools fired.
Before any model reviews a run, a pure pass derives what the transcript decides
on its own, and each finding names an owner. A tool that errored repeatedly, or
answered the same call identically three times, is a harness finding — the
fix is a schema or an error string in NodeTool’s own code. A run that called no
tool at all is a prompt finding. A run that took far more calls than the job
needs is unattributed, because whether that is a prompt failing to describe
the short path or a tool surface forcing the long one is the judgement a pure
pass cannot make.
nodetool jtbd optimize
Hands one recorded run — the system prompt verbatim, every assistant turn, every call with its arguments and result — to a different model and asks what one change would make the next run go better.
-p, --provider <id>— reviewing provider; use a different one than ran the jobs.-m, --model <id>— reviewing model id.--bundle <dir>— bundle directory to review (defaultnodetool-debug/jtbd).--all— review clean runs too, not just failures and friction.--json— print the proposals as JSON.
nodetool jtbd optimize -p openai -m gpt-5.4-mini
nodetool jtbd optimize -p openai -m gpt-5.4-mini --all --bundle ./my-runs
A proposal must name a target and a change; “improve the prompt” is rejected by
the parser. Proposals land in proposals.json next to the run.
It proposes; it never applies. Nothing in the loop edits a prompt or a tool. Run and review are separate commands because the bundle is the handoff: a bundle can be re-reviewed with a better optimizer without paying for the runs again, and a person can read the transcript before any model proposes anything.
Tips
- Use
--jsonflags for machine-readable output suitable for scripting. - Set
NODETOOL_API_URLenvironment variable to avoid specifying--api-urlon every command. - Use
nodetool serveto start the local backend server before running API commands. - See Environment Variables for a complete list of configurable variables.