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.x. Check with node --version; 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).
Global Options
These flags work on any nodetool command and control OpenTelemetry tracing:
--trace-file <path>— append every LLM/agent/workflow span to<path>as JSONL (analyzer-friendly).--trace-stdout [format]— stream spans to stdout:pretty(default) orjson.--no-trace-stdout— disable stdout span output (overridesNODETOOL_TRACE_STDOUT).
nodetool --trace-file trace.jsonl run workflow.ts
nodetool --trace-stdout pretty workflows run <id>
Core Commands
nodetool info
Display system and environment information including Node.js version, platform, and API key configuration.
Options:
--json— output as JSON.
Example:
nodetool info
nodetool info --json
nodetool serve
Starts the TypeScript WebSocket + HTTP backend server. This serves the REST API, WebSocket endpoints, and static assets.
Options:
--host(default127.0.0.1) — bind address (use0.0.0.0for all interfaces).--port(default7777) — listen port.
Examples:
# Start the server on the default port
nodetool serve
# Bind to all interfaces on a custom port
nodetool serve --host 0.0.0.0 --port 8080
serve passes the two flags to the server as HOST and PORT, overwriting whatever those variables already held — use the flags, not the environment, to move the server.
nodetool workflows run <workflow_id_or_file>
Executes a workflow by ID (from the local database), JSON file, or TypeScript DSL file.
Arguments:
<workflow_id_or_file>— workflow ID, path to a.jsonworkflow file, or path to a.tsDSL file.
Options:
--params <json>— JSON string of workflow parameters.--json— output result as JSON.--superviseand its bounds — see Supervised runs.
Examples:
# Run workflow by ID
nodetool workflows run workflow_abc123
# Run workflow from JSON file
nodetool workflows run ./my_workflow.json
# Run workflow from TypeScript DSL
nodetool workflows run ./my_workflow.ts
# Run with parameters as JSON
nodetool workflows run workflow_abc123 --params '{"input": "hello"}'
# JSON output for automation
nodetool workflows run ./my_workflow.json --json
--json never expands a large binary output into the JSON text. An image,
audio, or video payload over 64 KiB is written to
nodetool-output/<job_id>/payload-N.<ext> and appears as
{"$file": "…", "bytes": N, "mimeType": "…"} in its place; the path is
reported on stderr. Image nodes emit raw RGBA as their in-flight format, so a
raw ref is PNG-encoded on the way out. nodetool run and nodetool node run
do the same, spilling into nodetool-output/.
nodetool workflows export-dsl <workflow_id_or_file>
Exports a workflow as a TypeScript DSL file.
Arguments:
<workflow_id_or_file>— workflow ID or path to a.jsonworkflow file.
Options:
-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 run <dsl-file>
Shorthand for running a TypeScript DSL workflow file directly.
Options:
--json— output results as JSON.--superviseand its bounds — see Supervised runs.
Examples:
nodetool run workflow.ts
nodetool run workflow.ts --json
Supervised runs
--supervise puts an agent on the failure path: when a node invocation throws
after its own error handling is exhausted, the agent sees the failure and
answers with one verdict — retry, repair the output, skip the item, or fail.
Without the flag nothing changes: no escalation is constructed and the run is
the run it was before.
Available on nodetool run, nodetool workflows run, and nodetool debug
(server surface).
Options:
--supervise— supervise this run. Off unless passed.--max-decisions <n>— decisions allowed in the run (default 10).--max-retries <n>— retries per node invocation (default 2).--supervisor-cost-cap <usd>— ceiling on supervisor spend (default 0.50), enforced by reservation before each model turn, not after.--supervisor-model <provider/model>— who supervises (defaultanthropic/claude-sonnet-4-6, orNODETOOL_SUPERVISOR_MODEL). The leading segment must be a registered provider; the rest is the model id, slashes and all (openrouter/openai/gpt-5.4-mini).
Passing a bound without --supervise is an error rather than a silent
unsupervised run.
Output. Each decision prints a ⛨ line as it happens, and the run ends
with a supervised summary:
⛨ fetch-item [3] skipped — HTTP 404 (agent, $0.0041)
⛨ supervised: 2 skipped, 1 retried, 3 decisions, +$0.0200
With --json, the decisions are in interventions — alongside the outputs in
workflows run, and under {results, interventions} in nodetool run, whose
bare results shape is left alone for unsupervised runs. nodetool debug puts
them in server.summary.interventions with a server.supervised rollup. Each
record carries the
escalation the agent saw (node, item lineage, redacted inputs, allowed
actions), the verdict, who decided it (agent, sticky, bounds, default,
kernel), and its cost.
Cost. Supervisor spend lands in the same ledger nodetool costs reads, one
row per billable decision, attributed to the run and tagged supervisor in
node_type — so supervision is separable from the workflow’s own spend:
nodetool costs list --limit 20 # supervisor rows show node_type=supervisor
Bounds are the guarantee. Every supervisor failure (timeout, an unparseable
verdict, an exhausted budget, a cancelled run) resolves as fail, which is
what would have happened without it. What each verdict means and why
retry is opt-in per node: workflow-supervisor-design.md.
Database Migrations
nodetool db migrate
Applies NodeTool migrations to a PostgreSQL/Supabase database. For Supabase, use the direct connection URL from Settings → Database (port 5432), not the transaction pooler URL.
Options:
--direct-url <url>— Supabase/PostgreSQL direct connection URL.--database-url <url>— connection URL; defaults toDIRECT_URLorDATABASE_URL.--target <version>— stop after a specific migration version.--dry-run— show pending migrations without applying them.--skip-checksums— skip checksum validation.--json— output as JSON.
Examples:
DIRECT_URL="postgresql://postgres:[password]@db.[project].supabase.co:5432/postgres" \
nodetool db migrate
nodetool db status --direct-url "$DIRECT_URL"
nodetool db migrate --direct-url "$DIRECT_URL" --dry-run
Other migration commands:
nodetool db status --direct-url "$DIRECT_URL"
nodetool db baseline --direct-url "$DIRECT_URL" # for existing DBs
nodetool db rollback --direct-url "$DIRECT_URL" --steps 1
Chat
nodetool chat
Starts an interactive TUI chat session.
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).-w, --workspace <path>— workspace directory for file operations (default: current directory).--tools <tools>— comma-separated list of enabled tools.--permission-mode <default|auto|plan>— how tool calls are gated when the input is piped (see Permission mode). Unset runsauto. The interactive TUI does not gate its belt, and says so if the flag is passed.--cost-cap <usd>— ceiling on provider spend for one turn;0lifts it. Default: theNODETOOL_AGENT_TURN_COST_CAP_USDsetting.--timeout <s>— wall-clock bound on one turn, in seconds;0leaves it no time at all. Default: theNODETOOL_AGENT_TURN_DEADLINE_MSsetting.
Both flags override two of the five NODETOOL_AGENT_* settings; the other
three — concurrency, total turns, unpriced-token ceiling — come from the
settings alone. The budget is one object per turn, shared by every loop the
turn starts, so a ceiling bounds the turn rather than each loop separately. A
turn a ceiling refuses prints the reason: [stopped] turn budget of $5 reached
piped, or a Stopped: line in the TUI. Unlike --permission-mode, these two
apply to the interactive session as well.
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, get, run, export-dsl, 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:
--package <name>— owning package (defaultnodetool-base).-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:
-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.
nodetool workflows import-bundle my-pack.nodetool
nodetool workflows migrate-code-inputs
A one-time repair for saved nodetool.code.Code bodies. A Code node’s declared
inputs used to arrive as globals of their own name; they now arrive on one
inputs object, so a body written the old way throws a ReferenceError on its
first input read. This walks the saved workflows and rewrites name to
inputs.name for every name the node can read — its declared slots, its inline
dynamic properties, and any handle an edge feeds it.
The rewrite is done on the AST, so a name inside a string, a comment, an object
key, or a local binding is left alone. It is safe to re-run: a body already
reading inputs.* has nothing to rewrite.
Options:
--dry-run— report what would change and write nothing.--user-id <id>— migrate this user’s workflows instead of the local user’s (1).--json— print the report as JSON rather than the per-node lines.
# See what would change first
nodetool workflows migrate-code-inputs --dry-run
# Then do it
nodetool workflows migrate-code-inputs
Each rewritten node prints one line naming the inputs it moved, followed by a count:
4 Code node(s) in 3 workflow(s) were rewritten (11 scanned across 7 workflows).
A body that fails to parse is counted under a trailing N failed. line and left
untouched, so one bad node does not stop the pass.
nodetool validate <workflow_id_or_file>
Check a workflow against the node registry without running it: unknown node
types, missing required properties, dangling or mis-typed edges, model
properties naming a provider or model id that does not exist,
and nodetool.code.Code bodies that never return or leave an output unset. On
a workflow-id target, where the secret store is reachable, it also warns about
declared credentials the install cannot resolve. It finishes in well under a
second, which makes it the cheap pre-flight before a run that costs money.
The target is a workflow id, a workflow JSON file, or a TypeScript DSL file. File targets need no database.
Options:
--json— print the full validation report as JSON.--warnings-as-errors— exit non-zero on warnings, not just errors.
Examples:
nodetool validate <workflow_id>
nodetool validate workflow.json
nodetool validate workflow.ts --json
nodetool validate <workflow_id> --warnings-as-errors
A clean graph prints a one-line verdict and exits 0:
✅ Workflow is valid — 3 node(s), 2 edge(s).
A broken one names the node and the rule it broke, then exits 1:
❌ Workflow has 1 error(s).
error Unknown node type "nodetool.text.NoSuchNode" (not in the registry; Python-only nodes are not validated statically) [nodetool.text.NoSuchNode 1] (unknown_node)
Codes:
unknown_node — node type is not in the registry
nodetool debug <workflow_id_or_file>
Run a workflow end to end and collect everything it emitted into a
self-contained bundle — every message, log line, node input/output, and error —
then print a verdict. Reach for it when validate passes but the run still does
the wrong thing.
The headless server run is on by default. The expensive surfaces are opt-in:
--browser starts Playwright and Chromium, --trace adds OpenTelemetry
spans, and --stages screenshots the canvas at each run stage.
Options:
--no-server— skip the headless server surface.--browser— also run the workflow in a real browser (Playwright).--trace— capture an OpenTelemetry trace of the server run (timing, tokens, cost).--stages— screenshot the canvas at every browser run stage (implies--browser).--params <json>— JSON params keyed by input-node name.--out <dir>— bundle output directory (defaultnodetool-debug/<id>-<timestamp>).--timeout <ms>— per-surface run timeout.--json— print the fullDebugReportas JSON to stdout.--watch— re-run on file change and print a diff of the verdict (file targets only).--superviseand its four bounds — see Supervised runs above.
Examples:
# Server surface only
nodetool debug <workflow_id>
nodetool debug workflow.json --params '{"prompt":"hi"}'
# Opt into the expensive surfaces
nodetool debug <workflow_id> --trace
nodetool debug <workflow_id> --browser --stages
# Tight edit→verify loop on a file target
nodetool debug workflow.json --watch
The bundle holds report.json and report.md, the resolved workflow.json,
and server/messages.jsonl. --trace adds server/trace.jsonl; --browser
adds browser/record.json, a canvas screenshot, and browser/console-errors.log.
nodetool node run <node_type>
Instantiate one node, hand it a property bag, and print what it emits — no
workflow, no graph. Use it when debug says a run went wrong and you want to
know whether one node is the reason.
Options:
--props <json>— JSON object of property values keyed by@propname (default{}).--no-secrets— skip secret and asset resolution, so the run touches no database.--json— print the full run result as JSON.
Examples:
nodetool node run nodetool.text.Concat --props '{"a":"hi ","b":"there"}'
nodetool node run nodetool.text.Concat --props '{"a":"hi ","b":"there"}' --no-secrets
nodetool node run nodetool.text.Concat --props '{"a":"hi ","b":"there"}' --json
The verdict names the node type, its title, and how long it took, then lists every record it emitted:
✅ nodetool.text.Concat (Concat) — 0ms
Emitted 1 record(s):
{"output":"hi there"}
Mini App Management
nodetool apps
Move mini apps between installs. Reads and writes the local database directly — no running server.
Subcommands: list, export-bundle, 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 import-bundle <bundle_file>
Import an application bundle into the local library: create its workflows and the app, with operations rewired to the new ids.
Options:
--project <id>— project to create the app in (defaultdefault).--json— output the created application as JSON.
A bundled workflow carrying a sourceId gets a row id derived from it, so importing two bundles that ship the same
workflow — two example apps binding one template — reuses the existing row instead of duplicating it. Without a
sourceId every import creates fresh workflows.
nodetool apps import-bundle my.app.json
nodetool apps import-bundle my.app.json --project my-project --json
nodetool app
Where nodetool apps moves finished apps around, nodetool app runs one and
builds one. Both subcommands are headless — no browser, no editor — and both
write a debug bundle under nodetool-debug/.
nodetool app debug <application_id_or_file>
Run a mini app the way the web runtime would: validate every widget binding against the workflows it binds, seed input defaults, apply params, replay a scripted interaction sequence, execute the workflows on the kernel, fold the streamed messages into the app’s reactive values, and report what each widget ends up showing.
The target is an application id (read from the applications table), an
ApplicationBundle JSON file (self-contained — operations reference bundle
keys, so it runs without a database), or a workflow id/file carrying a legacy
app_doc.
Options:
--params <json>— reactive values applied before interactions, keyed by input name. Aresource:<binding id>key seeds that collection with an array of items.--interact <json>— scripted steps:set,change,click,run(by operation id),cancel,seedResource.--no-run— static wiring check only; never execute a workflow.--out <dir>— bundle directory (defaultnodetool-debug/app-<id>-<timestamp>).--timeout <ms>— per-run timeout.--json— print the fullAppDebugReportto stdout.
Examples:
# Static wiring check — no provider calls, no workflow execution
nodetool app debug my.app.json --no-run
nodetool app debug <application_id> --params '{"prompt":"hi"}'
nodetool app debug <application_id> --interact \
'[{"set":{"key":"prompt","value":"hi"}},{"run":"draft"}]'
The verdict catches what a workflow-only run cannot: a widget bound to a token
the workflow has no input, output, node, or variable for, one naming an
operation the document never declares, an unknown widget type, a display widget
that never received a value, an app no interaction ever ran, an operation that
overran its timeout, and a click or change on a widget its own visibleWhen /
disabledWhen hides or disables — that step fails and names the condition.
Every declared operation runs, not just the first, and state is keyed per
operation.
The report lists what it did not simulate under notSimulated: layout, styling,
focus, and scroll, since nothing renders a DOM; the stored resource collections,
since a run reads the seeded in-memory provider rather than the database; and
reactive subgraph runs, where the browser reruns one input’s downstream subgraph
and the harness runs the whole workflow.
nodetool app build <prompt_or_spec_file>
Turn a prompt, or a hand-written spec.json, into a verified
ApplicationBundle. Six stages run in order: spec pins what the app must
do, plan builds one workflow per operation, author places and wires the
widgets through the real ui_app_* tool contract, check validates the
wiring, run replays every interaction on the kernel, and judge asks a
model whether each interaction achieved what was asked. Everything still wrong
at the end of a pass becomes one complaint, and the next round edits the
document rather than rebuilding it.
Plan, author, and judge each call a model, and run executes real workflows, so a build spends money and takes minutes.
Options:
-p, --provider <name>/-m, --model <id>— the builder’s provider and model.--judge-model <provider/model>— model that judges each interaction (envNODETOOL_APP_JUDGE_MODEL). Defaults to a configured model other than the builder’s, because a model grading its own work is the weakest reviewer available.--workflow <id>— pin an existing workflow instead of planning one (repeatable, in operation order).--max-repairs <n>— repair rounds after the first pass (default3).--cost-cap <usd>— ceiling on build spend (default2).--timeout <ms>— wall clock for the whole build (default600000).--out <dir>— bundle directory (defaultnodetool-debug/app-build-<slug>-<timestamp>).--json— print the fullBuildReportto stdout.--no-judge— skip the judge stage. The verdict’snotSimulatedthen says nothing scored the app.--watch— re-build on spec-file change and print a diff of the verdict (spec-file targets only). Every save spends money.--supervise,--max-decisions <n>,--max-retries <n>,--supervisor-cost-cap <usd>,--supervisor-model <provider/model>— the same flags described under Supervised runs. They apply to the Run stage, whose interactions execute on the kernel;buildAppitself is never supervised.
Examples:
nodetool app build "an app that drafts a note from a prompt" \
-p anthropic -m claude-sonnet-5
nodetool app build spec.json -p openai -m gpt-5.4-mini --json
# Bind an existing workflow instead of planning one
nodetool app build "..." -p anthropic -m claude-sonnet-5 --workflow <id>
# Structural check only, one repair round, tight cap
nodetool app build spec.json -p anthropic -m claude-sonnet-5 \
--no-judge --max-repairs 1 --cost-cap 1.00
The bundle holds report.json, report.md, spec.json,
interactions/<name>/run-N.messages.jsonl per replayed run, and — only for a
green build — app.bundle.json, the deliverable. The loop fails closed: an
exhausted budget, an issue that reappears after being fixed, or a cancelled
signal ends the build as failed with the reason named, and there is no bundle
behind a failed verdict. Exit code is 0 only when verdict.ok.
Editor Documents
Timelines, sketches, and JS scripts each get the same three commands:
validate checks the document without rendering or running it, debug replays
a scripted edit session against it, and versions reads and writes its snapshot
history. All of them take a JSON file or a row id — a path that exists on disk
wins over an id, and file targets need no database.
nodetool timeline
Subcommands: validate, debug, versions
nodetool timeline validate <timeline_id_or_file>
Check a timeline sequence without rendering it: clips on tracks the document
does not have, fields a schema round trip would strip, animation presets that
do not exist, and timings nothing can render. The target is a timeline JSON
file — a bare TimelineDocument or anything carrying one under document, so
a timeline.get response works as-is — or a timeline_sequences row id.
Options:
--json— print the fullTimelineValidationas JSON.--warnings-as-errors— exit non-zero on warnings, not just errors.
nodetool timeline validate sequence.json
nodetool timeline validate <timeline_id> --json
nodetool timeline validate <timeline_id> --warnings-as-errors
✅ 0 error(s), 0 warning(s)
Every finding carries a stable code. An error is a document no reading of
which produces the scene its author described; a warning is a picture that
renders and is not the one that was written — usually because this build could
not read something a newer one wrote (forward compatibility), or because the
authored motion did not fit the clip.
| Code | Severity | What it caught |
|---|---|---|
schema_invalid |
error | The document does not parse; every structural check is skipped |
duplicate_id |
error | Two tracks, clips or markers share an id |
clip_track_missing |
error | A clip sits on a track the document does not declare |
negative_timing |
error | A clip or marker starts before the origin, or a clip lasts zero |
fade_exceeds_duration |
error | The in and out audio fades overlap |
in_out_points_invalid |
error | The source span is negative or empty |
speed_multiplier_invalid |
error | A non-positive playback rate |
unknown_animation_preset |
error | A preset this build does not ship; nothing animates |
custom_animation_invalid |
error | Baked curves the one gate refuses; re-bake from the script |
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 |
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 |
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 |
replace_curves_overlap |
warning | Two animations drive one absolute channel (positionX/Y, anchorX/Y, trimStart/End) at the same time; the last in document order wins and the other is discarded |
text_illegible |
warning | Type under 2.5% of frame height, or under a 3:1 contrast ratio against its own background plate or a full-frame shape clip behind it |
clips_overlap, clip_shorter_than_frame, caption_out_of_range, binding_incomplete, duplicate_track_index, transcript_clip_missing, link_partner_missing |
warning | Structural smells that still render |
text_illegible refuses to guess: a colour notation it cannot read, a
translucent plate, gradient-filled type, or a backdrop it cannot prove sits
behind the text ends the contrast check rather than producing a finding.
nodetool timeline debug <timeline_id_or_file>
Run the same check, then execute each scripted step against the headless
ui_timeline_* bridge and validate the document the session left behind. A
step names a tool with or without the ui_timeline_ prefix. A failing step is
recorded and the script continues, so one bad target does not hide everything
after it. Rendering, playback, decode, and generation are not simulated; the
report lists that under notSimulated.
Options:
--interact <json>— the scripted steps, e.g.[{"tool":"add_track","input":{"type":"audio"}}].--out <dir>— bundle output directory (defaultnodetool-debug/timeline-<id>-<timestamp>).--json— print the fullTimelineDebugReportas JSON to stdout.
nodetool timeline debug sequence.json \
--interact '[{"tool":"add_track","input":{"type":"audio","name":"Music"}}]'
✓ ui_timeline_add_track
✅ Timeline is sound — 1 interaction(s) ran clean.
timeline: 2 track(s), 1 clip(s), 4000ms @ 30fps
session: 1 step(s), 0 failed
The bundle holds report.json, report.md, and timeline.json (the input
document). The command exits 0 only when the verdict is ok.
nodetool timeline versions
Read and write a sequence’s snapshot history against the local database:
manual saves, the autosaves timeline.update writes at most every five
minutes, and the pre-restore snapshot that makes a restore undoable.
Subcommands: list, show, create, restore, delete. All five take --json.
list <timeline_id>— newest first;--save-type <manual|autosave|restore>,--limit <n>(default 100).show <timeline_id> <version>— one version’s metadata and the document it stored.create <timeline_id>— snapshot the sequence as it stands now;--name <name>labels it.restore <timeline_id> <version>— snapshot the current state first, write the old document back, then re-validate it.delete <timeline_id> <version>—-y, --yesskips the confirmation prompt.
nodetool timeline versions list <timeline_id> --save-type manual --limit 10
nodetool timeline versions show <timeline_id> 3 --json
nodetool timeline versions create <timeline_id> --name "before the recut"
nodetool timeline versions restore <timeline_id> 3
nodetool timeline versions delete <timeline_id> 3 --yes
An old document is restored against today’s schema, so what it used to pass is not what it passes now: a restore whose document no longer validates prints the issues and exits non-zero.
nodetool sketch
Subcommands: validate, debug, versions
nodetool sketch validate <sketch_id_or_file>
Check a sketch (image document) without opening an editor: a duplicate layer
id, an activeLayerId or binding pointing at a layer the document lacks,
opacity or a blend mode no compositor ships, a binding with no workflow or
prompt behind it, and fields a schema round trip would strip. The target is a
{sketch, layerBindings} JSON file — or anything carrying one, so a
sketch.get response works as-is — or an image_documents row id. Layer
bitmaps stay opaque to the check.
Options:
--json— print the fullSketchValidationas JSON.--warnings-as-errors— exit non-zero on warnings, not just errors.
nodetool sketch validate sketch.json
nodetool sketch validate <image_document_id> --json
nodetool sketch debug <sketch_id_or_file>
Run the same check, then execute each scripted step against the headless
ui_sketch_* bridge (the prefix is optional) and validate the document the
session left behind. Pixels, painting, rendering, generation, and asset I/O are
not simulated; the report lists that under notSimulated.
Options:
--interact <json>— the scripted steps, e.g.[{"tool":"add_layer","input":{"name":"Glow"}}].--out <dir>— bundle output directory (defaultnodetool-debug/sketch-<id>-<timestamp>).--json— print the fullSketchDebugReportas JSON to stdout.
nodetool sketch debug sketch.json \
--interact '[{"tool":"add_layer","input":{"name":"Shadow"}},
{"tool":"set_layer_props","input":{"target":"Shadow","opacity":0.4,"blendMode":"multiply"}}]'
✓ ui_sketch_add_layer
✓ ui_sketch_set_layer_props
✅ Sketch is sound — 2 interaction(s) ran clean.
sketch: 2 layer(s), 0 binding(s), 1024x768
session: 2 step(s), 0 failed
The bundle holds report.json, report.md, and sketch.json.
nodetool sketch 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. The target of every
subcommand is a script JSON file (a bare JsScriptDocument or anything carrying
one under document) or a js_scripts row id.
Subcommands: validate, run, test, debug, versions
nodetool jsscript validate <script_id_or_file>
Check the body’s syntax, its imports against the installed pack catalog,
undefined names, undeclared inputs.* reads, outputs no emit/output call
reaches, duplicate or non-identifier port names, and tests naming ports the
script does not declare. A body that declares outputs and returns them instead
of emitting them is an error — a script has no legacy return contract. Zero
saved tests, and a declared secret this install lacks, are warnings.
Options:
--json— print the fullJsScriptValidationas JSON.--warnings-as-errors— exit non-zero on warnings, not just errors.
nodetool jsscript validate script.json
nodetool jsscript validate <js_script_id> --warnings-as-errors
nodetool jsscript run <script_id_or_file>
Execute the body once in the QuickJS sandbox and print its outputs, streamed
emits, logs, and error. 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>
Run the document’s own saved test cases and grade each one. Exits non-zero when any case fails, which makes it the regression check after an edit.
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. The
editor, persistence of a debug session, and secret values are not simulated.
Options:
--interact <json>— the scripted steps, e.g.[{"tool":"set_code","input":{"code":"..."}}].--out <dir>— bundle output directory (defaultnodetool-debug/jsscript-<id>-<timestamp>).--json— print the fullJsScriptDebugReportas JSON to stdout.
nodetool jsscript debug script.json \
--interact '[{"tool":"set_code","input":{"code":"await output(\"n\", 1);"}}]'
nodetool jsscript versions
The same five subcommands as timeline versions, against a script’s history.
nodetool jsscript versions list <js_script_id> --limit 10
nodetool jsscript versions create <js_script_id> --name "before the rewrite"
nodetool jsscript versions restore <js_script_id> 3
Job Management
nodetool jobs
Query job status and results. Reads the local database by default.
Subcommands: list, get
Options:
--api-url <url>— query a remote server instead of the local database.--workflow-id <id>— filter by workflow ID (forlist).--limit <n>— max results (default:100).--json— output as JSON.
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>
Asset Management
nodetool assets
Manage uploaded files and workflow assets. Reads the local database by default.
Subcommands: list, get
Options:
--api-url <url>— query a remote server instead of the local database.--query <q>— search query (forlist).--content-type <type>— filter by content type (forlist).--limit <n>— max results (default:100).--json— output as JSON.
Examples:
# List assets
nodetool assets list
# Search assets
nodetool assets list --query "landscape"
# Get a specific asset
nodetool assets get <asset_id>
Storage Maintenance
nodetool storage migrate-keys
Move asset objects under their owner’s prefix, so an object’s key is
<userId>/<assetId>.<ext> and the owner is the leading path segment — the
boundary a Supabase RLS policy or an S3 bucket policy can enforce on the object
itself.
Run it once when upgrading an S3 or Supabase install that still holds objects written under the older flat layout. The local file backend needs no migration: it falls back to the flat key on a miss.
Options:
--dry-run— report what would move without writing anything.--user-id <id>— migrate only this user’s objects.--json— output the report as JSON.
Examples:
# See what would move first
nodetool storage migrate-keys --dry-run
# Move them
nodetool storage migrate-keys
# One user, machine-readable
nodetool storage migrate-keys --user-id 1 --json
A dry run reports the tally and writes nothing:
scanned 0, would move 0, already migrated 0, absent 0, failed 0
Dry run — nothing was written. Re-run without --dry-run.
Vector Collections
nodetool collections
Manage the RAG vector-store collections behind semantic search. Runs in-process
against the default vector provider — sqlite-vec unless
NODETOOL_VECTOR_PROVIDER points elsewhere — so no server has to be running.
Subcommands: list, get, create, delete, query, index
Options:
--embedding-model <model>/--embedding-provider <provider>— record the embedding model and provider on a new collection (forcreate).-n, --n-results <n>— number of matches to return (forquery, default10).-y, --yes— skip the confirmation prompt (fordelete).--json— output as JSON. Available on every subcommand.
Examples:
# Create a collection and record what embeds it
nodetool collections create my_docs --embedding-model text-embedding-3-small
# Chunk and index files, with the same splitter the server uses
nodetool collections index my_docs notes.md report.txt
# Semantic search
nodetool collections query my_docs "how does the runner cancel a job" -n 5
# Inspect and remove
nodetool collections list
nodetool collections get my_docs
nodetool collections delete my_docs --yes
Re-indexing the same file replaces its chunks rather than adding a second copy,
so an index run is safe to repeat after a document changes.
Spend Tracking
nodetool costs
Report what NodeTool has spent on LLM and provider calls. Every call writes a row carrying its cost and token counts; these subcommands aggregate them straight from the local database, so no server has to be running.
Reach for it to answer “what did that run cost” after the fact, or to find which model is eating a budget.
Subcommands: summary, list, by-provider, by-model
Options:
--provider <name>— filter by provider (forlistandby-model).--model <id>— filter by model (forlist).--limit <n>— max results (forlist, default50).--json— output as JSON. Available on every subcommand.
Examples:
# Overall spend plus per-provider and per-model breakdowns
nodetool costs summary
# Recent calls, most recent first
nodetool costs list --limit 20
nodetool costs list --provider anthropic
# Grouped totals
nodetool costs by-provider
nodetool costs by-model --provider openai
summary prints the overall total and both breakdowns:
Overall
key │ value
──────────────┼─────────
total_cost │ $0.0000
total_tokens │ 0
calls │ 0
By provider
(no results)
By model
(no results)
Supervised runs and nodetool app build write to the same ledger, tagged
supervisor and app-build in node_type, so costs list shows what the
harnesses spent alongside the workflow’s own calls.
Secrets Management
nodetool secrets
Manage encrypted secrets stored in the local database with per-user encryption.
Subcommands: list, store, get
Examples:
# List stored secret keys
nodetool secrets list
# Store a secret (prompts for value)
nodetool secrets store OPENAI_API_KEY
# Retrieve a secret value
nodetool secrets get OPENAI_API_KEY
Provider Sign-In
nodetool auth
Sign in to providers that use an account instead of an API key. Today that is
Claude: auth claude runs the same OAuth flow the claude CLI does and writes
the tokens to the Claude Agent SDK’s credential file
(~/.claude/.credentials.json), so a NodeTool login and a claude login are
interchangeable. The Claude Agent provider picks the file up with no further
configuration. No database and no server are involved.
Reach for it when you want to run agents on a Claude subscription rather than
store an ANTHROPIC_API_KEY.
Subcommands: claude login, claude status, claude refresh, claude logout
Options:
--console— sign in with a Console (API-billed) account instead of a subscription (forlogin).--manual— skip the loopback listener and paste the code the browser shows. This is the flow for a headless or remote machine (forlogin).--no-browser— print the URL instead of opening a browser (forlogin).--force— refresh even when the current token is still valid (forrefresh).--json— output as JSON. Available onlogin,status, andrefresh.
Examples:
# Browser opens, loopback callback completes the flow
nodetool auth claude login
# Headless box: paste the code yourself
nodetool auth claude login --manual
# Check and refresh
nodetool auth claude status
nodetool auth claude refresh --force
nodetool auth claude logout
The same flow is available over HTTP at
/api/oauth/claude/{start,complete,tokens,disconnect} and as a sign-in card on
the Models & Providers settings page.
Settings
nodetool settings show
Display current settings from environment variables.
Options:
--json— output as JSON.
Example:
nodetool settings show
nodetool settings show --json
Model Management
nodetool models
List models and providers. Queries local providers and caches by default; --api-url targets a remote server.
Subcommands:
list— list all models (recommended + provider + HuggingFace cached).providers— list configured providers and their capabilities.recommended— list recommended models.ollama— list Ollama models.huggingface— list HuggingFace cached models (--query,--typeto filter).by-provider <provider>— list models for a provider;--kindone ofllm,image,tts,asr,video,embedding(defaultllm).
Examples:
nodetool models list
nodetool models providers
nodetool models ollama
nodetool models by-provider openai --kind image
nodetool models recommended
The curated shortlist — what to reach for when you want a model id that is known
to work, rather than everything a provider will sell you. It reads the shipped
RECOMMENDED_MODELS table in-process, so it needs no server, no API key, and no
network.
--category <category>— one ofall(default),image,image-text-to-image,image-image-to-image,language,language-text-generation,language-embedding,asr,tts,video-text-to-video,video-image-to-video. An unknown value exits1listing the valid ones.--system <darwin|linux|windows>— keep only models that run on that platform. A model that declares no platforms is kept for all of them.--limit <n>— cap the results. Applied after both filters; a value that is not a positive integer exits1.--check-servers— fetch the list from a running server instead, which also probes whether the local Ollama and llama.cpp servers can serve each entry. This is the only mode that uses--api-url(defaulthttp://localhost:7777, orNODETOOL_API_URL).--json— the full model records rather than the five-column table.
# The top of the curated list, no network
nodetool models recommended --limit 3
# What to embed with
nodetool models recommended --category language-embedding
# What a Mac can actually run, machine-readable
nodetool models recommended --system darwin --json
# Which of them a local Ollama has pulled
nodetool models recommended --check-servers
nodetool models — HuggingFace Hub and cache
Five subcommands search the Hub and manage the local HuggingFace cache. They talk to the Hub’s HTTP API and the cache on disk, so they need no running NodeTool server. Reach for them when you want to find a repo id for a local model node, or pull the weights before a workflow needs them.
hf-types— print the nodetool HF model types, one per line, followed by aGeneric types (require --task):block. The types in that block need a pipeline tag when you search them; the ones above it do not.--jsonprints{types, generic}instead.list-hf <model_type>— search the Hub for models matching a nodetool model type.--task <task>supplies the HF pipeline tag (required for the generic types);--limit <n>caps the results;--jsonprints the raw entries. Without--task, a generic type fails withModel type 'hf.model' requires --task (e.g. 'text-to-image').list-hf-all— search across every nodetool HF model type at once.--limit <n>caps the total;--repo-onlydrops file-level entries and keeps one row per repo.hf-cache— list the local cache with disk detail: repo id, path, type, whether the repo is fully downloaded, size on disk, pipeline tag.--downloaded-onlykeeps only complete repos;--limit <n>and--jsonbehave as elsewhere.huggingface(above) reads the same cache but prints id/name/provider/type/repo_id and can search it (--query,--type) or target a remote server (--api-url); usehf-cachewhen you care about what is on disk and how big it is.download-hf --repo-id <owner/name>— download a repo into the local HuggingFace cache, printing progress to stderr.--file-path <path>fetches a single file instead of the whole repo;-a, --allow-patterns <glob>and-i, --ignore-patterns <glob>are repeatable globs that narrow the file set. Passing--cache-dir <dir>switches the download to the flat llama.cpp cache layout (~/.cache/llama.cppon Linux,~/Library/Caches/llama.cppon macOS) — it does not redirect the files into<dir>.
Examples:
# Which model types can be searched, and which need --task
nodetool models hf-types
# Search one type, then every type at once
nodetool models list-hf qwen3 --limit 5
nodetool models list-hf hf.text_to_image --task text-to-image --limit 5
nodetool models list-hf-all --repo-only --limit 20
# What is already on disk
nodetool models hf-cache --downloaded-only
# Pull weights ahead of a run — whole repo, then just the GGUF files
nodetool models download-hf --repo-id Qwen/Qwen3-0.6B
nodetool models download-hf --repo-id Qwen/Qwen3-0.6B-GGUF -a "*.gguf"
Media Generation
nodetool generate <provider> <model> <prompt...>
Generate an image from any registered provider straight to a file, with no workflow in between. Use it to try a model, check that a provider’s key resolves, or produce one image from a script.
Provider and model names are matched leniently against the registry and the
provider’s own model manifest, so fal-ai finds fal_ai and flux-schnell
finds fal-ai/flux/schnell. The API key comes from the secret store or the
environment; when it is missing the error names the variable to set.
Options:
-o, --output <path>— output file or directory. A directory (or a path ending in a separator) keeps the generated name; a file path is used as the base name. Default is anodetool-<model>-<timestamp>.<ext>file in the working directory.--width <n>/--height <n>/--aspect-ratio <ratio>— output size, e.g.--aspect-ratio 16:9.--negative-prompt <text>— what to avoid in the image.--seed <n>— random seed, for reproducible output.-n, --num-images <n>— number of images (default1). Extra images get a-2,-3, … suffix.--steps <n>/--guidance <n>— inference steps and guidance scale.--image <path...>— input image(s); switches to image-to-image.--strength <n>— image-to-image strength, with--image.--list-models— list the provider’s image models and exit.--json— print the result as JSON.
Examples:
# Discover model ids for a provider
nodetool generate 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
Install, remove, or inspect the NodeTool MCP server configuration for AI coding assistants (Claude Code, Codex, OpenCode).
Subcommands: install, uninstall, status, serve
Examples:
# Install for all detected assistants (default URL http://127.0.0.1:7777/mcp)
nodetool mcp install
# Install for Claude Code only, with a custom URL
nodetool mcp install --claude --url http://127.0.0.1:7777/mcp
# Show installation status
nodetool mcp status
# Remove from all assistants
nodetool mcp uninstall
HTTP MCP (/mcp) needs the API server (nodetool serve). nodetool mcp serve
is stdio and does not.
What the MCP server exposes
Exactly two tools land on /mcp and on nodetool mcp serve:
execute_code— the CodeAct action tool, built by the samecreateChatCodeActSessionthe in-app chat agent runs on, so the two surfaces cannot drift. Everything NodeTool can do is reached from inside an action: workflow building and debugging (create_workflow,validate_workflow,debug_workflow,debug_app, theui_*graph editing tools), media generation (generate_image,generate_video,generate_speech,transcribe_audio, …), files (read_file,write_file,edit_file,glob,grep), web (web_search,browser,http_request), collections, documents, code execution, image critique, and memory — asnodetool.<namespace>.<method>(), by importing them, or found withawait nodetool.searchTools("query"). Google Workspace tools appear only on deployments with a Google login.view_image— direct, because image content cannot ride a sandbox action’s JSON observation envelope.
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.
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 prints on stderr and waits for y (this call), n (refuse), or
a (this tool for the rest of the session). Stdout carries the run’s result, so
nothing about the prompt goes there.
Behind a pipe, nobody is there to answer, so the run takes the headless gate:
auto, printing once up front that escalated calls are denied. An explicit mode
still applies — a piped --permission-mode plan blocks what plan mode blocks —
but the answer to anything the ladder escalates is deny, never a silent allow.
See the Agent CLI reference for full details.
The
nodetool dbgroup (migrate,status,baseline,rollback) is documented under Database Migrations above.
Deployment and Workers
Two different things, and the CLI keeps them apart. A server is long-lived
and people connect in to it — nodetool deploy manages one over Docker. A
worker is a rented GPU that a NodeTool instance connects out to, bills by
the minute, and is meant to be torn down — nodetool worker provisions it. See
Deployment for which one you want.
nodetool deploy
Manage a Docker self-host server target described by a deployment.yaml.
init scaffolds the file, add fills in one target, and every other
subcommand names that target.
The file lives in the user config directory, not the working directory:
~/Library/Application Support/nodetool/deployment.yaml on macOS,
%APPDATA%\nodetool\deployment.yaml on Windows, and
$XDG_CONFIG_HOME/nodetool/deployment.yaml — falling back to
~/.config/nodetool/ — elsewhere. Run nodetool deploy edit to open it rather
than guessing the path.
Subcommands: init, add, edit, list, show, plan, apply,
status, logs, destroy, plus the remote groups workflows, database,
collections and the users-* verbs below.
Options:
--dry-run— print whatapplywould do without executing it.-f, --follow/--tail <n>/--service <service>— forlogs;--taildefaults to100.--force— skip thedestroyconfirmation.--json— machine-readablelistoutput.
# 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, rows, and collections
Once a target is up, three groups act on it over the admin API rather than on the local database:
# Push a local workflow and everything it references, 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>
# Read, upsert, and delete a single remote DB row
nodetool deploy database get my-server users alice
nodetool deploy database save my-server users '{"id":"alice","role":"admin"}'
nodetool deploy database delete my-server users alice
# Push a local RAG collection to the deployment
nodetool deploy collections sync my-server my_docs
workflows run takes -p, --param <k=v>, repeatable. <table> is passed
through to the deployment, which resolves it against its own adapters — the
valid names are the remote server’s tables, not a list this CLI holds. save
takes the row as a positional JSON string, and get on a row that is not there
answers 404. collections sync uploads in batches of --batch-size,
default 100.
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
three remote groups above — sends an admin bearer token. --token <token>
passes it explicitly and wins over NODETOOL_ADMIN_TOKEN; with neither, an
interactive shell prompts and a non-interactive one exits 1.
nodetool worker
Provision, attach to, and tear down a rented GPU worker on RunPod or Vast. A
worker bills by the minute, so stop is part of the workflow, not cleanup you
get to postpone.
Subcommands: profile (add, list, rm), create, list, status,
token, stop, models
Options:
--target <runpod|vast>,--image <image>,--gpu <gpu>,--vcpu <n>— what to rent, onprofile addor inline oncreate.--idle-timeout <minutes>/--max-lifetime <minutes>— auto-stop when idle, and a hard TTL. Both are the guard against a forgotten worker.--token-policy <generate|fixed>— defaultgenerate.--profile <name>/--attach— forcreate.--all— forstop, stops every non-stopped worker.--json— forlist.
# Save a preset once, then rent from it
nodetool worker profile add hf-a40 --target runpod \
--image ghcr.io/nodetool-ai/nodetool-worker:latest \
--gpu "NVIDIA A40" --idle-timeout 15
nodetool worker create --profile hf-a40 --attach
# Inspect and tear down
nodetool worker list
nodetool worker status <instance-id>
nodetool worker stop --all
token <id> prints one worker’s bearer token and nothing else — no table, no
label — so it pipes into the variable the bridge reads:
export NODETOOL_WORKER_TOKEN=$(nodetool worker token <instance-id>)
list withholds tokens on purpose; this is the one command that decrypts one,
and only for the worker you name. Reach for it when a second shell, a CI step,
or a --api-url client has to talk to a worker --attach did not configure. A
worker that carries no token is an error rather than empty output:
Worker instance '<id>' has no token (open worker).
The full walkthrough, including what --attach changes locally, is
Worker Deployment.
nodetool worker models
Manage the HuggingFace cache on the worker itself, over the WebSocket bridge — no NodeTool server has to be running. Pre-pulling weights here is what keeps the first run of a graph from paying the download on rented GPU time.
The [worker-id] argument is optional: omit it and the command uses the
currently attached worker, or errors telling you to attach one.
Options:
--repo-id <id>— HuggingFace repo,owner/name. Required fordownloadanddelete.--file-path <path>— download a single file instead of the repo.-a, --allow-patterns <pattern>/-i, --ignore-patterns <pattern>— glob filters ondownload, both repeatable.--json— forlist.
# What is already cached on the attached worker
nodetool worker models list
# Pull weights ahead of a run — whole repo, then just the safetensors
nodetool worker models download --repo-id stabilityai/sdxl-turbo
nodetool worker models download --repo-id stabilityai/sdxl-turbo -a "*.safetensors"
# Reclaim disk on a named worker
nodetool worker models delete <worker-id> --repo-id stabilityai/sdxl-turbo
Messaging Bridges
nodetool telegram
Run the Telegram bridge: it turns private-chat messages into turns on the agent
loop of a running NodeTool server, and streams the answers back. The bridge
holds no conversation state and no user credentials — the agent loop, tools,
permissions, threads, and cost tracking all stay on the server, which the
bridge reaches over /ws as the linked user.
Reach for it when you want NodeTool answering from a phone without opening the
web UI. The server it talks to must have NODETOOL_INTEGRATION_TOKEN set, or
the linking routes the bridge calls do not exist.
Subcommands: serve, register-commands
Options:
--config <path>— path totelegram-bot.json, relative to the working directory. Both subcommands take it. Without the flag,telegram-bot.jsonis read if it is there and skipped if it is not; a path you asked for explicitly and that does not exist is an error.
Configuration is environment variables plus that optional file. The env vars are listed in the Environment Variables Index; the file carries only the tuning knobs:
// telegram-bot.json
{
"allowUsers": [], // Telegram user ids allowed to link; empty = anyone
"editThrottleMs": 1500, // minimum gap between edits of one streamed reply
"maxQueuedTurns": 3 // messages queued behind an in-flight turn
}
# Publish the bot's command list — a deploy step, not a boot step
export TELEGRAM_BOT_TOKEN=123456:AA...
export NODETOOL_INTEGRATION_TOKEN=the-same-value-the-server-has
nodetool telegram register-commands
# Long-poll the Bot API and serve turns
nodetool telegram serve --config ./telegram-bot.json
Both subcommands validate the whole configuration before doing anything and print the fields that failed rather than a stack trace:
Invalid Telegram bridge environment:
TELEGRAM_BOT_TOKEN: TELEGRAM_BOT_TOKEN is required
NODETOOL_INTEGRATION_TOKEN: NODETOOL_INTEGRATION_TOKEN is required
serve polls with getUpdates. Webhook mode is not implemented yet, and
setting TELEGRAM_WEBHOOK_URL makes serve refuse to start rather than
silently polling. Design notes: telegram-bot-design.md.
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. Takes --json to print the registry.
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...]
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.
nodetool harness gate --base main
nodetool harness gate --dry-run packages/base-nodes/src/text.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
Manage TypeScript node packages — the packs that contribute node types to
the registry. Not to be confused with nodetool packs, which handles the
sandbox packs a Code node imports; the names are one letter apart and the
subjects are unrelated.
Reach for it when you are authoring a node pack: init scaffolds one, list
tells you what this install has, and the three *-docs commands generate the
Markdown that ships with a pack.
Subcommands: list, init, docs, node-docs, workflow-docs
nodetool package list
List the installed packages with their node counts, or the registry’s catalog
with --available.
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 reads the index at NODETOOL_PACKAGE_REGISTRY_URL. 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 the current package: docs writes one overview,
node-docs writes a page per node, and workflow-docs documents the workflow
JSONs a pack ships. Each takes -v, --verbose.
Options:
docs—-o, --output-dir <dir>(defaultdocs),-c, --compactfor a shorter overview.node-docs—-o, --output-dir <dir>(defaultdocs/nodes),-p, --package-name <name>to emit only nodes under a namespace prefix.workflow-docs—-o, --output-dir <dir>(defaultdocs/workflows),-e, --examples-dir <dir>to point at the workflow JSONs, and-p, --package-name <name>to keep only those whosepackage_namematches.
nodetool package docs --compact
nodetool package node-docs --package-name nodetool.text
nodetool packs compile
Compile every npm-backed sandbox module of every installed pack. A pack can
declare a guest module by npm dependency name instead of authoring code
({"name": ".", "kind": "js", "npm": "js-yaml"}); this builds it — esbuild
bundles the dependency with no externals, a scope-aware scan rejects free
references to globals the guest lacks, and a capability-free QuickJS probe
imports the bundle to prove it initializes. Results are cached by content
digest, never by version.
The server compiles during its own catalog refresh and Electron compiles after an install, so reach for this to warm the cache or to diagnose one pack.
Options:
--json— print the compile report as JSON.--force— recompile and re-probe even when the cache has an answer.--pack-search-path <dir>— search thisnode_modulesroot (repeatable).
nodetool packs compile
nodetool packs compile --json
nodetool packs compile --force
Each module reports its source dependency and bundled size:
ok @nodetool-ai/sandbox-markdown ← marked — 66101 bytes
ok @nodetool-ai/sandbox-yaml ← js-yaml — 103863 bytes
10 compiled, 0 skipped.
Everything that stops a module short of admission is a named skip, not an
error: npm-module-builtin-import (the dependency needs node:*),
npm-module-unresolved, npm-module-too-large (1 MB cap),
npm-module-forbidden-global, and npm-module-probe-failed.
nodetool reliability
Run a reliability journey on more than one execution surface and diff each one against the kernel oracle. A journey is a small workflow plus the invariants its run must hold — lifecycle pairing, one terminal message, no leaked cleanup — and the point is that the kernel runner and the WebSocket server must produce the same stream for it. Reach for it when a change touches execution and you need to know whether the surfaces still agree.
Journeys live in reliability/journeys/.
Working in the monorepo, run this one from the built CLI (npm run nodetool --)
rather than from source: the journey fixtures use decorators, and the
dev:nodetool transform rejects them with Decorators are not valid here.
Subcommands: list, run, update-goldens
nodetool reliability list
List the journeys, each with its surfaces and the invariants it asserts. Takes
--json for the full summaries.
nodetool reliability list
nodetool reliability list --json
linear-text-pipeline
Journey #1 (docs/RELIABILITY_ARCHITECTURE.md §5): input -> transform -> output.
The baseline — if this diverges across surfaces, everything is suspect.
surfaces: kernel, ws-server
invariants: lifecycle-pairing, terminal-uniqueness
nodetool reliability run <journey>
Run one journey on the surfaces it declares and diff every non-oracle surface against the kernel oracle.
Options:
--surface <name>— run only this surface, repeatable, overriding the journey’s own list. The kernel oracle is always included.--faults <name>— inject this fault, repeatable, replacing the journey’s declared fault matrix. The provider-seam faults are implemented (provider-429,provider-500,provider-timeout,truncated-stream,malformed-sse,slow-drip,cost-omission); thews/bridge/host/clientnames parse but report as unimplemented.--diff— print the full per-channel stream diff for a diverging surface.--json— print the wholeCompareReportinstead of the summary.
nodetool reliability run linear-text-pipeline
nodetool reliability run linear-text-pipeline --surface kernel
nodetool reliability run mid-run-cancel-node --faults provider-429 --diff
nodetool reliability update-goldens <journey>
Rewrite a journey’s expected/ fixtures from a fresh unfaulted kernel run. For
a golden that legitimately moved — read the diff before committing it, since
this command cannot tell a fixed bug from a new one.
nodetool reliability update-goldens linear-text-pipeline
nodetool eval <suite>
Run one of the agent evaluation suites against a provider and model, and report its metrics — success rate, expectation score, tool calls, duration, and cost.
Suites: graph-planner, graph-e2e, code-gen, task-planner, subtask,
codeact, app-build, tool-loop, workflow-escalation, script-tools,
jsscript-tools, sketch-tools, timeline-tools, storyboard-tools,
model3d-tools, app-tools, memory-tools, creative-pipeline.
Options (the same on every suite):
-p, --provider <id>— provider id (anthropic,openai,claude_agent_sdk,ollama, …).-m, --model <id>— model id for that provider.--cases <ids>— comma-separated case ids to run (default: all).--list— list the suite’s cases and exit.--json— print the full report as JSON.--out <path>— write the JSON report to a file.--max-iterations <n>— turn cap per case for the loop-style suites (default 12).--timeout <ms>— per-case execution timeout for the suites that run what they plan (default 300000).--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).--no-find-model— run without configured model providers, skipping the model-dependent cases.
Examples:
# See what a suite covers before spending on it — needs no provider
nodetool eval graph-planner --list
nodetool eval graph-planner -p anthropic -m claude-sonnet-5
nodetool eval timeline-tools -p openai -m gpt-5.4-mini --cases cut-and-trim,titles-with-motion
nodetool eval graph-e2e -p anthropic -m claude-sonnet-5 --timeout 600000
nodetool eval codeact -p anthropic -m claude-sonnet-5 --json --out report.json
# Gate a run in CI
nodetool eval sketch-tools -p ollama -m qwen-3.5:4b --min-success 0.8
--list prints each case id with what it checks:
summarize Single LLM step with one wired input and one output
branch-both-paths Conditional with both If branches wired
deterministic-over-llm Pure string mechanics must not be solved with an LLM step
nodetool jtbd
An eval suite scores a model. A job asks whether the product let the agent finish something, and keeps the transcript so you can say what to change.
A job is one objective taken end to end across whatever surfaces it needs,
stated the way a user would state it, handed to the agent in the user’s own
words, and graded on the world it left behind. No job names a tool — which tool
the agent reaches for, and in what order, is what is under test. The worlds are
the same headless bridges the tool-loop eval suites drive.
Subcommands: list, run, optimize.
# What the jobs are, before spending anything — needs no provider
nodetool jtbd list
nodetool jtbd list --json
Each entry prints the job id, its difficulty tier, the surfaces it crosses, the statement, and the outcomes it grades:
storyboard-a-scene [standard] storyboard
When I have a scene to shoot, I want it broken into shots with the action
described, so I can see the coverage before I spend on renders.
outcomes: five-shots, action-written, savable
nodetool jtbd run
Drives a model through the jobs and writes one bundle per job.
-p, --provider <id>— provider id (anthropic,openai,ollama, …). Required.-m, --model <id>— model id for that provider. Required.--jobs <ids>— comma-separated job ids (default: all; seelist).--out <dir>— bundle directory (defaultnodetool-debug/jtbd).--max-iterations <n>— turn cap per job, for jobs that declare none.--min-achieved <rate>— exit non-zero when the achievement rate falls below this threshold (0..1).--no-find-model— run without configured model providers.--json— print the full report as JSON.
nodetool jtbd run -p anthropic -m claude-sonnet-5
nodetool jtbd run --jobs workflow-from-prompt,timeline-assemble-cut -p openai -m gpt-5.4-mini
# Gate a run in CI
nodetool jtbd run -p anthropic -m claude-sonnet-5 --min-achieved 0.8
run writes nodetool-debug/jtbd/<job>/ holding report.json (transcript,
tool calls, outcomes, friction) and review.md, the same run rendered for a
person to read. The transcript is the point: it records what the model was told
and what it said between calls, not only which tools fired.
Before any model reviews a run, a pure pass derives what the transcript decides
on its own, and each finding names an owner. A tool that errored repeatedly, or
answered the same call identically three times, is a harness finding — the
fix is a schema or an error string in NodeTool’s own code. A run that called no
tool at all is a prompt finding. A run that took far more calls than the job
needs is unattributed, because whether that is a prompt failing to describe
the short path or a tool surface forcing the long one is the judgement a pure
pass cannot make.
nodetool jtbd optimize
Hands one recorded run — the system prompt verbatim, every assistant turn, every call with its arguments and result — to a different model and asks what one change would make the next run go better.
-p, --provider <id>— reviewing provider; use a different one than ran the jobs.-m, --model <id>— reviewing model id.--bundle <dir>— bundle directory to review (defaultnodetool-debug/jtbd).--all— review clean runs too, not just failures and friction.--json— print the proposals as JSON.
nodetool jtbd optimize -p openai -m gpt-5.4-mini
nodetool jtbd optimize -p openai -m gpt-5.4-mini --all --bundle ./my-runs
A proposal must name a target and a change; “improve the prompt” is rejected by
the parser. Proposals land in proposals.json next to the run.
It proposes; it never applies. Nothing in the loop edits a prompt or a tool. Run and review are separate commands because the bundle is the handoff: a bundle can be re-reviewed with a better optimizer without paying for the runs again, and a person can read the transcript before any model proposes anything.
Tips
- Use
--jsonflags for machine-readable output suitable for scripting. - Set
NODETOOL_API_URLenvironment variable to avoid specifying--api-urlon every command. - Use
nodetool serveto start the local backend server before running API commands. - See Environment Variables for a complete list of configurable variables.