Skip to content
Skip to main content
A brass go/no-go plug gauge on a steel workbench next to a machined part on white paper and a second part tagged with a red reject tag, a metaphor for ComfyUI rejecting an API prompt that fails validation
8 min readBy Carlos Aragon

ComfyUI Prompt Outputs Failed Validation: API Fix

"Prompt outputs failed validation" means ComfyUI rejected your graph before running a single node. The reason isn't in the message. It's in node_errors, keyed by node id, with the exact input name and the value that failed. Fix that input and resubmit. The UI shows this as a red toast. Over the API it's an HTTP 400 that a lot of client code logs as "bad request" and throws away. I broke a known-good graph 11 different ways on my own ComfyUI server to see what each failure looks like on the wire, and found one case where ComfyUI says 200 and still skips part of your workflow.

What Does the Validation Error Look Like From the API?

Every time you POST /prompt, ComfyUI walks back from each output node (SaveImage, PreviewImage, video combine) and checks every input on the way. If something on that path is wrong, you get this, straight from my server running ComfyUI 0.33.0:

HTTP 400
{
  "error": {
    "type": "prompt_outputs_failed_validation",
    "message": "Prompt outputs failed validation",
    "details": "", "extra_info": {}
  },
  "node_errors": {
    "4": {
      "class_type": "CheckpointLoaderSimple",
      "dependent_outputs": ["9"],
      "errors": [{
        "type": "value_not_in_list",
        "message": "Value not in list",
        "details": "ckpt_name: 'juggernautXL_v10.safetensors' not in (list of length 23)",
        "extra_info": {
          "input_name": "ckpt_name",
          "input_config": null,
          "received_value": "juggernautXL_v10.safetensors"
        }
      }]
    }
  }
}

The top-level error is the same for almost every failure. Read node_errors. The key ("4") is the node id from your API-format JSON. extra_info.input_name tells you which input, received_value tells you what you sent, and dependent_outputstells you which output node would have used it. That last field matters more than it looks, as you'll see below.

Nothing gets queued and nothing touches the GPU. Every rejection in my tests came back in 0.06 to 0.23 seconds, so validation is cheap. You can afford to hit it on every job.

What Causes "Prompt Outputs Failed Validation"?

I took a plain SDXL text-to-image graph (checkpoint, two prompts, KSampler, VAE decode, save) that passes validation, and broke one thing at a time:

What I brokeError type returnedSeconds
Checkpoint filename from another machinevalue_not_in_list0.23
A1111 sampler name dpmpp_2m_karrasvalue_not_in_list0.11
seed: -1 for randomvalue_smaller_than_min0.07
cfg: 150value_bigger_than_max0.06
steps: "twenty"invalid_input_type0.08
KSampler with no model linkrequired_input_missing0.06
CLIP output wired into a MODEL inputreturn_type_mismatch0.10
Link to node id 99, which doesn't existexception_during_inner_validation0.08
Two broken nodes in one graphboth reported, one response0.07
No SaveImage or PreviewImageprompt_no_outputs (top-level)0.08
Custom node not installedmissing_node_type (top-level)0.08

The last two are different. They come back as their own error.type with an empty node_errors, so code that only reads node_errors will log nothing useful. Read both.

value_not_in_list: the one you'll see most

Model filenames cause most of these. A workflow exported on one machine carries that machine's filenames, subfolders and all, and the server only accepts exact matches. Annoyingly, the error doesn't tell you what is valid. input_config comes back null and the message just says "list of length 23". Ask the server directly: GET /models/checkpoints returns the 23 names, and GET /object_info/CheckpointLoaderSimplereturns them inside the node's input spec.

The second most common one bites anyone coming from Automatic1111. dpmpp_2m_karrasisn't a ComfyUI sampler. ComfyUI splits it into sampler_name: "dpmpp_2m" and scheduler: "karras". My server lists 63 samplers and none of them carries a scheduler suffix.

Min and max errors: seed -1 is the classic

KSampler's seed has min: 0. Sending -1to mean "random" gets Value -1 smaller than min of 0. Generate the random number in your client. The same family covers cfg over 100 and steps under 1. ComfyUI also tries to convert what you send to the declared type. "twenty" can't become an INT, so you get invalid_input_type with the Python exception in details.

Broken links lie about where they are

