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) or json.
  • --no-trace-stdout — disable stdout span output (overrides NODETOOL_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 (default 127.0.0.1) — bind address (use 0.0.0.0 for all interfaces).
  • --port (default 7777) — 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 .json workflow file, or path to a .ts DSL 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, or temp_url.
  • --workspace <dir> — workspace directory for workspace mode 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.
  • --supervise and 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 .json workflow file.

Options:

  • --api-url <url> — fetch the workflow from a remote server instead of the local database (default NODETOOL_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. node writes a standalone .mjs that 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.
  • --supervise and 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 (default anthropic/claude-sonnet-4-6, or NODETOOL_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 to DIRECT_URL or DATABASE_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-only run_search fan-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 to default, with approval prompts. Piped input defaults to auto.
  • --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; 0 lifts it. Default: the NODETOOL_AGENT_TURN_COST_CAP_USD setting.
  • --timeout <s> — wall-clock bound on one turn, in seconds; 0 leaves it no time at all. Default: the NODETOOL_AGENT_TURN_DEADLINE_MS setting.

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 (default NODETOOL_API_URL).
  • --package <name> — owning package (default nodetool-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 (default NODETOOL_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 (default nodetool-debug/<id>-<timestamp>).
  • --timeout <ms> — per-surface run timeout.
  • --json — print the full DebugReport as JSON to stdout.
  • --watch — re-run on file change and print a diff of the verdict (file targets only).
  • --supervise and 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 @prop name (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 (default default).
  • --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. A resource:<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 (default nodetool-debug/app-<id>-<timestamp>).
  • --timeout <ms> — per-run timeout.
  • --json — print the full AppDebugReport to 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 (env NODETOOL_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 (default 3).
  • --cost-cap <usd> — ceiling on build spend (default 2).
  • --timeout <ms> — wall clock for the whole build (default 600000).
  • --out <dir> — bundle directory (default nodetool-debug/app-build-<slug>-<timestamp>).
  • --json — print the full BuildReport to stdout.
  • --no-judge — skip the judge stage. The verdict’s notSimulated then 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; buildApp itself 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 full TimelineValidation as 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 (default nodetool-debug/timeline-<id>-<timestamp>).
  • --json — print the full TimelineDebugReport as 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 --stills or --sheet (default nodetool-render/<name>.<ext>, <name>-frames/, or <name>-sheet.png).
  • --format <format> — mp4 (default), webm, mov or png_sequence.
  • --frames <spec> — render only these frames: indices and inclusive ranges, e.g. 0-89 or 175,290,380-390. Use coverage:<file> to read the Frame column of a Markdown table in a builder file’s leading comment. Non-integer rows are skipped and reported.
  • --stills — write each selected frame as frame_<index>.png instead 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. --sheet implies 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 renderer timeline render --sheet uses.
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, --yes skips 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 full SketchValidation as 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 (default nodetool-debug/sketch-<id>-<timestamp>).
  • --json — print the full SketchDebugReport as 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 .png or .psd).
  • --format <png|psd> — png (default) or a layered psd at 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-layer drawn and issue).
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 full JsScriptValidation as 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 (default nodetool-debug/jsscript-<id>-<timestamp>).
  • --json — print the full JsScriptDebugReport as 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> (default 1), --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> (default 1), --backend <canvas2d|webgpu|webgl2>, --assets-dir <directory>, --json. A schema version 3 (3D) document defaults to webgl2. A 2D document accepts canvas2d (default) or webgpu.
  • game build <game_file> — --out <directory> (required), --assets-dir <directory>, --target <web|tauri> (default web), --json. --target tauri writes a Tauri desktop app project: the web player in dist/ and a native shell in src-tauri/. It accepts --name <name>, --identifier <reverse-dns>, --app-version <MAJOR.MINOR.PATCH>, --icon <png|jpeg|webp>, and --compile, which runs cargo build --release and reports the executable path. Run npm install && npm run build in 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 with npx 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 (for list).
  • --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 (for list).
  • --content-type <type> — filter by content type (for list).
  • --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 (for create).
  • -n, --n-results <n> — number of matches to return (for query, default 10).
  • -y, --yes — skip the confirmation prompt (for delete).
  • --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 (for list and by-model).
  • --model <id> — filter by model (for list).
  • --limit <n> — max results (for list, default 50).
  • --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, or interrupted (for list).
  • --provider <name> / --capability <name> — filter by provider, or by capability such as text_to_video (for list).
  • --thread-id <id> / --job-id <id> — only what a chat thread or a workflow run asked for (for list).
  • --since <iso> — only generations created at or after this time (for list).
  • --limit <n> — max results (for list, default 50).
  • --timeout <seconds> — how long await waits before giving up (default 300).
  • --provider <name> is required, with --model <id>, for provider-list and provider-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 — 0 once the row reaches a terminal status, 1 if it is still running when the timeout expires.
  • cancel — 0 if it was running and is now cancelled, 1 if 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 — 0 when the provider answered and the row was updated, 1 otherwise, with a reason naming which: no request id, or no reconciler for <provider> when that provider has no billing API, which also marks the row unavailable so 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 (for login).
  • --manual — skip the loopback listener and paste the code the browser shows. This is the flow for a headless or remote machine (for login).
  • --no-browser — print the URL instead of opening a browser (for login).
  • --force — refresh even when the current token is still valid (for refresh).
  • --json — output as JSON. Available on login, status, and refresh.

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, --type to filter).
  • by-provider <provider> — list models for a provider; --kind one of llm, image, tts, asr, video, embedding (default llm).

Examples:

nodetool models list
nodetool models providers
nodetool models ollama
nodetool models by-provider openai --kind image

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 of all (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 exits 1 listing 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 exits 1.
  • --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 (default http://localhost:7777, or NODETOOL_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 a Generic types (require --task): block. The types in that block need a pipeline tag when you search them; the ones above it do not. --json prints {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; --json prints the raw entries. Without --task, a generic type fails with Model 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-only drops 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-only keeps only complete repos; --limit <n> and --json behave 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); use hf-cache when 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.cpp on Linux, ~/Library/Caches/llama.cpp on 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 a nodetool-<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 (default 1). 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 same createChatCodeActSession the 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, the ui_* 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 — as nodetool.<namespace>.<method>(), by importing them, or found with await 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 db group (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 on add. docker is the only supported value.
  • --dry-run — print what apply would do without executing it.
  • -f, --follow / --tail <n> / --service <service> — for logs; --tail defaults to 100.
  • --force — skip the confirmation on destroy, workflows delete, and users-remove.
  • --json — machine-readable output on list, workflows list, and users-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, on profile add or inline on create.
  • --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 exposes 22/tcp so the worker can be debugged.
  • --disk <gb> — persistent volume size, which holds the model cache (default 100).
  • --token-policy <generate|fixed> — default generate.
  • --profile <name> / --attach — for create.
  • --all — for stop, stops every non-stopped worker.
  • --json — for list and profile 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 for download and delete.
  • --file-path <path> — download a single file instead of the repo.
  • -a, --allow-patterns <pattern> / -i, --ignore-patterns <pattern> — glob filters on download, both repeatable.
  • --json — for list.
# 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 to telegram-bot.json, relative to the working directory. Both subcommands take it. Without the flag, telegram-bot.json is 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.

  • install registers the native messaging host with Chrome, Chromium, Brave, Edge and Arc. It prints the wrapper path, each manifest it registered, and the extension ID.
  • status prints connected or not connected, followed by one socket: <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 run suiteOnly selfchecks, which only re-run suites npm run test:affected runs for the same diff, and the workspace test steps dropped because test:affected runs their whole suite.
  • --jobs <n> — run cheap selfchecks n at 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> (default docs), -c, --compact for a shorter overview.
  • node-docs — -o, --output-dir <dir> (default docs/nodes), -p, --package-name <name> to emit only nodes under a namespace prefix.
  • workflow-docs — -o, --output-dir <dir> (default docs/workflows), -e, --examples-dir <dir> (required) to point at the workflow JSONs, and -p, --package-name <name> to keep only those whose package_name matches.
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 this node_modules root (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); the ws/bridge/host/ client names parse but report as unimplemented.
  • --diff — print the full per-channel stream diff for a diverging surface.
  • --json — print the whole CompareReport instead 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 than n cases 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; see list).
  • --out <dir> — bundle directory (default nodetool-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 (default nodetool-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 --json flags for machine-readable output suitable for scripting.
  • Set NODETOOL_API_URL environment variable to avoid specifying --api-url on every command.
  • Use nodetool serve to start the local backend server before running API commands.
  • See Environment Variables for a complete list of configurable variables.