Skip to content
,

ComfyUI API Guide: Run Workflows From Python (Prompt, WebSocket, Images)

8–13 minutes
Smiling woman coding on a laptop with images generating on a second screen, API card, hook automate ComfyUI with Python

Once a ComfyUI workflow works, the next step is often to run it without clicking: from a Python script, a website, a Discord bot or a batch job that makes 200 images overnight.

ComfyUI has a built-in HTTP and WebSocket API for exactly this. It is the same API the web interface uses, so anything you can do in the browser you can do from code.

This guide covers exporting a workflow in API format, sending it to the /prompt endpoint, changing the prompt and seed from code, waiting for the result over WebSocket, downloading images, uploading inputs, batch runs, remote access, and the errors you will hit along the way. All code is plain Python.

TL;DR

  • Export: open your workflow, then Workflow, Export (API). You get a JSON file keyed by node ID.
  • Run: POST {"prompt": workflow, "client_id": id} to http://127.0.0.1:8188/prompt. It returns a prompt_id.
  • Wait: listen on ws://127.0.0.1:8188/ws?clientId=id. The run is done when an executing message has node set to null for your prompt.
  • Get results: GET /history/{prompt_id}, then download each image from /view?filename=...&subfolder=...&type=output.
  • Inputs: POST images to /upload/image, then use the returned name in a LoadImage node.
  • Official examples: script_examples/ in the ComfyUI repo.

How the ComfyUI API Works

When you press Queue in the browser, the web page sends your graph to the ComfyUI server as JSON and listens for progress messages. Your script does the same thing:

  1. Send the workflow JSON to POST /prompt. The server puts it in the queue and returns a prompt_id.
  2. Listen on the /ws WebSocket for progress, or poll /history/{prompt_id}.
  3. When it finishes, read the outputs from /history and fetch each file from /view.

The server runs on port 8188 by default. Nothing extra needs installing on the ComfyUI side. For the script, you need Python 3 and the websocket-client package (pip install websocket-client), which is what ComfyUI’s own examples use.

Step 1: Export the Workflow in API Format

The normal saved workflow (with node positions, groups and links) is not what the API wants. You need the API format.

  1. Open the workflow in ComfyUI and run it once to make sure it works.
  2. Click Workflow in the top menu, then Export (API).
  3. Save the file, for example as workflow_api.json, next to your script.

In older builds, this option only appears after you turn on dev mode in the settings (the “Enable Dev mode Options” setting), and the button is called “Save (API Format)”. Our guide to saving and exporting workflows shows both formats side by side.

The API file is a dictionary keyed by node ID. Each node has a class_type and its inputs:

{
  "3": {
    "class_type": "KSampler",
    "inputs": {
      "seed": 156680208700286,
      "steps": 20,
      "cfg": 8,
      "sampler_name": "euler",
      "scheduler": "normal",
      "denoise": 1,
      "model": ["4", 0],
      "positive": ["6", 0],
      "negative": ["7", 0],
      "latent_image": ["5", 0]
    }
  },
  "6": {
    "class_type": "CLIPTextEncode",
    "inputs": {"text": "a cosy cabin in the snow", "clip": ["4", 1]}
  }
}

Links between nodes appear as ["node_id", output_index]. Widget values, like the seed or prompt text, are plain values you can change.

Step 2: Queue Your First Prompt

Start ComfyUI as normal, then run this:

import json
import uuid
import urllib.request

SERVER = "127.0.0.1:8188"
CLIENT_ID = str(uuid.uuid4())

def queue_prompt(workflow):
    data = json.dumps({"prompt": workflow, "client_id": CLIENT_ID}).encode("utf-8")
    req = urllib.request.Request(f"http://{SERVER}/prompt", data=data,
                                 headers={"Content-Type": "application/json"})
    return json.loads(urllib.request.urlopen(req).read())

with open("workflow_api.json", encoding="utf-8") as f:
    workflow = json.load(f)

result = queue_prompt(workflow)
print(result)   # {"prompt_id": "...", "number": 0, "node_errors": {}}

If it works, you will see the job appear in ComfyUI’s queue and the image land in ComfyUI/output/. The response contains the prompt_id you need for the next steps.

The client_id ties WebSocket messages to your script. Generate it once per session and reuse it.

Step 3: Change the Prompt and Seed From Code

Because the workflow is just a dictionary, you change it before sending. Look up the node IDs in your own exported file, because they differ between workflows.

import random

workflow["6"]["inputs"]["text"] = "a lighthouse at sunset, oil painting"
workflow["3"]["inputs"]["seed"] = random.randint(0, 2**32 - 1)
workflow["3"]["inputs"]["steps"] = 25