required_input_missing and return_type_mismatch are honest. The second one even prints received_type(CLIP) mismatch input_type(MODEL)and the linked node id. A link to a node id that doesn't exist is not honest. I pointed KSampler's model at node 99 and got exception_during_inner_validation with a KeyError: '99', an input_name of samples(an input that belongs to the downstream VAE decode, not KSampler) and a Python traceback that includes the server's install path. That traceback goes to whoever called the API, which is one more reason not to leave /prompt open to the internet. I covered locking that down in ComfyUI 403 behind a reverse proxy.

When I broke two nodes at once, both came back in the same response. ComfyUI doesn't stop at the first error, so fix everything in node_errors before you retry.

Can ComfyUI Return 200 and Still Skip Part of the Workflow?

Yes, and this is the case I'd worry about in production. I built a graph with two outputs: one tiny valid branch (empty image to SaveImage) and one branch fed by a checkpoint loader with a bad filename. Here's what came back:

HTTP 200
{
  "prompt_id": "59a93834-...",
  "number": 12,
  "node_errors": {
    "3": {
      "class_type": "CheckpointLoaderSimple",
      "dependent_outputs": ["6"],
      "errors": [{ "type": "value_not_in_list", ... }]
    }
  }
}
# /history/59a93834-... -> status_str "success", outputs: ["2"]

If any output validates, ComfyUI queues the prompt, returns 200, runs only the valid outputs, and history says success. The broken branch never ran. The only trace is the node_errorsin the 200 body, which nearly every client I've seen ignores because the status code was fine. In a pipeline that saves a preview and a final upscale as two outputs, you'd get the preview, a green check, and no final image.

The opposite case is harmless. A broken node that feeds no output at all, like a leftover loader sitting off to the side of the canvas, is never validated. That graph returned 200 with an empty node_errors.

So the rule for client code is short: a job is accepted only if the status is 200 and node_errors is empty. Then confirm the files exist, as I explained in why ComfyUI's /history comes back empty. "Queued" isn't "done", and "success" isn't "every output".

How Do I Validate a Workflow Before Sending It?

The server already publishes everything it validates against. GET /object_info returns every node class with its required and optional inputs, types, combo options, min, max and outputs. The ComfyUI server routes docs list it next to /prompt. On my box that's 4,431 node types and 8.2 MB, fetched in about a second, so cache it and refresh when you add models or custom nodes.

Here's the preflight I now run before every submit. Standard library only:

import json, urllib.request

BASE = "http://127.0.0.1:8188"

def call(path, payload=None):
    req = urllib.request.Request(
        BASE + path,
        data=json.dumps(payload).encode() if payload is not None else None,
        headers={"Content-Type": "application/json"},
    )
    try:
        with urllib.request.urlopen(req) as r:
            return r.status, json.load(r)
    except urllib.error.HTTPError as e:
        return e.code, json.loads(e.read() or b"{}")

def options(spec):
    if isinstance(spec[0], list):          # legacy combo: [[...], {...}]
        return spec[0]
    if spec[0] == "COMBO":                 # newer combo: ["COMBO", {"options": [...]}]
        return spec[1].get("options", [])
    return None

def preflight(graph, info):
    problems = []
    for nid, node in graph.items():
        ct = node.get("class_type")
        if ct not in info:
            problems.append(f"#{nid}: {ct} is not installed")
            continue
        req = info[ct]["input"].get("required", {})
        spec = {**req, **info[ct]["input"].get("optional", {})}
        inputs = node.get("inputs", {})
        for name in req:
            if name not in inputs:
                problems.append(f"#{nid} {ct}.{name}: required input missing")
        for name, val in inputs.items():
            s = spec.get(name)
            if s is None:
                continue
            if isinstance(val, list) and len(val) == 2 and isinstance(val[1], int):
                src, slot = str(val[0]), val[1]
                if src not in graph:
                    problems.append(f"#{nid} {ct}.{name}: links to missing node #{src}")
                    continue
                outs = info.get(graph[src].get("class_type"), {}).get("output", [])
                got, want = (outs[slot] if slot < len(outs) else None), s[0]
                if got is None:
                    problems.append(f"#{nid} {ct}.{name}: #{src} has no output {slot}")
                elif isinstance(want, str) and "*" not in (want, got) and got not in want.split(","):
                    problems.append(f"#{nid} {ct}.{name}: gets {got}, wants {want}")
                continue
            opts = options(s)
            if opts is not None:
                if val not in opts:
                    problems.append(f"#{nid} {ct}.{name}: {val!r} not in {len(opts)} options")
                continue
            if s[0] in ("INT", "FLOAT"):
                cfg = s[1] if len(s) > 1 else {}
                try:
                    num = int(val) if s[0] == "INT" else float(val)
                except (TypeError, ValueError):
                    problems.append(f"#{nid} {ct}.{name}: {val!r} is not {s[0]}")
                    continue
                if num < cfg.get("min", num) or num > cfg.get("max", num):
                    problems.append(f"#{nid} {ct}.{name}: {num} outside {cfg.get('min')}..{cfg.get('max')}")
    if not any(info.get(n.get("class_type"), {}).get("output_node") for n in graph.values()):
        problems.append("no output node (SaveImage, PreviewImage, ...)")
    return problems

