Tag: Api

  • 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 API Automation: A Reliable Python Harness for /prompt, /history, and Output Downloads

    ComfyUI API Automation: A Reliable Python Harness for /prompt, /history, and Output Downloads

    ComfyUI ships with a small but complete HTTP API that turns the node graph into something you can drive from code. The web interface itself is just one client of this API. Once you understand its three core endpoints and, crucially, how their failures behave, you can batch hundreds of generations, integrate image generation into a pipeline, or run a headless worker without ever opening the browser.

    This article documents a working pattern for orchestrating ComfyUI over HTTP with Python. It assumes you already have a correct workflow exported in API format. If your nodes keep vanishing when you switch from the UI format, start with the guide on workflow.json vs workflow_api.json before coming back here.

    The Three Endpoints Every Automation Needs

    Reliable /prompt submission with error handling

    Most automation work touches only three routes, plus one for retrieving the generated images.

    • POST /prompt — submits a graph in API format and returns a prompt_id.
    • GET /history/{prompt_id} — returns execution results once (or whether) the job finished.
    • GET /queue — returns the current queue and running job state.
    • GET /view?filename=...&subfolder=...&type=... — streams back an output image.

    The prompt_id is the only handle you need to bind the whole lifecycle together. Treat it as the job token: submit once, then poll /history with it until the result appears.

    Step 1: Submit a Prompt and Capture the ID

    The minimal submission is a POST of the prompt object from your exported API JSON. Do not send the whole workflow file verbatim; send only the prompt key, and attach a deterministic client_id so progress events can be correlated on the WebSocket.

    import json, requests
    
    SERVER = "http://127.0.0.1:8188"
    
    with open("workflow_api.json") as f:
        workflow = json.load(f)
    
    resp = requests.post(f"{SERVER}/prompt", json={
        "prompt": workflow["prompt"],
        "client_id": "my-worker-01",
    })
    resp.raise_for_status()
    prompt_id = resp.json()["prompt_id"]
    print(prompt_id)

    Two things will bite you at this stage. First, the response may be an error object rather than a success body: a validation failure returns a non-2xx with a node_errors map. Always call raise_for_status() and read the JSON body on failure before assuming the job queued. Second, an empty or malformed prompt object is accepted-shaped but rejected at validation; if you get a validation error back, inspect the node it names instead of guessing at model or sampler settings.

    Step 2: Poll /history Instead of Sleeping Blind

    Newcomers often reach for time.sleep() and a fixed delay. That works until a job runs long or the queue is backed up. Poll /history/{prompt_id} on an interval and inspect the response shape.

    import time
    
    def wait_for_job(prompt_id, timeout=600, interval=2.0):
        deadline = time.time() + timeout
        while time.time() < deadline:
            r = requests.get(f"{SERVER}/history/{prompt_id}")
            r.raise_for_status()
            data = r.json()
            if prompt_id in data:
                return data[prompt_id]
            time.sleep(interval)
        raise TimeoutError(f"job {prompt_id} did not finish")
    
    result = wait_for_job(prompt_id)

    The key detail is the response shape: /history/{prompt_id} returns a dict keyed by prompt id, and the entry appears only once the job has completed. While the job is queued or running, the id is simply absent. That absence, not an exception, is your in progress signal. A status field inside the returned entry tells you whether it succeeded or errored, and the outputs map holds the generated filenames.

    Step 3: Resolve and Download Outputs

    History returns filenames, not image bytes. To actually fetch the result you reconstruct the file through /view using the filename, subfolder, and type reported in the output node.

    def download_outputs(result):
        files = []
        for node_id, node in result.get("outputs", {}).items():
            for image in node.get("images", []):
                params = {
                    "filename": image["filename"],
                    "subfolder": image.get("subfolder", ""),
                    "type": image.get("type", "output"),
                }
                r = requests.get(f"{SERVER}/view", params=params)
                r.raise_for_status()
                out_path = Path("downloads") / image["filename"]
                out_path.parent.mkdir(parents=True, exist_ok=True)
                out_path.write_bytes(r.content)
                files.append(str(out_path))
        return files

    Do not hard-code an output path. The subfolder and type fields exist precisely because outputs are not always flat in the output/ directory. Reconstruct the path from what history reports, and you will never chase a file that landed in a dated subfolder.

    Step 4: Handle Timeouts, Partial Failures, and the Queue

    Automation fails in predictable ways. Design for them explicitly rather than catching a broad Exception.

    • Queue backpressure. If you submit faster than the GPU processes, prompts pile up. Check /queue before submitting and either throttle or reject new work when queue_running is non-empty and the pending list is long.
    • Validation rejection. Catch the non-2xx on /prompt, log the node_errors, and skip that job cleanly instead of crashing the batch loop.
    • Execution error after acceptance. A job in history can carry a status of error with a traceback in its messages. Treat that as a terminal state and surface the traceback, not as something to retry blindly.
    • View endpoint misses. /view can 404 transiently if the file is still being written. Retry with a short backoff.

    A robust loop combines all four paths into one: submit, poll until present in history, branch on status, then download or record the error.

    Common Pitfalls When Automating ComfyUI

    Several failures recur across nearly every first integration and are easy to misattribute.

    • Sending the UI workflow instead of the API format. The browser format references nodes by links and widget-only inputs; the API wants every input resolved. If your POST returns a flood of missing-input errors, you are almost certainly sending the wrong format. See the workflow API format guide.
    • Ignoring node_errors. The validation body names the exact node id and class that failed. Read it before changing anything else; it is the single most information-dense error ComfyUI emits.
    • Assuming instant id presence in history. A job that just submitted will not be in /history yet, and polling too fast with no backoff just hammers the server. A 1–2 second interval with a bounded deadline is the right default.
    • Shared state across threads. If you parallelize, keep a distinct client_id per worker so WebSocket progress for one job does not get mis-attributed to another.
    • No timeout on requests. A hung connection will block your whole pipeline. Set a per-request timeout on every requests call.

    Verifying the Harness End to End

    Before trusting the automation in production, verify each failure mode deliberately rather than only the happy path.

    • Submit a known-good API prompt and confirm the prompt_id comes back and the job appears in history with a success status.
    • Submit a deliberately broken prompt (for example, a required input set to null) and confirm you catch the validation rejection and log the node id.
    • Point /view at a filename that does not exist and confirm the 404 is retried and then recorded, not silently swallowed.
    • Run two workers with distinct client_ids against the same queue and confirm outputs are attributed correctly.

    This harness pattern has been used to drive batch generation over the HTTP API described in the official ComfyUI documentation; the endpoint semantics above are those documented for the server, and the history-absent-until-complete behavior is the standard contract for /history/{prompt_id}. Treat this as a reference pattern tuned from the documented API rather than a benchmark of any specific hardware.

    When to Reach for a Client Library

    Hand-rolling the three endpoints is worth it once, because it teaches you exactly where failures live. After that, a maintained client can save you boilerplate. Libraries such as comfyui_xy wrap ComfyUiClient-style submission, and community wrappers add queue-status polling on top of the same routes. The trade-off is opacity: a wrapper that hides the node_errors body will make validation failures harder to diagnose. If you adopt a library, make sure it exposes the raw response body or the underlying error, and keep the direct /prompt /history /view flow in your back pocket for debugging.

    Conclusion

    ComfyUI automation is fundamentally a lifecycle problem: submit, poll, fetch, handle failure. The HTTP API gives you a stable, language-agnostic contract to build that lifecycle on. The pattern that survives contact with production is one that reads the validation body before anything else, treats an absent history entry as its in-progress signal, reconstructs output paths from what history reports, and verifies every failure mode on purpose. Get those four behaviors right and the rest of the pipeline — whether it is a cron job, a webhook, or a headless worker — becomes routine.

    🛠️ Resources & Tools Mentioned

    Tools our readers use most for AI image:

    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.