Change the seed every run. ComfyUI caches results. If you send exactly the same workflow twice, the second run finishes instantly and produces no new output.

To make scripts easier to maintain, find nodes by type or title instead of hard-coding IDs:

def find_node(workflow, class_type):
    for node_id, node in workflow.items():
        if node["class_type"] == class_type:
            return node_id
    raise KeyError(class_type)

sampler_id = find_node(workflow, "KSampler")

If a workflow has two nodes of the same type (for example positive and negative prompts), give them titles in ComfyUI. The title is stored in the exported file under _meta, so you can search for it.

Step 4: Wait for the Result Over WebSocket

The WebSocket sends live messages: status, execution_start, execution_cached, executing, progress and executed. The pattern from ComfyUI’s own websockets_api_example.py is to wait for an executing message whose node is null for your prompt:

import websocket  # pip install websocket-client

def wait_for(prompt_id):
    ws = websocket.WebSocket()
    ws.connect(f"ws://{SERVER}/ws?clientId={CLIENT_ID}")
    while True:
        msg = ws.recv()
        if isinstance(msg, str):
            message = json.loads(msg)
            if message["type"] == "progress":
                d = message["data"]
                print(f"step {d['value']}/{d['max']}")
            if message["type"] == "executing":
                data = message["data"]
                if data["node"] is None and data["prompt_id"] == prompt_id:
                    break   # finished
    ws.close()

Connect the WebSocket before you queue the prompt if the job is fast, or you may miss the finish message. Binary messages are preview images, which this loop skips.

If you do not want WebSockets, you can poll instead: call /history/{prompt_id} every second or two until it returns your prompt.

Step 5: Download the Images

Once the run finishes, /history/{prompt_id} lists the outputs of every node. Each image has a filename, subfolder and type, which you pass to /view:

import urllib.parse

def get_images(prompt_id):
    with urllib.request.urlopen(f"http://{SERVER}/history/{prompt_id}") as r:
        history = json.loads(r.read())[prompt_id]
    images = []
    for node_id, output in history["outputs"].items():
        for img in output.get("images", []):
            params = urllib.parse.urlencode({
                "filename": img["filename"],
                "subfolder": img["subfolder"],
                "type": img["type"],
            })
            with urllib.request.urlopen(f"http://{SERVER}/view?{params}") as r:
                images.append((img["filename"], r.read()))
    return images

Preview Image nodes return type: temp, and Save Image nodes return type: output. Videos from video nodes usually appear under a different key, such as gifs or videos, depending on the node pack, so print the history once to see what your workflow returns.

Complete Script

Putting it together, this script loads a workflow, sets a new prompt and seed, runs it and saves the images:

import json, random, uuid, urllib.request, urllib.parse
import websocket

SERVER = "127.0.0.1:8188"
CLIENT_ID = str(uuid.uuid4())

def queue_prompt(workflow):
    data = json.dumps({"prompt": workflow, "client_id": CLIENT_ID}).encode()
    req = urllib.request.Request(f"http://{SERVER}/prompt", data=data,
                                 headers={"Content-Type": "application/json"})
    return json.loads(urllib.request.urlopen(req).read())["prompt_id"]

def run(workflow):
    ws = websocket.WebSocket()
    ws.connect(f"ws://{SERVER}/ws?clientId={CLIENT_ID}")
    prompt_id = queue_prompt(workflow)
    while True:
        msg = ws.recv()
        if isinstance(msg, str):
            m = json.loads(msg)
            if m["type"] == "executing" and m["data"]["node"] is None \
               and m["data"]["prompt_id"] == prompt_id:
                break
    ws.close()
    with urllib.request.urlopen(f"http://{SERVER}/history/{prompt_id}") as r:
        outputs = json.loads(r.read())[prompt_id]["outputs"]
    saved = []
    for out in outputs.values():
        for img in out.get("images", []):
            q = urllib.parse.urlencode(img)
            with urllib.request.urlopen(f"http://{SERVER}/view?{q}") as r:
                name = f"api_{img['filename']}"
                with open(name, "wb") as f:
                    f.write(r.read())
                saved.append(name)
    return saved

workflow = json.load(open("workflow_api.json", encoding="utf-8"))
workflow["6"]["inputs"]["text"] = "a red fox in a misty forest, morning light"
workflow["3"]["inputs"]["seed"] = random.randint(0, 2**32 - 1)
print(run(workflow))

Change "6" and "3" to the IDs of the prompt and sampler nodes in your own file.

Uploading Input Images

For image-to-image, upscaling or editing workflows, the server needs your input image. Upload it to /upload/image as a multipart form with the field name image. The requests package makes this easy:

import requests  # pip install requests

