Start hereQuick start · 6 you’ll reach for daily
Target versions · paceVersions
ComfyUI (main, May 2026)
torch ≥ 2.2
ComfyUI-Manager ≥ 3.0
python ≥ 3.10
ComfyUI is a node-graph runtime for diffusion models. Same models as Stable Diffusion via diffusers, but you wire components on a canvas instead of writing Python. The repo moves fast — expect breaking changes when you pull main. ComfyUI-Manager is the de-facto package manager; install it first, then add custom node packs from there. The server speaks plain HTTP + WebSocket — any language can drive it.
Install · launchSetup
# Clone + Python venv install
git clone https://github.com/comfyanonymous/ComfyUI.git
cd ComfyUI
python -m venv venv && source venv/bin/activate
# NVIDIA (CUDA 12)
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
pip install -r requirements.txt
# Apple Silicon — Metal backend
pip install torch torchvision torchaudio # nightly-MPS builds are usually fine
pip install -r requirements.txt
# Drop checkpoints / LoRAs / VAEs into:
# models/checkpoints/ models/loras/ models/vae/ models/controlnet/
# Launch (web UI at http://127.0.0.1:8188)
python main.py
# Listen on LAN: python main.py --listen 0.0.0.0
# Low VRAM (<6GB): python main.py --lowvram
# CPU only (slow): python main.py --cpu
# Auto-launch browser: python main.py --auto-launch
Canvas controlsUI & hotkeys
| Double-click canvas | Search and add a node. |
| Right-click node | Properties, bypass, mute, pin, color. |
| Ctrl+Enter / Queue Prompt | Run the workflow. |
| Ctrl+drag selection | Group multiselect. |
| Ctrl+G | Group selected nodes (reusable subgraph). |
| Ctrl+M | Mute / unmute a node — skip without disconnecting. |
| Ctrl+B | Bypass — pass input through as if the node weren’t there. |
| Ctrl+S / Save | Save the canvas as JSON. |
| Save (API Format) | Different file. Use this for HTTP / scripting. |
| Drag a PNG onto the canvas | Loads the workflow embedded in the image metadata. |
| Q / Queue panel | See pending and historical runs. |
Where files liveFolders & models
| models/checkpoints/ | .safetensors / .ckpt — SD, SDXL, SD3, Flux. |
| models/loras/ | LoRA / LoCon files. Picked up by LoraLoader nodes. |
| models/vae/ | Standalone VAE files. |
| models/clip/, models/clip_vision/ | Text encoders, vision encoders for IP-Adapter. |
| models/controlnet/ | ControlNet weights. |
| models/upscale_models/ | ESRGAN / Real-ESRGAN models for the Upscale nodes. |
| input/ | Files referenced by LoadImage nodes. |
| output/ | Where SaveImage writes results. |
| custom_nodes/ | Each subfolder is a node pack. Manager installs here. |
| extra_model_paths.yaml | Map external model folders (Automatic1111 share, NAS) into ComfyUI. |
The starter setCore nodes
Model + conditioning
| CheckpointLoaderSimple | Outputs MODEL, CLIP, VAE from one .safetensors. |
| CLIPTextEncode | Prompt → CONDITIONING. Use two: positive and negative. |
| CLIPTextEncodeSDXL | Dual-encoder version for SDXL with refiner-style params. |
| LoraLoader | Stack on top of MODEL+CLIP. Chain for multi-LoRA. |
| ControlNetLoader, ControlNetApply, ControlNetApplyAdvanced | Add a control map. Advanced = strength + start/end %. |
Latents + sampling
| EmptyLatentImage | Blank latent at WxH. Source for txt2img. |
| VAEEncode | Image → latent. Source for img2img. |
| VAEDecode | Latent → image. Almost always the last step. |
| KSampler | The denoising loop. Takes model, positive, negative, latent. |
| KSamplerAdvanced | Same but with start_at_step / end_at_step for base+refiner chains. |
| LatentUpscale, LatentUpscaleBy | Resize a latent before further sampling. Hi-res fix. |
Images + I/O
| LoadImage | Read from input/. Outputs IMAGE + MASK. |
| SaveImage | Write PNG to output/. Embeds the workflow in metadata. |
| PreviewImage | Show in UI without writing to disk. |
| ImageUpscaleWithModel | Run an ESRGAN-family upscaler. |
| ImageCompositeMasked, ImagePadForOutpaint | Compositing helpers for inpaint / outpaint flows. |
Inside KSamplerSamplers & schedulers
| sampler_name="dpmpp_2m" | Preferred DPM++ 2M. Strong default. |
| sampler_name="euler_ancestral" | Adds noise each step. Stylistic, varied. |
| sampler_name="uni_pc" | Great at low step counts. |
| sampler_name="lcm" | Pair with LCM-LoRA + 4–8 steps. |
| scheduler="karras" | Karras sigmas. Sharper outputs at low steps. |
| scheduler="exponential" / "normal" / "simple" | Other schedule shapes. Karras is the safe default. |
| steps=20…30 | SDXL plateaus around 30. Distilled models work at 4–8. |
| cfg=5…8 | Classifier-free guidance. Higher = follow prompt more. SD3/Flux want lower (~3.5). |
| denoise=1.0 | Full denoise = txt2img. 0.4–0.7 = img2img strength. |
| seed=-1 | -1 = random each run. Fix for reproducibility. |
Shape on the wireWorkflow JSON
Save (API Format) emits a flat dict keyed by node id. Each node has a class_type and an
inputs map. Wires are encoded as ["from_node_id", output_slot_index].
| "3": {"class_type":"KSampler","inputs":{…}} | Node entry. Key = id, class_type = node name. |
| "seed": 42, "steps": 25, "cfg": 7.0 | Scalar inputs — same widgets you see in the UI. |
| "model": ["4", 0] | Wire input. "Read output 0 of node 4." Order matters. |
| CheckpointLoader outputs: MODEL(0), CLIP(1), VAE(2) | Use those indices when wiring elsewhere. |
| Different export: Save (Default) | UI-only format with positions and links. Not accepted by the API. |
| Embed in PNG | SaveImage writes the canvas JSON into the PNG’s parameters chunk. |
Minimal txt2img workflow
{
"3": {
"class_type": "KSampler",
"inputs": {
"seed": 42,
"steps": 25,
"cfg": 7.0,
"sampler_name": "dpmpp_2m",
"scheduler": "karras",
"denoise": 1.0,
"model": ["4", 0],
"positive": ["6", 0],
"negative": ["7", 0],
"latent_image": ["5", 0]
}
},
"4": {
"class_type": "CheckpointLoaderSimple",
"inputs": { "ckpt_name": "sd_xl_base_1.0.safetensors" }
},
"5": {
"class_type": "EmptyLatentImage",
"inputs": { "width": 1024, "height": 1024, "batch_size": 1 }
},
"6": {
"class_type": "CLIPTextEncode",
"inputs": { "text": "cinematic photo of an old fisherman", "clip": ["4", 1] }
},
"7": {
"class_type": "CLIPTextEncode",
"inputs": { "text": "blurry, text, watermark", "clip": ["4", 1] }
},
"8": {
"class_type": "VAEDecode",
"inputs": { "samples": ["3", 0], "vae": ["4", 2] }
},
"9": {
"class_type": "SaveImage",
"inputs": { "filename_prefix": "out", "images": ["8", 0] }
}
}
Drive it from codeHTTP & WebSocket API
| POST /prompt | Queue a workflow. Body: {"prompt": …, "client_id": …}. Returns prompt_id. |
| GET /history/{prompt_id} | Status + outputs (filenames) of a run. |
| GET /history | Full history. Heavy — prefer per-id. |
| GET /queue | Pending + running prompts. |
| POST /interrupt | Stop the current run. |
| POST /queue { "clear": true } | Drain pending runs. |
| POST /upload/image | Multipart upload to input/. Returns name to pass into LoadImage. |
| GET /view?filename=&subfolder=&type= | Fetch a generated image. type = output / temp / input. |
| GET /object_info | Schema of every installed node. Great for editor tooling. |
| GET /system_stats | GPU / RAM utilization. |
| WS /ws?clientId=… | Live events: progress, executing, executed, execution_error. |
Queue + poll
import json, urllib.request, uuid
SERVER = "127.0.0.1:8188"
CLIENT_ID = str(uuid.uuid4())
# 1 · Load a workflow exported as API JSON (Settings → "Save (API Format)")
with open("workflow_api.json") as f:
workflow = json.load(f)
# 2 · Override values just like editing nodes in the UI
workflow["6"]["inputs"]["text"] = "a wide-angle photo of a foggy harbour at dawn"
workflow["3"]["inputs"]["seed"] = 123_456
# 3 · Queue the prompt
req = urllib.request.Request(
f"http://{SERVER}/prompt",
data=json.dumps({"prompt": workflow, "client_id": CLIENT_ID}).encode(),
headers={"Content-Type": "application/json"},
)
prompt_id = json.loads(urllib.request.urlopen(req).read())["prompt_id"]
print("queued:", prompt_id)
# 4 · Poll history → get the saved file
hist = json.loads(
urllib.request.urlopen(f"http://{SERVER}/history/{prompt_id}").read()
)[prompt_id]
for node_out in hist["outputs"].values():
for img in node_out.get("images", []):
url = f"http://{SERVER}/view?filename={img['filename']}&subfolder={img['subfolder']}&type={img['type']}"
print("image:", url)
Write · shareCustom nodes
| custom_nodes/<pack>/__init__.py | Entry file. ComfyUI imports it on startup. |
| NODE_CLASS_MAPPINGS = {"NodeName": NodeClass} | Register classes by their canonical name. |
| NODE_DISPLAY_NAME_MAPPINGS | Optional. Pretty name in the search palette. |
| @classmethod INPUT_TYPES(cls) | Declare typed inputs. Types: IMAGE, LATENT, MODEL, CLIP, VAE, INT, FLOAT, STRING, CONDITIONING, MASK… |
| RETURN_TYPES, RETURN_NAMES | Tuple of output types and labels. |
| FUNCTION = "run" | Name of the instance method ComfyUI calls. |
| CATEGORY = "image/filters" | Where it sits in the search tree. |
| IS_CHANGED = staticmethod(…) | Override cache invalidation. Return changing value to force re-run. |
| requirements.txt next to __init__.py | Manager installs deps from this on add. |
Minimal node
# custom_nodes/my_pack/__init__.py
class GrayscaleNode:
"""Convert an image batch to grayscale."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "run"
CATEGORY = "image/filters"
def run(self, image, strength):
# image is a torch.Tensor of shape [B, H, W, C], values in 0..1
gray = image.mean(dim=-1, keepdim=True).repeat(1, 1, 1, 3)
return (gray * strength + image * (1 - strength),)
NODE_CLASS_MAPPINGS = {"Grayscale": GrayscaleNode}
NODE_DISPLAY_NAME_MAPPINGS = {"Grayscale": "Grayscale (mine)"}
Install · updateManager & ecosystem
| ComfyUI-Manager | Drop into custom_nodes/ first. UI button installs everything else. |
| Manager → Install Custom Nodes | Browse + install community packs without git clone. |
| Manager → Install Missing | Load a workflow with unknown nodes → auto-install them. |
| Manager → Model Manager | Browse + download checkpoints / VAEs / LoRAs from Hub + Civitai. |
| ComfyUI-Impact-Pack | Detailer / face-fix / segmentation. Most workflows use it. |
| ComfyUI-AnimateDiff-Evolved | Video / animation pipeline. |
| ComfyUI-IPAdapter-Plus | Reference-image conditioning (style + face). |
| rgthree-comfy | Power-user nodes: muting groups, fast bypassing, image comparer. |
| was-node-suite-comfyui | Hundreds of utility nodes — math, text, conditioning, debugging. |
WebSocket-driven run · ~30 linesEnd-to-end · WebSocket client
Subscribe to progress, queue a workflow, stream node-by-node events until done, fetch the saved image URL. Same shape as any other event-driven backend — treat ComfyUI as a microservice.
import json, uuid, urllib.parse, urllib.request, websocket
SERVER = "127.0.0.1:8188"
CLIENT_ID = str(uuid.uuid4())
with open("workflow_api.json") as f:
workflow = json.load(f)
workflow["6"]["inputs"]["text"] = "a tea stall in monsoon Mumbai, cinematic, 35mm"
# Subscribe to progress BEFORE queuing the prompt
ws = websocket.WebSocket()
ws.connect(f"ws://{SERVER}/ws?clientId={CLIENT_ID}")
req = urllib.request.Request(
f"http://{SERVER}/prompt",
data=json.dumps({"prompt": workflow, "client_id": CLIENT_ID}).encode(),
headers={"Content-Type": "application/json"},
)
prompt_id = json.loads(urllib.request.urlopen(req).read())["prompt_id"]
# Stream events: progress %, current node, completion
while True:
msg = ws.recv()
if not isinstance(msg, str):
continue
event = json.loads(msg)
data = event["data"]
if event["type"] == "progress":
print(f"step {data['value']} / {data['max']}")
elif event["type"] == "executing" and data["node"] is None and data["prompt_id"] == prompt_id:
break # done
# Fetch the result
hist = json.loads(urllib.request.urlopen(f"http://{SERVER}/history/{prompt_id}").read())
img = next(iter(hist[prompt_id]["outputs"].values()))["images"][0]
q = urllib.parse.urlencode(img)
print("done:", f"http://{SERVER}/view?{q}")
Best practiceGood to know
Common trapsWatch out for
/prompt endpoint only accepts the API export. Sending the UI save format returns 400. Pick "Save (API Format)" specifically when scripting.
filename field, the run is cached and returns the old image. Toggle bypass or change seed to force re-run.