Generate imagesLarge Language ModelsVisual Effects
ComfyUI API Python: Run Workflows, Endpoints and Examples
ComfyUI already runs an HTTP server, so Python can drive it end to end. Export a workflow in API format, queue it, follow progress over WebSocket, download the images, and avoid the mistakes that break unattended scripts. Includes a reusable client class and a batch loop.
ComfyUI looks like a drawing board for node graphs, but under the canvas it is an ordinary HTTP server. Every button you press in the browser calls an endpoint, and a Python script can call the very same endpoints. That is the whole idea behind the ComfyUI API with Python: export a workflow as JSON, change two or three values, POST it to /prompt, then collect the finished images. No browser tab, no clicking, no babysitting. This article shows the real endpoints, a WebSocket listener for live progress, and working examples you can paste into a file and run on your own machine.
Why Script ComfyUI at All
Making one image by hand is fine. Making two hundred product shots, running a nightly thumbnail job, or letting customers press a button in your own app is a different story, and the canvas cannot do any of it. A headless ComfyUI server can, and Python is the shortest road there. As a bonus, your prompts, seeds and settings end up in a Git repository instead of a screenshot folder.
The API earns its place in three situations:
Batch work: hundreds of prompts, one template, zero manual clicks.
Products: your own app sends a request and gets an image back.
Automation: a cron job, a chat bot or a CI step that renders assets on a schedule.
What the Server Exposes
Start ComfyUI the usual way (python main.py) and it listens on 127.0.0.1:8188. Add --listen 0.0.0.0 to accept connections from other machines and --port to change the port. These are the endpoints you will touch most often:
Method
Endpoint
What it does
POST
/prompt
Queues a workflow and returns a prompt_id
GET
/history/{prompt_id}
Returns outputs and status once the run is finished
GET
/view
Downloads an image by filename, subfolder and type
POST
/upload/image
Puts an image into ComfyUI's input folder
GET
/queue
Lists running and pending prompts
POST
/interrupt
Stops the prompt that is running right now
GET
/object_info
Describes every node class and its inputs
GET
/system_stats
Reports VRAM, RAM and device details
WebSocket
/ws?clientId=...
Streams live events for your client
The rhythm never changes: queue, wait, fetch. You POST a graph, you wait (by polling or by listening on the WebSocket), and you download what the graph produced.
Export Your Workflow in API Format
Build and test the graph on the canvas first. When it makes the image you want, export it. Your script cannot run the normal workflow file, because that format stores canvas positions, colors and widget layout. The script needs the lean version, where every node is reduced to its class and its inputs.
API Format vs Normal JSON
In current frontends, open the Workflow menu and choose Export (API). On older builds, switch on Dev mode options in the settings and use the Save (API Format) button. Save the result as workflow_api.json next to your script.
💡 Tip: Keep both files. The normal JSON reopens on the canvas for editing, while the API JSON is what your code sends.
Anatomy of the Exported JSON
Open the file and you will see a flat dictionary. Every entry is named after a node ID (a string), and its value holds a class_type plus the node's inputs:
This example assumes a Flux Dev checkpoint, which is why cfg sits at 1.0. Stable Diffusion 3.5 Large style checkpoints usually want a higher value, often between 4 and 8, so copy the numbers from your own export rather than from a tutorial.
Two details matter. Plain values such as seed and steps are the knobs you change from Python. Values such as ["4", 0] are links: the first item is the ID of the source node, the second is the output slot to read. Leave links alone unless you are deliberately rewiring the graph.
💡 Tip: Rename your prompt nodes on the canvas ("Positive Prompt", "Negative Prompt") before exporting. The name lands in _meta.title, and your code can find nodes by title instead of by a fragile number.
Your First Python Call
Two packages are enough for everything in this article: pip install requests websocket-client. Save the export as workflow_api.json, start ComfyUI, and run the snippets in order.
Install and Queue a Prompt
import json
import requests
SERVER = "http://127.0.0.1:8188"
with open("workflow_api.json", "r", encoding="utf-8") as f:
workflow = json.load(f)
# "6" is the positive CLIPTextEncode, "3" is the KSampler
workflow["6"]["inputs"]["text"] = "a lighthouse at dawn, 35mm photo, film grain"
workflow["3"]["inputs"]["seed"] = 421337
response = requests.post(f"{SERVER}/prompt", json={"prompt": workflow})
response.raise_for_status()
prompt_id = response.json()["prompt_id"]
print("Queued:", prompt_id)
ComfyUI answers with a JSON body holding prompt_id and number, the position in the queue. Nothing has been rendered at this point. The prompt was only accepted. Hold on to the ID, because every later call needs it.
Poll the History Endpoint
The simplest way to know a prompt is finished is to ask the history endpoint until it answers. While the run is still going, /history/{prompt_id} returns an empty object.
import time
def wait_for_outputs(prompt_id, timeout=300):
started = time.time()
while time.time() - started < timeout:
history = requests.get(f"{SERVER}/history/{prompt_id}").json()
if prompt_id in history:
return history[prompt_id]["outputs"]
time.sleep(1)
raise TimeoutError(f"Prompt {prompt_id} took longer than {timeout}s")
The outputs dictionary is indexed by node ID. Each SaveImage node reports a list called images, and every image is a small dictionary with a filename, a subfolder and a type.
Download the Finished Image
import os
def download_images(outputs, folder="renders"):
os.makedirs(folder, exist_ok=True)
saved = []
for node_id, node_output in outputs.items():
for image in node_output.get("images", []):
if image["type"] != "output":
continue # skip PreviewImage temp files
data = requests.get(f"{SERVER}/view", params=image).content
path = os.path.join(folder, image["filename"])
with open(path, "wb") as f:
f.write(data)
saved.append(path)
return saved
print(download_images(wait_for_outputs(prompt_id)))
The image dictionary already holds the three parameters /view expects, so it can be passed straight in as the query string. SaveImage nodes report type: "output", while PreviewImage nodes report temp, which is why the loop filters on it.
Live Progress Over WebSocket
Polling works, but it wastes calls and says nothing until the very end. ComfyUI also speaks WebSocket, which gives you a live feed: queue changes, the node that is running, and a counter for every sampler step. In a web app, this is what drives the progress bar.
Connect With a Client ID
Generate a UUID once and use it in two places: the clientId query string of the socket and the client_id field of your /prompt request. ComfyUI sends the events of a prompt only to the client that queued it. Mismatch the IDs and your socket stays silent.
import json
import uuid
import requests
import websocket # pip install websocket-client
HOST = "127.0.0.1:8188"
CLIENT_ID = str(uuid.uuid4())
def run_with_progress(workflow):
ws = websocket.WebSocket()
ws.connect(f"ws://{HOST}/ws?clientId={CLIENT_ID}")
payload = {"prompt": workflow, "client_id": CLIENT_ID}
r = requests.post(f"http://{HOST}/prompt", json=payload)
r.raise_for_status()
prompt_id = r.json()["prompt_id"]
while True:
message = ws.recv()
if isinstance(message, bytes):
continue # binary frames are preview thumbnails
event = json.loads(message)
kind, data = event["type"], event["data"]
if kind == "progress":
print(f"step {data['value']}/{data['max']}")
elif kind == "execution_error":
raise RuntimeError(data.get("exception_message", "node failed"))
elif data.get("prompt_id") == prompt_id and (
kind == "execution_success"
or (kind == "executing" and data["node"] is None)
):
break
ws.close()
history = requests.get(f"http://{HOST}/history/{prompt_id}").json()
return history[prompt_id]["outputs"]
Binary frames carry preview thumbnails while the sampler works, so the loop skips anything that is not text. If you want live previews inside your own interface, decode those frames instead of skipping them.
Messages You Will Receive
Message type
Meaning
status
The queue size changed
execution_start
Your prompt left the queue and began running
execution_cached
Lists nodes skipped because their result was cached
executing
The node now running; node: null means the graph finished
progress
Sampler step value out of max
executed
A node produced output, such as saved filenames
execution_error
A node raised an exception
execution_success
The whole prompt succeeded (newer builds)
The executing message with node set to null is the classic end-of-run signal. Newer builds add execution_success, and handling both keeps your script working across versions.
Wrap It in a Client Class
Loose functions are fine for a first test. Anything that runs more than once deserves a small class that stores the host, the client ID and the operations you keep repeating.
import time
import uuid
import requests
class ComfyClient:
def __init__(self, host="127.0.0.1:8188"):
self.host = host
self.client_id = str(uuid.uuid4())
def queue(self, workflow):
r = requests.post(
f"http://{self.host}/prompt",
json={"prompt": workflow, "client_id": self.client_id},
)
if r.status_code != 200:
raise RuntimeError(r.text) # includes node_errors
return r.json()["prompt_id"]
def result(self, prompt_id, timeout=300):
deadline = time.time() + timeout
while time.time() < deadline:
history = requests.get(f"http://{self.host}/history/{prompt_id}").json()
if prompt_id in history:
return history[prompt_id]
time.sleep(1)
raise TimeoutError(prompt_id)
def fetch(self, image):
r = requests.get(f"http://{self.host}/view", params=image)
r.raise_for_status()
return r.content
def upload(self, path):
with open(path, "rb") as f:
r = requests.post(
f"http://{self.host}/upload/image",
files={"image": f},
data={"overwrite": "true"},
)
r.raise_for_status()
return r.json()["name"]
Swap Prompts and Seeds Safely
Never hard-code node IDs like "6" in a real project. Re-export the graph, and the numbers can change. Look nodes up by class and title instead, and always edit a copy of the template so one job cannot leak into the next.
import copy
import json
import random
def find_node(workflow, class_type, title=None):
for node_id, node in workflow.items():
if node["class_type"] != class_type:
continue
if title is None or node.get("_meta", {}).get("title") == title:
return node_id
raise LookupError(f"{class_type} {title or ''} not found")
def build(template, prompt, seed=None):
wf = copy.deepcopy(template)
wf[find_node(wf, "CLIPTextEncode", "Positive Prompt")]["inputs"]["text"] = prompt
wf[find_node(wf, "KSampler")]["inputs"]["seed"] = (
seed if seed is not None else random.randint(0, 2**32 - 1)
)
return wf
template = json.load(open("workflow_api.json", encoding="utf-8"))
client = ComfyClient()
prompts = [
"ceramic teapot on a linen cloth, soft window light",
"walnut desk with a fountain pen, low morning sun",
"leather boots on wet cobblestones, overcast sky",
]
ids = [client.queue(build(template, p)) for p in prompts] # queue everything first
for pid in ids:
entry = client.result(pid)
for out in entry["outputs"].values():
for image in out.get("images", []):
with open(image["filename"], "wb") as f:
f.write(client.fetch(image))
Queue everything first, then collect. ComfyUI runs prompts one at a time in the order they arrived, so the GPU never sits idle while your script downloads a file.
💡 Tip: Need 200 prompts rather than three? Ask a language model such as Claude Sonnet 5 or Gemini 3.5 Flash to write them as a JSON list, then feed that list straight into the loop above.
Upload Images for Image Edits
Image to image, inpainting and ControlNet graphs start with a LoadImage node. That node reads from ComfyUI's input folder, so upload the file first and point the node at the returned name.
name = client.upload("portrait.png")
wf = copy.deepcopy(template)
wf[find_node(wf, "LoadImage")]["inputs"]["image"] = name
pid = client.queue(wf)
This is where scripting gets close to visual effects work. Object removal, background swaps and relighting are all the same loop: upload a source image, set a mask and a prompt, queue, fetch. Wrap it in a function and a folder of 500 photos becomes one command.
Production Mistakes to Avoid
Scripts that work on your laptop break in predictable ways once they run unattended. Three problems account for most of the support questions.
Cached Prompts Return Instantly
ComfyUI caches node results by their inputs. Queue the exact same graph twice and the second run executes nothing, so you get the same image back in milliseconds. The "randomize seed after each run" option exists only in the browser interface. The API JSON holds a fixed number, so your code has to pick a new seed whenever it wants a new image.
Reading node_errors Properly
When validation fails, /prompt answers with HTTP 400 and a body containing error and node_errors. The second field names the exact node ID and input at fault, for example a checkpoint filename that is not installed on this machine. Print the whole body, not just the status code. Also remember that a prompt can pass validation and still fail during execution, in which case the history entry shows status_str: "error".
Never Expose Port 8188
ComfyUI ships with no login. Anyone who can reach the port can queue jobs, read your output folder and call /object_info. Custom nodes are plain Python and run with your user's permissions. Bind to 127.0.0.1, or put the server behind a reverse proxy with authentication or a VPN. If a browser app on another origin must call it, pass --enable-cors-header with that single origin rather than a wildcard.
💡 Tip: Running low on VRAM after many different checkpoints? POST {"unload_models": true, "free_memory": true} to /free between batches to release memory without restarting the server.
Skip the Server With Picasso IA
Not every project needs a GPU box, a Python environment and a queue to babysit. If your goal is simply good images from text or reference photos, Picasso IA runs comparable models in the browser. Here is how the models line up with common ComfyUI jobs:
Write your prompt. Name the subject, the light and the lens, the same way you would in a ComfyUI text node.
Pick an aspect ratio. The default is 1:1, 16:9 suits banners, and match_input_image keeps the shape of an uploaded photo.
Choose a resolution. The default is 1 MP, and the model accepts up to 4 MP, though 2 MP or below is recommended.
Add up to 8 input images if you want the result to follow a style, a face or a product shot.
Set the output format (WebP, JPG or PNG), then press generate. Reuse the seed later to recreate the same result.
Setting
Default
Practical advice
Resolution
1 MP
Stay at 2 MP or below for the best results
Output quality
80
Range 0 to 100, ignored for PNG
Safety tolerance
2
1 is strictest, 5 is most permissive
Seed
Random
Fix it to reproduce an image exactly
💡 Tip: The habits you built above transfer directly. Fixed seeds for repeatable results, one change per run, and short prompts that name light and lens work the same way on both platforms.
Make Your Own Images Today
You now have the full loop: export the graph, queue it, listen on the socket, fetch the files. Run the first snippet tonight and you will have an image on disk before your coffee goes cold. Then tighten the client class, add the seed logic, and let a batch run while you do something else.
And if you would rather skip the setup, open Picasso IA, pick Flux Dev or Flux 2 Pro, and type the first prompt that comes to mind. Change one setting, generate again, and compare. Five minutes of experiments will show you more about prompts, seeds and aspect ratios than any amount of reading.