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.
- Export: open your workflow, then Workflow, Export (API). You get a JSON file keyed by node ID.
- Run: POST
{"prompt": workflow, "client_id": id}tohttp://127.0.0.1:8188/prompt. It returns aprompt_id. - Wait: listen on
ws://127.0.0.1:8188/ws?clientId=id. The run is done when anexecutingmessage hasnodeset 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:
- Send the workflow JSON to
POST /prompt. The server puts it in the queue and returns aprompt_id. - Listen on the
/wsWebSocket for progress, or poll/history/{prompt_id}. - When it finishes, read the outputs from
/historyand 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.
- Open the workflow in ComfyUI and run it once to make sure it works.
- Click Workflow in the top menu, then Export (API).
- 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
--portto 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
- ComfyUI docs: Server routes (endpoints and WebSocket message types)
- ComfyUI docs: API examples (payload, history, view, finish condition)
- ComfyUI script_examples (basic_api_example.py, websockets_api_example.py)



