ComfyUI API Python: ejecuta flujos de trabajo, endpoints y ejemplos
ComfyUI ya ejecuta un servidor HTTP, así que Python puede controlarlo de principio a fin. Exporta un flujo de trabajo en formato API, ponlo en cola, sigue el progreso por WebSocket, descarga las imágenes y evita los errores que rompen los scripts desatendidos. Incluye una clase cliente reutilizable y un bucle por lotes.
ComfyUI parece una mesa de dibujo para grafos de nodos, pero bajo el lienzo es un servidor HTTP normal. Cada botón que pulsas en el navegador llama a un endpoint, y un script de Python puede llamar exactamente a los mismos endpoints. Esa es la idea central de la API de ComfyUI con Python: exportar un flujo de trabajo como JSON, cambiar dos o tres valores, enviarlo por POST a /prompt y después recoger las imágenes terminadas. Sin pestañas del navegador, sin clics y sin tener que vigilarlo. Este artículo muestra los endpoints reales, un oyente WebSocket para el progreso en vivo y ejemplos funcionales que puedes pegar en un archivo y ejecutar en tu propio equipo.
Por qué automatizar ComfyUI
Hacer una imagen a mano está bien. Hacer doscientas fotos de producto, ejecutar cada noche un trabajo de miniaturas o dejar que tus clientes pulsen un botón en tu propia aplicación es otra historia, y el lienzo no puede hacer nada de eso. Un servidor de ComfyUI headless sí puede, y Python es el camino más corto para llegar ahí. Como beneficio extra, tus prompts, semillas y ajustes acaban en un repositorio Git en lugar de en una carpeta de capturas de pantalla.
La API se gana su lugar en tres situaciones:
Trabajo por lotes: cientos de prompts, una sola plantilla, cero clics manuales.
Productos: tu propia aplicación envía una petición y recibe una imagen de vuelta.
Automatización: una tarea cron, un bot de chat o un paso de CI que genera recursos según un calendario.
Qué expone el servidor
Inicia ComfyUI de la forma habitual (python main.py) y escuchará en 127.0.0.1:8188. Añade --listen 0.0.0.0 para aceptar conexiones de otras máquinas y --port para cambiar el puerto. Estos son los endpoints que más vas a usar:
Método
Endpoint
Qué hace
POST
/prompt
Pone en cola un flujo de trabajo y devuelve un prompt_id
GET
/history/{prompt_id}
Devuelve las salidas y el estado cuando termina la ejecución
GET
/view
Descarga una imagen por filename, subfolder y type
POST
/upload/image
Coloca una imagen en la carpeta de entrada de ComfyUI
GET
/queue
Lista los prompts en ejecución y pendientes
POST
/interrupt
Detiene el prompt que se está ejecutando en este momento
GET
/object_info
Describe cada clase de nodo y sus entradas
GET
/system_stats
Informa de los datos de VRAM, RAM y del dispositivo
WebSocket
/ws?clientId=...
Transmite eventos en vivo a tu cliente
El ritmo nunca cambia: poner en cola, esperar, recuperar. Envías un grafo por POST, esperas (consultando o escuchando por WebSocket) y descargas lo que generó el grafo.
Exporta tu flujo de trabajo en formato API
Primero construye y prueba el grafo en el lienzo. Cuando genere la imagen que quieres, expórtalo. Tu script no puede ejecutar el archivo de flujo normal, porque ese formato guarda las posiciones en el lienzo, los colores y la disposición de los widgets. El script necesita la versión ligera, en la que cada nodo se reduce a su clase y sus entradas.
Formato API frente a JSON normal
En las interfaces actuales, abre el menú Workflow y elige Export (API). En versiones antiguas, activa Dev mode options en la configuración y usa el botón Save (API Format). Guarda el resultado como workflow_api.json junto a tu script.
💡 Tip: Guarda los dos archivos. El JSON normal se vuelve a abrir en el lienzo para editarlo, mientras que el JSON de la API es lo que envía tu código.
Anatomía del JSON exportado
Abre el archivo y verás un diccionario plano. Cada entrada lleva el nombre de un ID de nodo (una cadena), y su valor contiene un class_type más las inputs del nodo:
Este ejemplo supone un checkpoint Flux Dev, por eso cfg está en 1.0. Los checkpoints de estilo Stable Diffusion 3.5 Large suelen necesitar un valor más alto, a menudo entre 4 y 8, así que copia los números de tu propia exportación y no de un tutorial.
Dos detalles importan. Los valores simples, como seed y steps, son los controles que cambias desde Python. Los valores como ["4", 0] son enlaces: el primer elemento es el ID del nodo de origen y el segundo es la ranura de salida que se lee. No toques los enlaces salvo que estés reconectando el grafo a propósito.
💡 Tip: Renombra tus nodos de prompt en el lienzo ("Positive Prompt", "Negative Prompt") antes de exportar. El nombre aparece en _meta.title, y tu código puede encontrar los nodos por título en lugar de por un número frágil.
Tu primera llamada desde Python
Con dos paquetes basta para todo lo de este artículo: pip install requests websocket-client. Guarda la exportación como workflow_api.json, inicia ComfyUI y ejecuta los fragmentos en orden.
Instalar y poner un prompt en cola
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 responde con un cuerpo JSON que contiene prompt_id y number, la posición en la cola. En este punto no se ha renderizado nada. El prompt solo fue aceptado. Guarda el ID, porque todas las llamadas posteriores lo necesitan.
Consultar el endpoint de historial
La forma más sencilla de saber que un prompt ha terminado es preguntar al endpoint de historial hasta que responda. Mientras la ejecución sigue en marcha, /history/{prompt_id} devuelve un objeto vacío.
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")
El diccionario outputs se indexa por ID de nodo. Cada nodo SaveImage informa de una lista llamada images, y cada imagen es un pequeño diccionario con filename, subfolder y type.
Descargar la imagen terminada
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)))
El diccionario de la imagen ya contiene los tres parámetros que espera /view, así que puede pasarse directamente como cadena de consulta. Los nodos SaveImage informan de type: "output", mientras que los nodos PreviewImage informan de temp, por eso el bucle filtra por ese campo.
Progreso en vivo por WebSocket
La consulta funciona, pero desperdicia llamadas y no dice nada hasta el final. ComfyUI también habla WebSocket, lo que ofrece un flujo en vivo: cambios en la cola, el nodo que se está ejecutando y un contador para cada paso del muestreador. En una aplicación web, esto es lo que impulsa la barra de progreso.
Conectar con un ID de cliente
Genera un UUID una sola vez y úsalo en dos lugares: la cadena de consulta clientId del socket y el campo client_id de tu petición /prompt. ComfyUI envía los eventos de un prompt solo al cliente que lo puso en cola. Si los IDs no coinciden, tu socket se quedará en silencio.
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"]
Los frames binarios transportan miniaturas de vista previa mientras trabaja el muestreador, así que el bucle omite todo lo que no sea texto. Si quieres vistas previas en vivo dentro de tu propia interfaz, decodifica esos frames en lugar de omitirlos.
Mensajes que recibirás
Tipo de mensaje
Significado
status
El tamaño de la cola ha cambiado
execution_start
Tu prompt salió de la cola y empezó a ejecutarse
execution_cached
Lista los nodos omitidos porque su resultado estaba en caché
executing
El nodo que se está ejecutando ahora; node: null significa que el grafo terminó
progress
Paso value de max del muestreador
executed
Un nodo produjo una salida, como nombres de archivo guardados
execution_error
Un nodo lanzó una excepción
execution_success
Todo el prompt se completó con éxito (versiones recientes)
El mensaje executing con node en null es la señal clásica de fin de ejecución. Las versiones recientes añaden execution_success, y gestionar ambos mantiene tu script funcionando entre versiones.
Envolverlo en una clase cliente
Las funciones sueltas bastan para una primera prueba. Cualquier cosa que se ejecute más de una vez merece una pequeña clase que guarde el host, el ID de cliente y las operaciones que repites.
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"]
Cambiar prompts y semillas de forma segura
Nunca escribas de forma fija IDs de nodo como "6" en un proyecto real. Si vuelves a exportar el grafo, los números pueden cambiar. Busca los nodos por clase y título, y edita siempre una copia de la plantilla para que un trabajo no se filtre al siguiente.
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))
Primero pon todo en cola y luego recoge los resultados. ComfyUI ejecuta los prompts uno a uno en el orden en que llegaron, así que la GPU nunca se queda parada mientras tu script descarga un archivo.
💡 Tip: ¿Necesitas 200 prompts en lugar de tres? Pide a un modelo de lenguaje como Claude Sonnet 5 o Gemini 3.5 Flash que los escriba como una lista JSON y pásala directamente al bucle de arriba.
Subir imágenes para ediciones
Los grafos de imagen a imagen, inpainting y ControlNet empiezan con un nodo LoadImage. Ese nodo lee de la carpeta de entrada de ComfyUI, así que sube primero el archivo y apunta el nodo al nombre devuelto.
name = client.upload("portrait.png")
wf = copy.deepcopy(template)
wf[find_node(wf, "LoadImage")]["inputs"]["image"] = name
pid = client.queue(wf)
Aquí es donde la automatización se acerca al trabajo de efectos visuales. Eliminar objetos, cambiar fondos y reiluminar son el mismo bucle: sube una imagen de origen, define una máscara y un prompt, pon en cola y recupera. Envuélvelo en una función y una carpeta con 500 fotos se convierte en un solo comando.
Errores de producción que conviene evitar
Los scripts que funcionan en tu equipo portátil fallan de maneras previsibles cuando se ejecutan desatendidos. Tres problemas explican la mayoría de las consultas de soporte.
Los prompts en caché devuelven resultados al instante
ComfyUI guarda en caché los resultados de los nodos según sus entradas. Si pones en cola dos veces exactamente el mismo grafo, la segunda ejecución no ejecuta nada, así que obtienes la misma imagen en milisegundos. La opción "randomize seed after each run" solo existe en la interfaz del navegador. El JSON de la API contiene un número fijo, así que tu código tiene que elegir una semilla nueva cada vez que quiera una imagen nueva.
Leer node_errors correctamente
Cuando falla la validación, /prompt responde con HTTP 400 y un cuerpo que contiene error y node_errors. El segundo campo indica el ID de nodo y la entrada exactos que fallan, por ejemplo un nombre de archivo de checkpoint que no está instalado en esta máquina. Muestra el cuerpo completo, no solo el código de estado. Recuerda también que un prompt puede pasar la validación y fallar igualmente durante la ejecución; en ese caso, la entrada del historial muestra status_str: "error".
Nunca expongas el puerto 8188
ComfyUI no incluye inicio de sesión. Cualquiera que pueda llegar al puerto puede poner trabajos en cola, leer tu carpeta de salida y llamar a /object_info. Los nodos personalizados son Python normal y se ejecutan con los permisos de tu usuario. Vincúlalo a 127.0.0.1, o colócalo detrás de un proxy inverso con autenticación o una VPN. Si una aplicación de navegador en otro origen debe llamarlo, pasa --enable-cors-header con ese único origen en lugar de un comodín.
💡 Tip: ¿Te quedas sin VRAM después de muchos checkpoints distintos? Envía por POST {"unload_models": true, "free_memory": true} a /free entre lotes para liberar memoria sin reiniciar el servidor.
Prescindir del servidor con PicassoIA
No todos los proyectos necesitan una máquina con GPU, un entorno de Python y una cola que vigilar. Si tu objetivo es simplemente obtener buenas imágenes a partir de texto o de fotos de referencia, PicassoIA ejecuta modelos comparables en el navegador. Así encajan los modelos con los trabajos habituales de ComfyUI:
Escribe tu prompt. Nombra el sujeto, la luz y la lente, igual que lo harías en un nodo de texto de ComfyUI.
Elige una relación de aspecto. Por defecto es 1:1, 16:9 sirve para banners y match_input_image mantiene la forma de una foto subida.
Elige una resolución. Por defecto es 1 MP, y el modelo admite hasta 4 MP, aunque se recomienda 2 MP o menos.
Añade hasta 8 imágenes de entrada si quieres que el resultado siga un estilo, un rostro o una foto de producto.
Define el formato de salida (WebP, JPG o PNG) y pulsa generar. Reutiliza la semilla después para recrear el mismo resultado.
Ajuste
Valor por defecto
Consejo práctico
Resolución
1 MP
Quédate en 2 MP o menos para obtener los mejores resultados
Calidad de salida
80
Rango de 0 a 100, se ignora en PNG
Tolerancia de seguridad
2
1 es la más estricta, 5 la más permisiva
Semilla
Aleatoria
Fíjala para reproducir una imagen exactamente
💡 Tip: Los hábitos que has adquirido arriba se transfieren directamente. Semillas fijas para resultados repetibles, un solo cambio por ejecución y prompts cortos que nombren la luz y la lente funcionan igual en ambas plataformas.
Crea tus propias imágenes hoy
Ya tienes el ciclo completo: exporta el grafo, ponlo en cola, escucha el socket y descarga los archivos. Ejecuta el primer fragmento esta noche y tendrás una imagen en disco antes de que se enfríe el café. Luego ajusta la clase cliente, añade la lógica de semillas y deja que un lote se ejecute mientras haces otra cosa.
Y si prefieres saltarte la configuración, abre PicassoIA, elige Flux Dev o Flux 2 Pro y escribe el primer prompt que se te ocurra. Cambia un ajuste, vuelve a generar y compara. Cinco minutos de experimentos te enseñarán más sobre prompts, semillas y relaciones de aspecto que cualquier cantidad de lectura.