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 API Python: ejecuta flujos de trabajo, endpoints y ejemplos
Cristian Da Conceicao
Fundador de Picasso IA

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.

Manos escribiendo código Python en un equipo portátil junto a una taza de café

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.

Caja de PC abierta con una tarjeta gráfica grande sobre una mesa de trabajo de madera

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étodoEndpointQué hace
POST/promptPone 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/viewDescarga una imagen por filename, subfolder y type
POST/upload/imageColoca una imagen en la carpeta de entrada de ComfyUI
GET/queueLista los prompts en ejecución y pendientes
POST/interruptDetiene el prompt que se está ejecutando en este momento
GET/object_infoDescribe cada clase de nodo y sus entradas
GET/system_statsInforma 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.

Ingeniero estudiando un diagrama de nodos en un monitor grande de oficina

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:

{
  "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" }
  }
}

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.

Vista cenital de un escritorio con un equipo portátil, un diagrama en una libreta y una taza de café

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.

Técnico revisando cables en una sala de servidores estrecha

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 mensajeSignificado
statusEl tamaño de la cola ha cambiado
execution_startTu prompt salió de la cola y empezó a ejecutarse
execution_cachedLista los nodos omitidos porque su resultado estaba en caché
executingEl nodo que se está ejecutando ahora; node: null significa que el grafo terminó
progressPaso value de max del muestreador
executedUn nodo produjo una salida, como nombres de archivo guardados
execution_errorUn nodo lanzó una excepción
execution_successTodo 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"]

Pared de estudio con copias de fotos de producto clavadas

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)

Retocador repintando parte de un retrato en una pantalla de dibujo con lápiz

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.

Desarrollador trabajando solo de noche bajo la luz cálida de una lámpara de escritorio

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:

TrabajoModeloPor qué elegirlo
Texto a imagen cotidianoFlux Dev12B parámetros, 11 relaciones de aspecto hasta 21:9, modo img2img
Tomas y ediciones con referenciasFlux 2 ProHasta 8 imágenes de referencia, salidas de hasta 4 MP
Borradores rápidosFlux SchnellVistas previas rápidas antes de un render final
Inpainting y eliminación de objetosFlux Fill ProRepinta solo el área que enmascaras
Control de bordes y profundidadFlux Canny Pro y Flux Depth ProMantiene la composición de una imagen de origen
Otra familia de modelosStable Diffusion 3.5 LargeAspecto distinto, mismo flujo de trabajo

Generar en seis pasos

Diseñador sosteniendo una foto impresa de un lago de montaña junto a un monitor

Este es el proceso completo con Flux 2 Pro, el modelo que más se acerca a un flujo de trabajo de ComfyUI con imágenes de referencia:

  1. Abre la página de Flux 2 Pro en PicassoIA.
  2. Escribe tu prompt. Nombra el sujeto, la luz y la lente, igual que lo harías en un nodo de texto de ComfyUI.
  3. 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.
  4. Elige una resolución. Por defecto es 1 MP, y el modelo admite hasta 4 MP, aunque se recomienda 2 MP o menos.
  5. Añade hasta 8 imágenes de entrada si quieres que el resultado siga un estilo, un rostro o una foto de producto.
  6. Define el formato de salida (WebP, JPG o PNG) y pulsa generar. Reutiliza la semilla después para recrear el mismo resultado.
AjusteValor por defectoConsejo práctico
Resolución1 MPQuédate en 2 MP o menos para obtener los mejores resultados
Calidad de salida80Rango de 0 a 100, se ignora en PNG
Tolerancia de seguridad21 es la más estricta, 5 la más permisiva
SemillaAleatoriaFí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.

Compartir este artículo

Elige tu idioma