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 API Python: Run Workflows, Endpoints and Examples
Cristian Da Conceicao
Founder of Picasso IA

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.

Hands typing Python code on a laptop beside a cup of coffee

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.

Open PC case with a large graphics card on a wooden workbench

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:

MethodEndpointWhat it does
POST/promptQueues a workflow and returns a prompt_id
GET/history/{prompt_id}Returns outputs and status once the run is finished
GET/viewDownloads an image by filename, subfolder and type
POST/upload/imagePuts an image into ComfyUI's input folder
GET/queueLists running and pending prompts
POST/interruptStops the prompt that is running right now
GET/object_infoDescribes every node class and its inputs
GET/system_statsReports 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.

Engineer studying a node diagram on a large office monitor

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:

{
  "3": {
    "class_type": "KSampler",
    "inputs": {
      "seed": 421337,
      "steps": 20,
      "cfg": 1.0,
      "sampler_name": "euler",
      "scheduler": "simple",
      "denoise": 1.0,
      "model": ["4", 0],
      "positive": ["6", 0],
      "negative": ["7", 0],
      "latent_image": ["5", 0]
    }
  },
  "4": {
    "class_type": "CheckpointLoaderSimple",
    "inputs": { "ckpt_name": "flux1-dev-fp8.safetensors" }
  },
  "6": {
    "class_type": "CLIPTextEncode",
    "inputs": { "text": "a lighthouse at dawn", "clip": ["4", 1] },
    "_meta": { "title": "Positive Prompt" }
  }
}

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.

Overhead view of a desk with a laptop, notebook diagram and coffee

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.

Technician checking cables in a narrow server room

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 typeMeaning
statusThe queue size changed
execution_startYour prompt left the queue and began running
execution_cachedLists nodes skipped because their result was cached
executingThe node now running; node: null means the graph finished
progressSampler step value out of max
executedA node produced output, such as saved filenames
execution_errorA node raised an exception
execution_successThe 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"]

Studio wall hung with pinned product photo prints

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)

Retoucher repainting part of a portrait on a pen display

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.

Developer working alone at night under a warm desk lamp

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:

JobModelWhy choose it
Everyday text to imageFlux Dev12B parameters, 11 aspect ratios up to 21:9, img2img mode
Reference-based shots and editsFlux 2 ProUp to 8 reference images, outputs up to 4 MP
Fast draftsFlux SchnellQuick previews before a final render
Inpainting and object removalFlux Fill ProRepaints only the area you mask
Edge and depth controlFlux Canny Pro and Flux Depth ProKeeps the layout of a source image
Another model familyStable Diffusion 3.5 LargeDifferent look, same workflow

Generate in Six Steps

Designer holding a printed mountain lake photo beside a monitor

Here is the whole process with Flux 2 Pro, the model that comes closest to a ComfyUI reference-image workflow:

  1. Open the Flux 2 Pro page on Picasso IA.
  2. Write your prompt. Name the subject, the light and the lens, the same way you would in a ComfyUI text node.
  3. Pick an aspect ratio. The default is 1:1, 16:9 suits banners, and match_input_image keeps the shape of an uploaded photo.
  4. Choose a resolution. The default is 1 MP, and the model accepts up to 4 MP, though 2 MP or below is recommended.
  5. Add up to 8 input images if you want the result to follow a style, a face or a product shot.
  6. Set the output format (WebP, JPG or PNG), then press generate. Reuse the seed later to recreate the same result.
SettingDefaultPractical advice
Resolution1 MPStay at 2 MP or below for the best results
Output quality80Range 0 to 100, ignored for PNG
Safety tolerance21 is strictest, 5 is most permissive
SeedRandomFix 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.

Share this article