with open("photo.png", "rb") as f:
    r = requests.post(f"http://{SERVER}/upload/image",
                      files={"image": ("photo.png", f, "image/png")},
                      data={"overwrite": "true"})
info = r.json()   # {"name": "photo.png", "subfolder": "", "type": "input"}

workflow["10"]["inputs"]["image"] = info["name"]   # your LoadImage node

The file is saved in ComfyUI/input/. There is also /upload/mask for masks. If ComfyUI runs on the same machine, you can simply copy the file into ComfyUI/input/ instead.

Batch Runs

To make many images, loop over prompts or seeds and queue each one. ComfyUI runs them one after another from its queue.

prompts = ["a cabin in snow", "a beach at dawn", "a neon city street"]
for text in prompts:
    wf = json.loads(json.dumps(workflow))   # fresh copy each time
    wf["6"]["inputs"]["text"] = text
    wf["3"]["inputs"]["seed"] = random.randint(0, 2**32 - 1)
    print(text, run(wf))

Make a fresh copy of the workflow each time, so one change does not leak into the next. For speed, you can queue them all first and then collect results by prompt_id, rather than waiting for each.

Set a filename_prefix on the Save Image node from code (for example the prompt name) to keep the output folder tidy.

Useful Endpoints

These are from the ComfyUI server routes docs:

Route Method Use
/prompt POST Queue a workflow
/prompt GET Current queue status
/history/{prompt_id} GET Outputs of one run
/view GET Download an image by filename, subfolder and type
/upload/image POST Upload an input image
/queue GET / POST See or clear the queue
/interrupt POST Stop the current run
/free POST Unload models to free memory
/object_info GET Every node type with its inputs (handy for building UIs)
/models/{folder} GET List model files in a folder, such as checkpoints
/system_stats GET Python version, devices and VRAM

Calling ComfyUI From Another Machine

By default, ComfyUI only listens on your own PC (127.0.0.1). To call it from another device on your network:

  • Start ComfyUI with --listen (or --listen 0.0.0.0) so it accepts connections from other machines.
  • Use --port to change the port if 8188 is taken.
  • If a web page on another address calls the API from the browser, add --enable-cors-header.
  • Allow the port through your firewall.

Our ComfyUI command line arguments guide lists every flag.

Security warning: the ComfyUI API has no login. Anyone who can reach the port can run workflows, use your GPU and read your outputs. Never open it directly to the internet. Use it on your home network, through a VPN, or behind a reverse proxy with a password.

If you would rather not run your own server, Comfy also offers a hosted Comfy Cloud, and there are wrappers that turn a workflow into a ready REST service.

Troubleshooting

HTTP 400 “prompt_outputs_failed_validation”

The workflow is invalid. The response’s node_errors says which node and input. Common causes: you sent the normal workflow file instead of the API export, a model file name does not exist on the server, or a value is outside its allowed range.

“Invalid prompt” or missing class_type

You sent the UI format. Re-export with Export (API). The API file has node IDs as top-level keys, each with class_type.

The run finishes instantly with no new image

Nothing changed, so ComfyUI used its cache. Change the seed or another input each run.

Connection refused

ComfyUI is not running, is on another port, or is only listening locally. Check the address in the ComfyUI console and use --listen for remote calls.

A custom node class is missing

The server you call must have the same custom nodes installed as the machine where you built the workflow. Install them with ComfyUI Manager and restart.

The WebSocket loop never ends

Check you used the same client_id in the WebSocket URL and in the /prompt body, and that you compare against the right prompt_id. If the run errored, look for an execution_error message too.

FAQ

Does ComfyUI have an API?

Yes. Every ComfyUI server has a built-in HTTP and WebSocket API on the same port as the web interface, 8188 by default.

How do I get the API format workflow?

Use Workflow, Export (API) in the menu. In older builds, enable dev mode in settings and use Save (API Format).

Do I need to install anything to use the API?

Not on the ComfyUI side. Your script needs Python and, for live progress, the websocket-client package.

Can I use the API from JavaScript or another language?

Yes. It is plain HTTP and WebSocket, so any language works. The JSON payloads are the same.

Can I run several jobs at once?

You can queue as many as you like, but a single ComfyUI server runs them one at a time. For parallel jobs, run more servers on more GPUs. Our ComfyUI multi-GPU guide covers this.

Is it safe to expose the API to the internet?

No. It has no authentication. Keep it on a private network or put it behind a password-protected proxy.

Sources

Free weekly email

Get new AI guides every week

One short weekly email with our newest local AI, ComfyUI and AI tool guides. No spam, no pop-ups, just the good stuff.

Free. Unsubscribe anytime in one click. We only use your email to send this newsletter. See our privacy policy.

Smiling woman coding on a laptop with images generating on a second screen, API card, hook automate ComfyUI with Python