Open editor →

The noodle-graph.json format

What the editor's 💾 Save button writes, and what everything else — the app builder, the share links, the headless JS/Python executors — reads.

A saved workflow is plain JSON with no wrapper: the file is exactly the editor's serialized graph, pretty-printed. It is deliberately small and boring — nodes, links between their ports, and a little editor state you can ignore. The same file loads back into the editor, drives apps in the app builder, and re-executes from code via the nanoodle-js and nanoodle-py packages (see Run workflows headlessly).

Top-level shape

This is the starter workflow that ships with the app (text idea → LLM prompt-writer → image), verbatim:

{
  "v": 1,
  "nodes": [
    {
      "id": "n1",
      "type": "text",
      "x": 60,
      "y": 130,
      "fields": { "text": "a cozy ramen shop on a rainy night" },
      "w": 220
    },
    {
      "id": "n2",
      "type": "llm",
      "x": 340,
      "y": 70,
      "fields": {
        "model": "zai-org/glm-5.2",
        "system": "You write image prompts. Turn the idea into one vivid, detailed image prompt — scene, lighting, mood, style. Reply with the prompt only."
      },
      "w": 250,
      "sizes": { "system": 120 }
    },
    {
      "id": "n3",
      "type": "image",
      "x": 650,
      "y": 100,
      "fields": { "model": "nano-banana-2-lite", "size": "1k", "variations": "1" },
      "w": 250
    }
  ],
  "links": [
    { "id": "l1", "from": { "node": "n1", "port": "text" }, "to": { "node": "n2", "port": "prompt" } },
    { "id": "l2", "from": { "node": "n2", "port": "text" }, "to": { "node": "n3", "port": "prompt" } }
  ],
  "nid": 4,
  "lid": 3,
  "view": { "panX": 40, "panY": 60, "scale": 1 }
}
keymeaning
vFormat version — always 1.
nodesArray of node objects (below). The only key a loader strictly requires.
linksArray of wires: { "id", "from": {"node","port"}, "to": {"node","port"} }.
nid, lidNext node/link id counters, so newly added nodes never collide with loaded ids. Loaders recompute them from the ids anyway.
viewEditor camera: { "panX", "panY", "scale" }. Layout only — irrelevant to execution.

Node objects

keymeaning
idString id, "n1", "n2", … referenced by links.
typeOne of the node types in the table below.
fieldsThe node's parameter bag — type-specific (model, prompt, size, …). Uploaded media (image/audio/video/mask fields) is stored inline as data: URLs; share links may blank these to "" (see share links).
nameOptional user-given label. Display name resolution everywhere is: name (trimmed, if set) → the type's title → the type key.
x, y, w, sizesCanvas position, node width, per-textarea heights. Layout only — executors ignore them.

Generated results are not part of the format: the serializer never persists node outputs, so a saved file contains your workflow, not your previous runs.

Ports and wires

Every port has one of four kinds — text, image, audio, video — and only matching kinds connect. A link's from.port names an output port on the source node; to.port names an input on the target.

Two kinds of input ports exist:

  • Declared inputs — fixed per type (e.g. inpaint takes image and mask), plus dynamic families that grow as you wire more: img1, img2, … (LLM/Draw vision references), image, image2, … (Edit references), ref1, … (Text→Video references), clip1, … (Combine), and endframe (Image→Video) / audio (LLM audio input).
  • Field overrides — every multi-line text field is also a wireable text port whose port name equals the field name (prompt, system, lyrics, q, …). A wire into a field port overrides the typed value at run time. Exceptions: the text / choice / comment nodes' own fields and extraJson are not wireable.

Node types

"local" runs without any network call; "NanoGPT" calls the API (and spends); "browser-only" nodes do media processing with browser APIs — they work in the app but the headless executors reject them up front, before any paid call.

typenoderunsinputsoutputskey fields
textTextlocaltexttext
uploadImage inputlocalimageimage (data: URL)
auploadAudio inputlocalaudioaudio (data: URL)
vuploadVideo inputlocalvideovideo (data: URL)
choiceChoicelocaltextoptions, selected
joinJoinlocala, b (text)textsep
llmLLMNanoGPTimg1… (image), audiotextmodel, system, prompt, temperature, maxTokens, format, reasoningEffort, showThinking
imageImageNanoGPTimagemodel, prompt, size, variations, seed, customCivitaiAir, negativePrompt
drawDrawNanoGPTimg1… (image)image, textmodel, system, prompt, showThinking
editEditNanoGPTimage, image2…imagemodel, prompt, size, seed
inpaintInpaintNanoGPTimage, maskimagemodel, prompt, size, seed
resizeResize / cropbrowser-onlyimageimagemode, width, height
visionVisionNanoGPTimagetextmodel, q
tvideoText→VideoNanoGPTref1… (image)videomodel, prompt, duration, aspect, resolution, modelOpts
ivideoImage→VideoNanoGPTimage, endframevideomodel, prompt, duration, aspect, resolution, modelOpts
veditVideo editNanoGPTvideovideomodel, prompt, resolution, modelOpts
vframesVideo → framesbrowser-onlyvideoframe1…frameN (image)frames, gap, dir
combineCombine videosbrowser-onlyclip1… (video)videodedup
soundtrackSoundtrackbrowser-onlyvideo, audiovideoloop
lipsyncAvatar / lipsyncNanoGPTimage, audiovideomodel, prompt, resolution, modelOpts
musicMusicNanoGPTaudiomodel, prompt, lyrics, instrumental, duration, negative_prompt, seed, extraJson
remixRemix audioNanoGPTaudioaudiomodel, prompt, lyrics, duration, extraJson
ttsSpeechNanoGPTaudiomodel, prompt, voice, speed, instructions, extraJson
trimTrim audiobrowser-onlyaudioaudiostart, length
extractaudioExtract audiobrowser-onlyvideoaudiostart, length
transcribeTranscribeNanoGPTaudiotextmodel, language
commentCommentlocal (never runs)text, color

Loader rules

If you write your own reader, these are the rules the editor and the headless executors already follow:

  • Only nodes is required; treat links and everything else as optional ({"nodes": [...], "links": [...]} is a valid minimal graph).
  • Legacy alias: node type audio loads as tts (older graphs had a single combined audio node).
  • Unknown node types don't crash a load — the editor skips them; the headless libraries keep them, warn, and fail fast at run time.
  • Keep a link only if both endpoint nodes exist after filtering.
  • Migration: a link into a music or tts node's "text" port is rewritten to "prompt".

The normative spec — including execution semantics (topological order, concurrent lanes) and the exact NanoGPT request each node produces — lives with the executors: SPEC-format.md, SPEC-engine.md and SPEC-io.md.