Open editor →

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

  1. Install nanoodle — that is the package name on both registries. The GitHub repos are nanoodle-js and nanoodle-py. Do not npm install nanoodle-js.
  2. inspect the share link. Offline. No key. Prints inputs, outputs, settings.
  3. run it. That call spends your NanoGPT balance.
JavaScript · npm nanoodle 0.8.0
npx nanoodle inspect "https://nanoodle.com/#g=..."
export NANOGPT_API_KEY=...
npx nanoodle run "https://nanoodle.com/#g=..." --out ./out
Python · PyPI nanoodle 0.5.0
pip 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.

CLI
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 twin
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.

CLI
npx nanoodle inspect "https://nanoodle.com/#g=..."
npx nanoodle run "https://nanoodle.com/#g=..." --out ./out
Python twin
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.

JavaScript
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);
Python
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:

JavaScript
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");
Python
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 — load NANOGPT_API_KEY from a .env-style file (--key wins if both are set)
  • --input k=@path — read a file (media as media; .txt / .md / .json as 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:

nodeJavaScriptPython
resizePure JS for a PNG source; ffmpeg for any other formatffmpeg
trimPure JS for a PCM WAV source; ffmpeg for any other formatffmpeg
combinePure JS (lossless mp4 remux) when every clip is mp4 with matching stream parameters; ffmpeg otherwiseffmpeg
vframesffmpegffmpeg
soundtrackffmpegffmpeg
extractaudioffmpegffmpeg

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:

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.

JavaScript
npx nanoodle run "https://nanoodle.com/#g=..." --pay --out ./out
Python
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.