nanoodle
v0.6.1
Published
Run nanoodle visual AI workflows from JavaScript — zero-dependency executor for saved noodle-graph.json files
Maintainers
Readme
nanoodle (JavaScript)
Run visual AI workflows from Node.js. Design them in the
nanoodle editor, save as noodle-graph.json, then load
and re-run them here — same graph, same NanoGPT API,
your own key.
Zero runtime dependencies. Library + CLI in one install.
Looking for Python? → nanoodle-py
Which repo do I want?
| I want to… | Go to | |---|---| | Design workflows in the browser | nanoodle.com | | Run graphs from Node code or the CLI | this package | | Run graphs from Python | nanoodle-py | | Serve saved graphs as MCP tools | nanoodle-mcp | | Run graphs in GitHub Actions CI | run-noodle-action | | Teach an agent to build graphs | nanoodle-skill | | Prebuilt one-task agent skills | noodle-skills | | Browse ready-to-open graphs | awesome-noodles |
At a glance

Build once, run anywhere. The browser app is for designing and testing. This package is for automating the same workflows in scripts, servers, and agents.

| | |
|---|---|
| Package | nanoodle on npm |
| Runtime | Node ≥ 20 · built-in fetch · no deps |
| Sibling | Python package (same graphs, same semantics) |
| Editor | nanoodle.com — wire nodes, hit 💾, download the graph |
Install
npm install nanoodle # library + CLI
npx nanoodle --help # or run the CLI without installingQuickstart (library)
import { Workflow } from "nanoodle";
const wf = await Workflow.load("noodle-graph.json"); // key from NANOGPT_API_KEY
// …or load any nanoodle share link — the URL is the package:
// const wf = await Workflow.load("https://nanoodle.com/#g=…");
const result = await wf.run({ Text: "a cozy ramen shop on a rainy night" });
await result.get("Image").save("ramen.png"); // media: MediaRef (url + bytes()/save())
console.log(result.costUsd, result.remainingBalance);With the app’s starter graph (text → LLM prompt-writer → image), that’s the whole program.
Quickstart (CLI)
No graph yet? Scaffold the starter (text → LLM prompt-writer → image) and inspect it — both offline, no key needed:
npx nanoodle init # writes ./noodle-graph.json
npx nanoodle inspect noodle-graph.json # shows inputs, outputs, settingsThen run (calls NanoGPT and spends from your balance):
export NANOGPT_API_KEY=... # or --key K, or --env-file .env
npx nanoodle run noodle-graph.json --input Text="a cozy ramen shop on a rainy night"
npx nanoodle run graph.json --input [email protected] --set n3.size=1k --out ./rendersMedia outputs are saved under --out (default ./noodle-out, created only
when there is media to save); a JSON run summary always goes to stdout,
progress lines to stderr; exit code 0 on success, 1 on failure.
--input k=@path— read a file (media as media;.txt/.md/.jsonas text)--set k=v— override a setting (n3.model=flux-pro)--out dir— where media outputs land (default./noodle-out)--timeout ms— overall run timeout--json— quiet mode: skip the stderr progress lines (the JSON summary is printed either way)--env-file path— loadNANOGPT_API_KEYfrom a.env-style file (--keywins if both are set)
The URL is the package
Every nanoodle share link is a runnable artifact — paste one straight from a
README, a chat, or a tweet, wherever a graph.json path is accepted:
npx nanoodle inspect "https://nanoodle.com/#g=..." # what does it need? (offline)
npx nanoodle run "https://nanoodle.com/play.html#a=..." --input Text=hi # run itWorkflow links (#g=/#j=) and app links (#a=, graph only — the app shell
stays in the browser) both work, as do da.gd/TinyURL short links (resolved by
reading redirect headers; no credentials are ever sent). Direct links decode
fully offline. Quote the URL — # starts a comment in most shells.
Links mangled in transit (a character flipped or dropped by a chat app or a copy/paste) are recovered best-effort: the graph's nodes and wires are salvaged and a warning is printed. Only damage inside the graph itself makes a link unrecoverable.
Inputs, outputs, settings
wf.inputs // [{ key: "Text", nodeId: "n1", field: "text", kind: "textarea", optional: false, def: "..." }]
wf.outputs // [{ key: "Image", nodeId: "n3", type: "image", ports: [{ name: "image", type: "image" }] }]
wf.settings // [{ key: "n3.model", kind: "model", def: "nano-banana-2-lite" }, ...]Input keys are flexible (case-insensitive): the node’s custom name ("Text"),
nodeId.field ("n2.system"), a bare node id, or the input’s label. Output
keys are the sink node’s custom name (or its type name). A workflow with
exactly one required input also accepts a bare value: wf.run("hello").
Settings use nodeId.field keys ("n3.model").
An input is optional when its node-type spec says so (LLM system prompts),
or when the author checked optional on the node in the editor (saved as
fields.optional). Optional inputs can be omitted: the run proceeds and the
node yields an empty value (an omitted optional image/audio/video input is
simply dropped by downstream reference-style consumers).
Media inputs
import { mediaFromFile } from "nanoodle";
await wf.run({ Image: await mediaFromFile("photo.jpg") }); // local file
await wf.run({ Image: "https://example.com/photo.jpg" }); // hosted URL
await wf.run({ Image: bytesUint8Array }); // raw bytes (MIME sniffed)Media is sent inline as base64 (NanoGPT has no upload endpoint). Files over ~4.4 MB (~3.5 MB for transcription) are refused locally with a clear error before any paid call.
Progress and errors
const result = await wf.run(
{ Text: "sunset harbor" },
{
settings: { "n3.model": "flux-dev", "n3.size": "1024x1024" },
timeoutMs: 300000,
onProgress: (e) => console.error(e.type, e.name ?? "", e.status ?? ""),
},
);run() rejects with RunError when an output (sink) node fails —
err.result still has partial results, per-node statuses, and cost so far.
Failures in lanes no output depends on only appear in result.errors.
Unknown node types, missing required inputs, bad keys, and a missing API key
all fail before anything is spent.
Supported nodes
| runs | node types | |---|---| | local | text, upload (image/audio/video), choice, join, comment | | local media† | resize, vframes, combine, soundtrack, trim, extractaudio | | NanoGPT | llm (incl. vision + audio input), image, draw, edit, inpaint*, vision, tvideo, ivideo, vedit, lipsync, music, remix, tts, transcribe |
† local media prefers a pure-JS path that matches the browser (lossless mp4 remux, PCM-WAV trim, PNG resize). ffmpeg on PATH is the fallback for everything else (soft dependency — not an npm package); clear error if it’s required and missing.
* inpaint: the mask is composited onto black at the source image’s pixel size before send (same as the browser app) — white = repaint, black = keep. Brush-style white-on-transparent masks and plain B/W masks both work.
Use it as an agent skill
A saved workflow plus a short SKILL.md playbook is a skill any coding agent
can run — Claude Code, Cursor, Grok, or anything that reads markdown and runs
shell. Recipe and template: docs/agent-skills.md.
Example skill (idea → LLM prompt → poster image), installed with the
skills CLI:
npx skills add nanoodlecom/nanoodle-js@poster-generator -g -ySource: examples/agent-skill/poster-generator/.
Media is saved as Poster.<ext> (MIME-derived; often .jpg) — use the path the
CLI prints. The Python package ships the same skill name; installing both
overwrites — pick one runtime (see agent-skills.md).
API key and cost
Bring your own nano-gpt.com API key (or OAuth access
token) with balance. Every run() spends real money — NanoGPT bills per
generation.
result.costUsd— total of prices NanoGPT returnedresult.costExact—falseif any call omitted a price (total is then a floor)result.remainingBalance— freshest balance the API reported
A price of 0 means known-included (subscription), not unknown. inspect
and loading/validating workflows never call the API. No telemetry, no
analytics; the API key is never logged.
No account at all: pay per run in Nano (x402)
NanoGPT supports x402 accountless payments: call the API with no key, get an HTTP 402 invoice, settle it in Nano (XNO — instant, feeless), and the call completes. nanoodle wires that up end to end:
# each paid call prints a scannable QR + nano: URI on stderr and waits for the deposit
nanoodle run "https://nanoodle.com/#g=..." --input Text="hello" --payconst wf = await Workflow.load(url, {
payment: async (invoice) => {
// invoice: { payTo, amountRaw, amount, amountUsd, uri, expiresAt, explorerUrl, ... }
await myWallet.send(invoice.payTo, invoice.amountRaw); // YOUR signer does the send
},
});The library never touches funds or keys: payment must be a callback —
passing a seed or private key throws. Do the send inside the callback with
your own wallet/signer (e.g. a self-custody wallet, or print invoice.uri
for a human to scan — qrTerminal(invoice.uri) is exported). Each API call
pays at most once; graphs with several paid nodes produce one small invoice
per node. Chat and image nodes are live-verified; async video/audio job
polling under accountless mode is untested upstream.
Copy-paste scripts (CLI, QR callback, wallet stub, bare chat): examples/x402/.
Accountless image, start to finish
The starter graph (text → LLM prompt-writer → image), run with no NanoGPT account and no API key — scan the Nano invoice, and the image node settles for a few cents of XNO:
nanoodle run noodle-graph.json --input Text="a cozy ramen shop on a rainy night" --pay --out ./noodle-out
# → LLM node runs, image node prints a nano: QR, waits for the deposit,
# then writes noodle-out/Image.jpg
Specs and testing
Format and execution semantics live in docs/: DESIGN.md, SPEC-format.md, SPEC-engine.md, SPEC-io.md.
Same contract as the Python package.
Tests run fully offline against a mock NanoGPT server (tests/harness/):
npm testOpt-in live probe (spends a fraction of a cent):
node scripts/live-spot-check.mjs (add --image to also run the starter
graph’s image step).
License
MIT — see LICENSE. Not affiliated with NanoGPT. Build workflows at nanoodle.com.