def submit(graph, info):
    problems = preflight(graph, info)
    if problems:
        raise ValueError("preflight: " + "; ".join(problems))
    status, body = call("/prompt", {"prompt": graph})
    if status != 200 or body.get("node_errors"):
        raise RuntimeError(f"ComfyUI rejected {status}: {json.dumps(body)[:500]}")
    return body["prompt_id"]

# info = call("/object_info")[1]   # cache it; refresh when models or nodes change

I ran it against all 11 broken graphs and the two-output case. It flagged every one, with a message that names the right input. That includes the node-99 link, which it reports as KSampler.model: links to missing node #99 instead of a traceback. The known-good graph came back clean.

It's deliberately stricter than the server in one way: it checks every node, not just the ones that feed an output, so it'll complain about that orphan loader on the canvas. I want that. An orphan in an API workflow is almost always a branch someone forgot to wire up.

The preflight doesn't replace the server check, it moves the error earlier. Custom nodes can add their own VALIDATE_INPUTSlogic that /object_info can't describe, so keep the node_errors check in submit() too. The point is that a batch job from n8n or a queue worker fails on the machine that built the graph, with a readable message, before it ever reaches the GPU box. If you want to see how ComfyUI does it internally, validate_inputs in execution.py is the source of every error type in the table above.

Should I Just Turn Off Workflow Validation?

No. ComfyUI's troubleshooting guide mentions a validation toggle, but that setting lives in the browser frontend. The server checks every /promptregardless, so over the API there's nothing to turn off, and the job won't run until the input is fixed. If you're seeing this in the UI after loading someone else's workflow, the fix is the same: find the node with the red outline, pick a value your install actually has, and save the workflow from the canvas so the fix sticks.

For API work, I'd go further and always export with "Export (API)" from the same ComfyUI version the server runs. Most of my filename and sampler errors came from workflows built on one machine and replayed on another. More of that in my ComfyUI best practices post.

How I tested this: ComfyUI 0.33.0 on my own Windows GPU server, reached through a Cloudflare Tunnel, October 19, 2026. 11 deliberately broken copies of a 7-node SDXL graph sent to POST /prompt, plus two multi-output graphs. None of the broken graphs queued. The two 200 cases ran an 8×8 empty-image save, no model load. Timings are wall-clock from the client.

ComfyUI Validation Error FAQ

What does 'Prompt outputs failed validation' mean in ComfyUI?

ComfyUI checked the graph before running it and at least one input on the path to an output node was invalid. The top-level message is generic. The real reason is in node_errors in the same response, keyed by node id, with the input name and the value it received.

Why does ComfyUI say 'Value not in list' for a model that exists?

The filename has to match exactly what the server lists, including any subfolder and the extension. Workflows shared from another machine usually carry that machine's filenames. The error only says 'list of length N', so pull the real names from GET /object_info/CheckpointLoaderSimple or GET /models/checkpoints.

Can ComfyUI return 200 and still skip part of my workflow?

Yes. If at least one output node validates, POST /prompt returns 200 with a prompt_id and a non-empty node_errors. Only the valid outputs run, and history marks the job as success. In my test, a two-output graph with one broken branch saved one image and reported success.

How do I pass a random seed to the ComfyUI API?

Generate it in your client. KSampler's seed has a minimum of 0, so the A1111 habit of sending -1 for random fails with 'Value -1 smaller than min of 0'. Any integer from 0 up to 18446744073709551615 works.

Should I disable workflow validation to get rid of the error?

It won't help. That setting lives in the browser frontend. The server checks every POST /prompt regardless, so over the API there is nothing to turn off, and the job won't run until the input is fixed.

Running ComfyUI Behind an API?

I run ComfyUI as the image and video engine behind n8n pipelines and client apps. If your jobs fail quietly, or you want ComfyUI wired into a queue that tells you exactly what broke, send me what you've got.

Related Posts