Run workflows headlessly
The URL is the package. Paste a share link from chat, a tweet, or Steal a noodle. Inspect it offline. Run it from Node or Python. Same noodle-graph.json, same NanoGPT API, your own key.
Under a minute
- Install
nanoodle— that is the package name on both registries. The GitHub repos arenanoodle-jsandnanoodle-py. Do notnpm install nanoodle-js. inspectthe share link. Offline. No key. Prints inputs, outputs, settings.runit. That call spends your NanoGPT balance.
nanoodle 0.8.0npx nanoodle inspect "https://nanoodle.com/#g=..."
export NANOGPT_API_KEY=...
npx nanoodle run "https://nanoodle.com/#g=..." --out ./out
nanoodle 0.5.0pip install nanoodle
python -m nanoodle inspect "https://nanoodle.com/#g=..."
export NANOGPT_API_KEY=...
python -m nanoodle run "https://nanoodle.com/#g=..." --out ./out
Quote the URL. # starts a comment in most shells. Direct
#g= / #j= / #a= links decode fully offline;
only fragment-less short links touch the network (redirect headers, no credentials).
App links (#a=) load the graph only — the app shell stays in the
browser.
Install nanoodle. Not nanoodle-js.
npm 0.8.0 and PyPI 0.5.0 are both named nanoodle. Zero runtime
dependencies (Node ≥ 20 with built-in fetch; Python ≥ 3.9, stdlib
only). Same graphs, same semantics. A saved
noodle-graph.json works anywhere a share URL does.
Start from a working example
Choose from the five example workflows, inspect the saved result, and copy its Open this noodle link. The same graphs work in the browser and in the awesome-noodles Run headless instructions. Inspect first, then run.
Sing a trip-hop track
The song workflow turns a theme into lyrics and uses your favorite bands to arrange them before singing. Override the idea below or keep the saved singularity/trip-hop brief.
Sing needs the latest executor from GitHub while the music-style fix awaits
an npm/PyPI release. Install it with npm install github:nanoodlecom/nanoodle-js
or python -m pip install --force-reinstall git+https://github.com/nanoodlecom/nanoodle-py.git,
then use the commands below. The browser example includes the fix.
npx nanoodle inspect "https://nanoodle.com/#g=..."
npx nanoodle run "https://nanoodle.com/#g=..." \
--input "Song idea=A wistful trip-hop song about a city after its last train." --out ./out
python -m nanoodle inspect "https://nanoodle.com/#g=..."
python -m nanoodle run "https://nanoodle.com/#g=..." \
--input "Song idea=A wistful trip-hop song about a city after its last train." --out ./out
Compare four image models
The image comparison workflow
sends the same bicycle-workshop poster brief to four models. The brief is already
in the graph, so --input is optional. Running it makes four paid
image calls. The guide labels the saved Krea result and the current Flare replacement.
npx nanoodle inspect "https://nanoodle.com/#g=..."
npx nanoodle run "https://nanoodle.com/#g=..." --out ./out
python -m nanoodle inspect "https://nanoodle.com/#g=..."
python -m nanoodle run "https://nanoodle.com/#g=..." --out ./out
Copy Open this noodle from those cards and paste it in place of
#g=.... Or use the copy-paste one-liners in
recipes/sing.md
and
recipes/image-model-arena.md
(or clone
awesome-noodles
and point the CLI at graphs/sing.noodle-graph.json /
graphs/image-model-arena.noodle-graph.json).
Library
Using the image comparison link, load and run the graph from either library.
Workflow.load accepts a share URL, a bare #g= /
#a= fragment, or a local noodle-graph.json. Key from
NANOGPT_API_KEY unless you pass one.
import { Workflow } from "nanoodle";
const wf = await Workflow.load("https://nanoodle.com/#g=...");
const result = await wf.run(); // graph fields as-is
await result.get("Contender 1").save("poster.png");
console.log(result.costUsd, result.remainingBalance);
from nanoodle import Workflow
wf = Workflow.load("https://nanoodle.com/#g=...")
result = wf.run() # graph fields as-is
result["Contender 1"].save("poster.png")
print(result.cost_usd, result.remaining_balance)
Override the image comparison's brief the same way in both languages:
import { Workflow } from "nanoodle";
const wf = await Workflow.load("https://nanoodle.com/#g=...");
const result = await wf.run({
"Shared test brief": "A square poster for a bicycle repair workshop. Heading: FIX A FLAT. One red bicycle wheel on cream.",
});
await result.get("Contender 1").save("poster.png");
from nanoodle import Workflow
wf = Workflow.load("https://nanoodle.com/#g=...")
result = wf.run({
"Shared test brief": "A square poster for a bicycle repair workshop. Heading: FIX A FLAT. One red bicycle wheel on cream.",
})
result["Contender 1"].save("poster.png")
CLI extras
Python also installs as nanoodle-py; python -m nanoodle
always works. Flags match across both CLIs:
npx nanoodle run graph.json --input "Shared test brief=One red bicycle wheel on a cream background." --set n3.size=1:1 --out ./out --json
npx nanoodle run graph.json --input "Shared test brief=@brief.txt" --env-file .env
--env-file path— loadNANOGPT_API_KEYfrom a.env-style file (--keywins if both are set)--input k=@path— read a file (media as media;.txt/.md/.jsonas text)--set k=v— override a setting (n3.model=…)--out dir— save media outputs to disk--json— machine-readable result
Inputs, outputs, settings
Both packages expose the workflow's interface before you run it —
wf.inputs, wf.outputs, wf.settings. Input keys are
flexible (case-insensitive): the node's custom name ("Still brief",
"Shared test brief"), nodeId.field ("n2.system"),
or the input's label. A workflow with exactly one required input also accepts a bare
value: wf.run("hello"). Settings use nodeId.field keys
("n3.model"). Named-node keys match across JS 0.8.0 and Python 0.5.0 —
run inspect if a published graph used an older Python label.
Media inputs take a local file (mediaFromFile /
media_from_file), a hosted URL, or 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.
Every run() spends real money from your NanoGPT
balance — NanoGPT bills per generation. Loading, validating and inspecting workflows
never calls the API, and bad inputs (unknown node types, missing required inputs,
missing API key) fail before anything is spent. No telemetry, no analytics;
the key is never logged. Damaged share links (a character flipped in transit) are
recovered best-effort and warned; only damage inside the graph itself is
unrecoverable.
What runs where
Pure-logic nodes (text, uploads, choice, join, comment) run locally; generation nodes
(llm, image, edit, inpaint, vision, video, lipsync, music, remix, tts, transcribe)
call NanoGPT. The 6 media-processing nodes (resize, vframes, combine, soundtrack, trim,
extractaudio) also run headlessly. They do their work on your machine and never spend.
Some of them need ffmpeg and ffprobe on PATH:
| node | JavaScript | Python |
|---|---|---|
resize | Pure JS for a PNG source; ffmpeg for any other format | ffmpeg |
trim | Pure JS for a PCM WAV source; ffmpeg for any other format | ffmpeg |
combine | Pure JS (lossless mp4 remux) when every clip is mp4 with matching stream parameters; ffmpeg otherwise | ffmpeg |
vframes | ffmpeg | ffmpeg |
soundtrack | ffmpeg | ffmpeg |
extractaudio | ffmpeg | ffmpeg |
ffmpeg is a soft dependency, not a package dependency. Install it with
apt install ffmpeg or brew install ffmpeg. If a node needs it
and it is absent, that node stops the run with a clear error that names it. The error
arrives when the node's turn comes, so paid nodes earlier in the graph have already
spent — run a media-only graph once to prove your setup before you wire a paid node in
front of it.
The ⚖️ decide node (NanoGPT decision models) runs headless too, with the
same requests and costs as the editor. Text-only decisions need nothing extra. Wired images
are shrunk with ffmpeg. A yes/no gate that answers no is a result, not a failure:
that node reports gated and everything behind it reports skipped
(never run, never billed). The run succeeds and the CLI exits 0 with a gated
list in its JSON summary. Support landed on the libraries' main branches after
nanoodle 0.8.0 (npm) and 0.5.0 (PyPI), and those published releases still refuse a graph
containing decide, up front, before anything spends.
The 🎧 cleanvoice node (NanoGPT voice isolation), the 🧊 model3d
node, the 📦 mupload node, and the 🔌 endpoint node are supported
by the headless runners once the packages are released. npm 0.8.0 and PyPI 0.5.0 still lack
them, and those published releases refuse a graph that contains one, up front, before
anything spends.
Only an unknown node type is refused up front: the libraries warn at load and
throw at run() before any network call. That is how a graph saved before
2026-07-22 with the retired draw node behaves. The full node table is on the
format page.
Same graphs, other doors
Need the graph as an agent tool, a CI step, or a one-task skill? Same
noodle-graph.json, different wrapper:
- nanoodle-mcp — saved graphs become typed MCP tools
- run-noodle-action — run a graph in GitHub Actions
- noodle-skills — prebuilt one-task agent skills, no graph editing
A saved workflow plus a short SKILL.md is also a skill any coding agent
can run. Recipe and a poster-generator example ship in both executor repos:
docs/agent-skills.md.
No account: pay per run in Nano (x402)
Skip the API key entirely. With --pay (or a library
payment callback), each paid call returns an HTTP 402 Nano (XNO)
invoice — scan/pay from a self-custody wallet; nanoodle waits for the deposit
and continues. The library never holds funds or keys.
npx nanoodle run "https://nanoodle.com/#g=..." --pay --out ./out
python -m nanoodle run "https://nanoodle.com/#g=..." --pay --out ./out
Worked examples (QR / print invoice, wallet stub, bare chat) live in both packages: nanoodle-js/examples/x402 · nanoodle-py/examples/x402.