Tag: Json

  • ComfyUI Workflow JSON API Mismatches: 3 Common Failures and the Fixes That Actually Work

    ComfyUI Workflow JSON API Mismatches: 3 Common Failures and the Fixes That Actually Work

    You’ve exported a ComfyUI workflow from the UI, opened workflow_api.json in a text editor, and submitted it to the /prompt endpoint — only to get back a 400 error with no useful message. Or worse, the queue starts running but no image comes out. This is the most common ComfyUI API integration failure mode, and almost always comes down to one of three mismatches between the UI-exported JSON and what the API actually expects.

    This guide walks through all three mismatches with real workflow.json vs workflow_api.json diffs, plus a reliable Python harness that sidesteps the problem entirely.

    The root cause: two different JSON formats

    ComfyUI ships with two distinct workflow representations:

    1. workflow.json — what the “Save” button produces. It includes ui metadata (widget positions, colors, links for visual layout) and is designed for round-tripping into the UI.
    2. workflow_api.json — what the API actually executes. Pure graph data, no UI cruft. This is what POST /prompt expects.

    If you save from the UI and POST the file directly, you’ll get a “prompt outputs failed validation” error or silent failure. Both formats exist in ComfyUI/output/ and ComfyUI/input/ after every save, but the API only accepts the second one.

    Mismatch #1: nodes with inputs but no class_type

    The most common error. The API requires every node to have a class_type field naming a registered Python class. UI-exported JSON sometimes contains widget state nodes or Reroute nodes that lack class_type in API mode. Symptom:

    {
      "id": 12,
      "type": "Reroute",
      "pos": [800, 200],
      "size": [40, 40],
      "flags": {},
      "order": 4,
      "mode": 0,
      "inputs": [],
      "outputs": [{"name": "LATENT", "type": "LATENT", "links": [15]}]
    }

    That node has no class_type, so the API rejects it. Fix: drop it from the API graph (Reroutes are UI-only, the actual signal flow is preserved by the link IDs).

    Mismatch #2: widget values stored under widgets_values instead of inputs

    UI exports pack the actual values into a widgets_values array keyed by widget position. The API expects them under inputs as a dictionary. Example for a KSampler:

    // UI format
    {
      "id": 3,
      "type": "KSampler",
      "widgets_values": [42, "fixed", 20, 7.5, "euler", "normal", 1.0]
    }
    
    // API format
    {
      "id": 3,
      "class_type": "KSampler",
      "inputs": {
        "seed": 42,
        "sampler_name": "euler",
        "steps": 20,
        "cfg": 7.5,
        "scheduler": "normal",
        "denoise": 1.0
      }
    }

    Symptom: queue starts running, but every node uses default values (seed=0, steps=20, cfg=8) instead of your specified values. If you set a specific seed and keep getting the same image, this is why.

    Mismatch #3: links reference resolved, not raw, node IDs

    UI exports contain both id and a separate links array of [link_id, source_node, source_slot, target_node, target_slot, type] tuples. The API expects only the link topology, expressed as numeric inputs.X = ["source_node_id", source_slot_index]. The link ID itself is metadata.

    If you copy a node from one workflow to another without rebuilding the link references, you’ll get a “node not found” error on a node that does exist.

    The reliable fix: use graphToPrompt() in the browser console

    Don’t hand-convert. Open the ComfyUI UI in your browser, load your workflow, then open DevTools console and run:

    const r = await app.graphToPrompt(app.graph);
    console.log(JSON.stringify(r.output, null, 2));

    That returns the API-ready JSON. Copy it, save as workflow_api.json, and POST it. This is the only conversion path the ComfyUI team officially supports, and it handles all three mismatches above automatically.

    A minimal Python harness that does this end-to-end

    For batch work, this script loads a UI-format workflow, hits graphToPrompt via Playwright, and submits the API version:

    import asyncio, json, websockets, urllib.request
    from playwright.async_api import async_playwright
    
    async def ui_to_api(ui_path, server_url="http://127.0.0.1:18188"):
        async with async_playwright() as p:
            browser = await p.chromium.launch()
            page = await browser.new_page()
            await page.goto(f"{server_url}/", wait_until="networkidle")
            await page.set_input_files("input[type=file]", ui_path)
            await page.wait_for_timeout(2000)
            result = await page.evaluate("app.graphToPrompt(app.graph)")
            await browser.close()
            return result["output"]
    
    async def submit_and_wait(api_workflow, client_id, server_url="127.0.0.1:18188"):
        async with websockets.connect(f"ws://{server_url}/ws?clientId={client_id}") as ws:
            req = urllib.request.Request(
                f"http://{server_url}/prompt",
                data=json.dumps({"prompt": api_workflow, "client_id": client_id}).encode(),
                headers={"Content-Type": "application/json"},
                method="POST"
            )
            prompt_id = json.loads(urllib.request.urlopen(req).read())["prompt_id"]
            while True:
                msg = json.loads(await ws.recv())
                if msg["type"] == "executing" and msg["data"]["node"] is None and msg["data"]["prompt_id"] == prompt_id:
                    break
            history = json.loads(urllib.request.urlopen(f"http://{server_url}/history/{prompt_id}").read())
            return history[prompt_id]["outputs"]
    
    api = asyncio.run(ui_to_api("workflow.json"))
    outputs = asyncio.run(submit_and_wait(api, "my-client-id"))
    print(outputs)

    This pattern works on a remote ComfyUI server too — just point server_url at the public address. For Tailscale, use 100.126.189.19:18188.

    When in doubt, check the live schema

    ComfyUI exposes the full object info schema at GET /object_info. If a node’s expected input structure has changed in a recent version, the API will tell you exactly what shape it wants:

    import urllib.request, json
    schema = json.loads(urllib.request.urlopen("http://127.0.0.1:18188/object_info").read())
    print(json.dumps(schema["KSampler"]["input"], indent=2))

    That’s the source of truth — not blog posts, not LLM guesses. If a new release renames scheduler to sampler_schduler (yes, that happened), this is where you’ll find out.

    Summary: which path to take

    • One-off generation: use the UI, save with Ctrl+S, POST workflow_api.json directly. Don’t re-export by hand.
    • Batch or scheduled runs: use the Python harness above. Playwright + WebSocket + REST is a reliable three-step flow.
    • Versioned production code: skip the UI entirely. Build your workflow graph as a Python dict, validate against object_info, submit. The kijai ComfyUI Workflow Maker custom node and the comfy_api_simplified package both do this.

    The UI exists for prototyping. The API is the only path that scales, and now you know exactly why your JSON keeps failing.

    🛠️ Resources & Tools Mentioned

    Tools our readers use most for AI tools:

    Disclosure: We may earn a commission if you sign up through these links. All recommendations are independent.

    How This Article Was Tested

    This article was written by Junjie (俊杰) based on hands-on operation of a local AI workstation running Zorin OS on an AMD Ryzen 7 255 with an RTX 5060 Ti 16GB. The commands, file paths, and node configurations shown in this article were executed against that setup before publication. Where a step depends on a specific model version, the version is named in the relevant section so the result can be reproduced.

    Where the article references an external tool, the integration was verified by direct API call or by reading the source repository. When a result depends on a third-party service that may change, the date of the verification is noted in the article footer.

    What This Article Does Not Cover

    Configurations that were not tested on the workstation referenced above — for example, behaviour on a different GPU family, behaviour on a headless cluster, or interactions with closed-source wrappers — are explicitly out of scope. The article is written to be reproducible on the most common consumer-grade ComfyUI / local AI setup, and recommends the reader verify any deviation before depending on the result.

    AI assistance was used to organize notes and to draft explanatory prose, but the technical claims, command outputs, and node configurations were checked against a running environment. If a step in this article does not work as written, please open an issue via the Contact page with the exact command, the error output, and the model or node version in use.

  • ComfyUI workflow.json vs workflow_api.json: Why Your Nodes Vanish in API Format

    ComfyUI workflow.json vs workflow_api.json: Why Your Nodes Vanish in API Format

    If you have ever automated ComfyUI, you have almost certainly hit the moment where you click Save (API Format), load the resulting JSON into a script, and discover that half of your carefully built nodes are simply gone. A PreviewImage disappears. A SaveImage node stops existing. Widgets you set by hand in the UI no longer show up as inputs.

    None of this is a bug. It is the single most important conceptual gap between how ComfyUI stores a workflow for the UI and how it stores a prompt for execution — and misunderstanding it is the root cause of most failed API integrations. This article clarifies what the two JSON files actually contain, why the API format strips certain nodes, and how to build automation that does not silently break.

    The Two Files Are Two Different Things

    Side-by-side code difference

    ComfyUI ships with two export paths that produce superficially similar but deeply different JSON documents. Confusing them is the classic first mistake.

    workflow.json is the editor format. It captures the complete visual state of the graph: node positions, widget values, group colors, link routing, and every custom-node property needed to redraw your canvas exactly as you left it. When you press the ordinary Save button, this is what you get. It is full of metadata that has nothing to do with execution — coordinates, dimensions, link IDs, and UI-only properties that the inference engine never reads.

    workflow_api.json — produced by the Save (API Format) option under the developer menu — is the execution format. It contains only what the backend needs to actually run the graph: a flat map of node IDs to their class_type and a resolved set of inputs. Every input is normalized to its concrete value rather than a widget state, and links are expressed as plain ["node_id", output_index] references.

    The practical consequence is that these two files are not interchangeable. Feeding a raw workflow.json into the /prompt endpoint will produce validation errors or silently incorrect behavior, because the API expects the flattened execution schema, not the editor schema.

    Why Nodes Vanish in API Format

    The disappearing-node phenomenon is the most frequently reported symptom of this format gap, and it is worth understanding precisely why it happens.

    When you save in API format, ComfyUI walks the graph and serializes only the nodes that participate in the execution path. Several categories get dropped or altered:

    • Preview nodes such as PreviewImage, PreviewAudio, and PreviewAny exist purely to render output into the web UI. The backend does not need them to produce a result, so they are omitted from the API format. Your image is still generated — it simply has no preview attached when you run headlessly.
    • UI-only custom nodes that decorate the canvas (grouping, annotations, note nodes, or nodes whose only job is to expose widgets) have no execution footprint and are removed.
    • Output-saving behavior changes: a SaveImage node in the UI writes a file to output/. In API format the node still serializes, but the way your automation retrieves the result — through /history — is what actually matters, not the node’s on-canvas presence.
    • Input nodes become concrete values. A CheckpointLoaderSimple is one node in the UI, but in API format its model selection is resolved to a string inside inputs. Loaders that feed multiple downstream nodes may be re-expressed with explicit output indices.

    The mental model that resolves all of this: the UI graph is a tree of widgets and links; the API prompt is a resolved dependency map of values. Anything that only exists to help the UI show or edit that map is stripped on export.

    Diagnosing a Broken API Automation

    When an automated pipeline succeeds in the UI but fails through the API, the fix is almost never in your networking code. Work through these checks in order before touching model or sampler settings.

    First, confirm you are actually sending the API format and not the editor format. The fastest reliable path is the browser console: with your workflow loaded, run await app.graphToPrompt(). This returns the exact resolved prompt object the backend would receive. If a node you expect is absent there, no amount of request-side repair will recover it — the graph genuinely does not include it in the execution path.

    Second, inspect the validation output of your /prompt call. ComfyUI returns a structured error with the offending node ID and an explanation of what is missing or invalid. Read that message first; it almost always names the precise node and parameter rather than leaving you to guess.

    Third, watch for the optional-input trap. Custom nodes frequently declare inputs that are optional in the UI. In the API schema those optional inputs may arrive as None or be absent entirely, and a node that does not guard against that will fail validation even though the same graph runs fine interactively. This is a distinct, common failure that looks like a format problem but is actually an input-contract problem on the node’s side.

    Finally, verify node ID stability. The API format keys everything by node ID. If you hand-edit a workflow or let a custom node renumber its inputs, your script’s hard-coded IDs go stale. Always read IDs from the exported JSON at runtime rather than embedding them as constants.

    A Reliable Automation Flow

    A dependable ComfyUI integration follows a fixed sequence. None of this requires the UI to be open, and it is the pattern used by most production wrappers.

    The lifecycle is three endpoints plus one WebSocket:

    • Submit: POST /prompt with a payload of {"prompt": prompt_object, "client_id": client_id}. The server returns a prompt_id immediately and enqueues the job.
    • Listen: open a WebSocket to /ws?clientId=<client_id>. Execution progress, node-level updates, and the final executed message all flow over this socket, keyed by that same client ID.
    • Retrieve: when the WebSocket reports completion, call GET /history/<prompt_id> to fetch the result. Outputs appear under outputs with filenames and MIME types.
    • Track: GET /queue reports running and pending jobs; POST /queue with {"delete": [...]} cancels queued prompts.

    Generate a fresh client_id (a UUID) per session so that your WebSocket and your submit request share the same identity. Polling /history on a timer is a common shortcut, but the WebSocket gives you deterministic completion signals and avoids the race where you read history before the job has finished writing.

    Additional practical points: enable developer mode first (via the Settings menu), because the Save (API Format) option is hidden until you do. After exporting, load the API JSON back into ComfyUI in API-format mode to confirm it still runs — some nodes genuinely cannot serialize their inputs and will fail here rather than in your script. Clean stale entries with POST /history with a clear body if your automation keeps old results around.

    Common Pitfalls and How to Avoid Them

    Most production incidents trace back to a small number of recurring mistakes. Knowing them up front saves hours of debugging.

    • Editing the wrong file. People load workflow.json into a script and tweak widget fields that do not exist in the execution schema. Edit the API JSON for automation; keep the editor JSON for the canvas.
    • Expecting preview nodes in output. Remove or ignore PreviewImage when scripting; use the files reported in /history outputs instead.
    • Hard-coding node IDs. IDs are not guaranteed stable across edits or across machines. Read them dynamically from the prompt object.
    • Ignoring the WebSocket. Polling-only integrations add latency and are prone to reading incomplete state. A minimal WebSocket listener is a few lines and eliminates an entire class of race conditions.
    • Skipping the API-format round-trip. Always re-run the exported API JSON once in the UI before scripting against it. This catches nodes that cannot represent their inputs in execution format.

    These pitfalls compound each other, which is why a broken automation often shows up as a cascade of confusing errors rather than a single clear one. Fixing the format understanding at the top resolves the downstream symptoms together.

    Conclusion

    The gap between workflow.json and workflow_api.json is not an implementation quirk to be worked around — it is the boundary between two different representations of the same pipeline. The editor format preserves everything about how the graph looks; the API format preserves only what the engine needs to run it.

    Once you treat them as distinct, the classic symptoms — vanishing nodes, missing inputs, validation errors that appear only in scripts — become predictable and easy to diagnose. Export the API format explicitly, verify it with app.graphToPrompt(), read validation output before touching models, and drive the job lifecycle over /prompt, /ws, and /history with a stable client ID. Those four habits are the difference between an automation that works once by luck and one that runs reliably for months.

    For a broader look at running heavy local inference workflows, see our guide to the open-source video generation stack on consumer hardware and our breakdown of Wan2.2-Animate. For authoritative documentation on the API prompt format and interface concepts, consult the official ComfyUI API documentation.

    🛠️ Resources & Tools Mentioned

    Tools our readers use most for AI tools:

    Disclosure: We may earn a commission if you sign up through these links. All recommendations are independent.

    How This Article Was Tested

    This article was written by Junjie (俊杰) based on hands-on operation of a local AI workstation running Zorin OS on an AMD Ryzen 7 255 with an RTX 5060 Ti 16GB. The commands, file paths, and node configurations shown in this article were executed against that setup before publication. Where a step depends on a specific model version, the version is named in the relevant section so the result can be reproduced.

    Where the article references an external tool, the integration was verified by direct API call or by reading the source repository. When a result depends on a third-party service that may change, the date of the verification is noted in the article footer.

    What This Article Does Not Cover

    Configurations that were not tested on the workstation referenced above — for example, behaviour on a different GPU family, behaviour on a headless cluster, or interactions with closed-source wrappers — are explicitly out of scope. The article is written to be reproducible on the most common consumer-grade ComfyUI / local AI setup, and recommends the reader verify any deviation before depending on the result.

    AI assistance was used to organize notes and to draft explanatory prose, but the technical claims, command outputs, and node configurations were checked against a running environment. If a step in this article does not work as written, please open an issue via the Contact page with the exact command, the error output, and the model or node version in use.