Skip to content
learn

1. The node graph, installing, and a first image

Code: the workflow of this lesson, workflows/01-txt2img.api.json, and the course’s C# tool, whose png-info command is in csharp/Png.cs.

What ComfyUI is, for a C# or Java developer

Section titled “What ComfyUI is, for a C# or Java developer”

ComfyUI generates images, and more recently video, audio and 3D models, with diffusion models. You don’t write a script: you build a graph of nodes in the browser, and a Python server runs it. Each node does one thing, such as loading a model file, encoding a prompt, or sampling, and passes its results to the next nodes through typed connections.

Two pieces work together:

  • The server is a Python program, main.py, built on PyTorch and aiohttp. It holds the node definitions, a queue of prompts, and a cache of node results, and it serves an HTTP and WebSocket API on port 8188.
  • The frontend is a web application, published as the Python package comfyui-frontend-package and served by the same server. It draws the graph, and when you press Run it converts the graph into a prompt and posts it to the API. Lesson 3 looks at both JSON formats, and lesson 4 calls the API from C# and Java.

If you have used TPL Dataflow or a build system, the model is familiar. Nodes are blocks with typed inputs and outputs. The server only runs the nodes that an output node needs, in dependency order. And like an incremental build, it skips a node whose inputs haven’t changed since the last run and reuses its cached result.

A node is a Python class. Here is EmptyImage, one of the simplest, in nodes.py at v0.36.0:

class EmptyImage:
@classmethod
def INPUT_TYPES(s):
return {"required": { "width": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}),
"height": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}),
"batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}),
"color": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFF, "step": 1, "display": "color"}),
}}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate"

INPUT_TYPES declares the inputs and their constraints, RETURN_TYPES the outputs, and FUNCTION names the method the server calls. In C# terms, it is a class whose attributes describe its parameters, discovered by reflection. The server publishes these declarations as JSON at /object_info, and the frontend builds its widgets from them. You don’t need to write Python in this course until lesson 11, which covers custom nodes.

Piece Version
ComfyUI v0.36.0, commit ee71d5c, released on 15 September 2026
Frontend comfyui-frontend-package 1.52.7, the version requirements.txt pins
Python and PyTorch Python 3.13.14 and PyTorch 2.13.0 for CUDA 13.0, as shipped in the Windows portable build
Model Stable Diffusion XL base 1.0, revision 4621659
Machine Windows 11, an NVIDIA GeForce RTX 5080 with 16 GB, driver 610.88, 64 GB of RAM

ComfyUI publishes a release about every week, and its README warns that commits between release tags “may be very unstable and break many custom nodes”. Every link to ComfyUI’s code in this course points at the v0.36.0 commit, so the line numbers stay right.

The portable build bundles its own Python, PyTorch and ComfyUI in one archive. For a recent NVIDIA card, download ComfyUI_windows_portable_nvidia.7z from the v0.36.0 release: 1,917,442,353 bytes, about 1.8 GB. Extract it with 7-Zip or with Explorer, then start it:

Terminal window
cd ComfyUI_windows_portable
.\run_nvidia_gpu.bat

The batch file runs .\python_embeded\python.exe -s ComfyUI\main.py --windows-standalone-build. The release also has builds for older NVIDIA cards (CUDA 12.6), for AMD and for Intel GPUs; this course only ran the NVIDIA one.

When the server is ready, its log ends with To see the GUI go to: http://127.0.0.1:8188. It listens on 127.0.0.1 only. The --listen flag opens it to the network, but the server has no login among its startup flags: anyone who reaches the port can queue work, upload files and read your outputs.

The course reuses a portable build that was already on the machine, without changing it. So it starts the server with its own base directory, which holds the input, output, temp, user and custom node folders, and it keeps the model folder of the install:

Terminal window
.\python_embeded\python.exe -s ComfyUI\main.py --base-directory D:\comfy-course `
--models-directory .\ComfyUI\models --database-url sqlite:///:memory: --disable-auto-launch

--database-url sqlite:///:memory: keeps the server’s small asset database in memory instead of in the user folder. Two surprises came up with --base-directory at v0.36.0:

  • The server stopped at startup with FileNotFoundError when the base directory had no custom_nodes folder, because it lists that folder before loading anything (main.py, lines 198 to 201). Creating the empty folder is enough.
  • The flag moves the models folder too, unless --models-directory points elsewhere.

On this machine the server answered in about 10 seconds. The start of its log says what it found:

Total VRAM 16303 MB, total RAM 65235 MB
pytorch version: 2.13.0+cu130
Set vram state to: NORMAL_VRAM
Device: cuda:0 NVIDIA GeForce RTX 5080 : cudaMallocAsync
DynamicVRAM support detected and enabled
Python version: 3.13.14 (tags/v3.13.14:fd17997, Jun 10 2026, 13:03:48) [MSC v.1944 64 bit (AMD64)]
ComfyUI version: 0.36.0
comfyui-frontend-package version: 1.52.7
Starting server
To see the GUI go to: http://127.0.0.1:8188

ComfyUI ships no model. A diffusion checkpoint is one file that holds three networks, which lesson 2 explains: a denoising network, one or more text encoders, and a VAE. This course starts with Stable Diffusion XL base 1.0, published by Stability AI in July 2023. It is old by 2026 standards, but it runs in 16 GB, needs no account to download, and is what most tutorials and custom nodes were first written for. Lesson 8 compares it with recent models.

File Size SHA-256
sd_xl_base_1.0.safetensors 6,938,078,334 bytes 31e35c80fc4829d14f90153f4c74cd59c90b779f6afe05a74cd6120b893f7e5b

Put it in models/checkpoints/, and check its hash with certutil -hashfile <file> SHA256 on Windows, sha256sum on Linux, or shasum -a 256 on macOS; the file on this course’s machine matched. The safetensors format holds tensors and a JSON header, and no code, unlike older .ckpt files, which are Python pickles that can run code when loaded. The server log says Checkpoint files will always be loaded safely.

The model is under the CreativeML Open RAIL++-M License. A RAIL license gives broad rights to use and share the model and its outputs, and adds use restrictions listed in an attachment. Read it before you use the model in a product: this course doesn’t interpret it. The model card adds its own limits: “The model does not achieve perfect photorealism”, “The model cannot render legible text”, and “Faces and people in general may not be generated properly.” The course’s images contain no real people and no brands.

No model file is in the repository: the course’s CI uses workflows that need none.

The simplest graph that makes an image has seven nodes:

flowchart LR
ckpt["Load Checkpoint<br/>CheckpointLoaderSimple"]
pos["Positive prompt<br/>CLIPTextEncode"]
neg["Negative prompt<br/>CLIPTextEncode"]
latent["Empty Latent Image<br/>1024 × 1024"]
sampler["KSampler<br/>seed, steps, cfg"]
decode["VAE Decode"]
save["Save Image"]
ckpt -- MODEL --> sampler
ckpt -- CLIP --> pos
ckpt -- CLIP --> neg
ckpt -- VAE --> decode
pos -- CONDITIONING --> sampler
neg -- CONDITIONING --> sampler
latent -- LATENT --> sampler
sampler -- LATENT --> decode
decode -- IMAGE --> save
Node What it does Outputs
CheckpointLoaderSimple reads the checkpoint file and splits it into its three networks MODEL, CLIP, VAE
CLIPTextEncode, twice turns a prompt into vectors the model is conditioned on: what you want, and what you don’t CONDITIONING
EmptyLatentImage makes a blank latent image, a tensor of zeros one eighth of the image size LATENT
KSampler adds noise from the seed, then removes it step by step, guided by the prompts LATENT
VAEDecode decodes the latent into pixels IMAGE
SaveImage writes a PNG to the output folder none: it is an output node

The types in capitals are how the frontend knows what can connect to what. A LATENT output only plugs into a LATENT input, the way a compiler checks argument types. SaveImage is an output node: when you queue the graph, the server starts from the output nodes and works back through their inputs. A node that no output needs doesn’t run.

In the browser, you would load this graph from the templates, type a prompt, and press Run. The API does the same thing without the browser. The course’s file workflows/01-txt2img.api.json describes the seven nodes in the API format: one entry per node, keyed by an id, with its type and its inputs. An input is either a value, or a link written ["4", 1], which means output 1 of node 4:

"3": {
"class_type": "KSampler",
"inputs": {
"seed": 42,
"steps": 25,
"cfg": 7.0,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1.0,
"model": ["4", 0],
"positive": ["6", 0],
"negative": ["7", 0],
"latent_image": ["5", 0]
}
},

To queue it, post it under a prompt key:

Terminal window
$workflow = Get-Content code\comfyui\workflows\01-txt2img.api.json -Raw
$body = '{"prompt": ' + $workflow + '}'
Invoke-RestMethod -Method Post -Uri http://127.0.0.1:8188/prompt -ContentType application/json -Body $body

The server checks the graph, puts it in its queue, and answers at once, before anything runs:

{"prompt_id": "fbbda761-f93f-4d80-9403-0a5d47fc3306", "number": 0, "node_errors": {}}

number is the position in the queue. When the prompt has run, GET /history/<prompt_id> returns what it produced; here, trimmed:

"outputs": {"9": {"images": [{"filename": "metronome_00001_.png", "subfolder": "l01", "type": "output"}]}},
"status": {"status_str": "success", "completed": true, "messages": [
["execution_start", {"prompt_id": "fbbda761-…", "timestamp": 1789575897123}],
["execution_cached", {"nodes": [], "prompt_id": "fbbda761-…", "timestamp": 1789575897124}],
["execution_success", {"prompt_id": "fbbda761-…", "timestamp": 1789575911832}]]}

A brass object on an old wooden workbench, lit by morning sun through a dusty window. It looks more like an ornate hourglass than a metronome: a tall glass body with a narrow waist, held in a brass frame on a round base.

Rendered by ComfyUI v0.36.0: Stable Diffusion XL base 1.0, seed 42, 25 steps, euler sampler, normal scheduler, CFG 7, 1024 × 1024, workflow 01-txt2img.api.json, reduced to 768 × 768 for this page.

The prompt asked for “a brass metronome on an old wooden workbench”. SDXL drew something closer to an hourglass. The image is still a correct result: the model makes what its training data associates with the words, and a metronome is a rare object. Lesson 2 changes the seed and the settings: with seed 43, the object comes closer to a metronome.

The log shows how the server spent the 14.71 seconds of this first run:

Requested to load SDXLClipModel
Model SDXLClipModel prepared for dynamic VRAM loading. 1560MB Staged. 0 patches attached. Force pre-loaded 180 weights: 400 KB.
Requested to load SDXL
Model SDXL prepared for dynamic VRAM loading. 4896MB Staged. 0 patches attached. Force pre-loaded 512 weights: 1197 KB.
100%|##########| 25/25 [00:03<00:00, 6.39it/s]
Requested to load AutoencoderKL
Model AutoencoderKL prepared for dynamic VRAM loading. 159MB Staged. 0 patches attached. Force pre-loaded 112 weights: 91 KB.
Prompt executed in 14.71 seconds

The two text encoders take 1.5 GB, the denoising network 4.9 GB, and the VAE 159 MB. The 25 steps themselves took about 4 seconds, at 6.4 steps per second, after a 6.5-second first step in which the model was initialized. The rest went to reading the 6.9 GB file and moving weights to the GPU.

Run Time
First run after the server started 14.71 s, then 12.99 s and 13.30 s after two restarts
Same graph, new seed: only the sampler, the decoder and the save run again 4.53 to 4.89 s
Same graph, only the file name prefix changed: only SaveImage runs 0.07 to 0.09 s

The last line is the cache at work: the history says "execution_cached": {"nodes": ["4", "5", "6", "7", "3", "8"]}, and SaveImage writes the cached image under its new name. When nothing at all has changed, even SaveImage is in the list, and no new file is written.

SaveImage wrote output/l01/metronome_00001_.png: the prefix l01/metronome gives a subfolder and a name, and the server adds a five-digit counter. The PNG also holds the prompt that made it. The course’s C# tool lists the file’s chunks:

> dotnet out/csharp/comfy.dll png-info output/l01/metronome_00001_.png
chunk IHDR: 1 x, 13 bytes
chunk tEXt: 1 x, 921 bytes
chunk IDAT: 23 x, 3146752 bytes once inflated
chunk IEND: 1 x, 0 bytes
text prompt: 914 characters: {"4": {"class_type": "CheckpointLoaderSimple", "inputs": {"c...
image: 1024 x 1024, 3 channels, pixel SHA-256 698e7867e7fc04fb

The tEXt chunk named prompt holds the API-format graph as JSON: open the PNG in ComfyUI’s frontend, and it rebuilds the graph. Lesson 3 reads both chunks, and explains why a PNG saved from the browser has a second one.

  • ComfyUI is a Python server that runs graphs of typed nodes, and a browser frontend that edits them. The API takes the same graphs as the browser.
  • The server runs only what an output node needs, and caches node results: an unchanged graph runs in a tenth of a second.
  • The Windows portable build is one archive. On Linux and macOS, ComfyUI is a Python application in a virtual environment.
  • A checkpoint holds a denoising network, text encoders and a VAE. SDXL base 1.0 is 6.9 GB, under the CreativeML Open RAIL++-M License, and draws what its training data suggests, not always what you asked for.
  • The server has no authentication: keep it on 127.0.0.1.

Get your own machine to the same place, then make it say so. Start the server, queue the lesson’s graph with curl, and read the three numbers this lesson read: how long the first image took, how long the second took with the models already loaded, and how much memory nvidia-smi showed while it sampled. Then change the prompt to something you care about — an object on your desk — and keep the first image you get, however wrong it is. Lesson 2 explains why it is wrong, and the comparison only means something if you have your own before.

  1. Queue the lesson’s workflow twice with the same inputs, then once with "seed": 43. Compare the three entries of /history: which nodes ran each time?
  2. GET /object_info/EmptyLatentImage returns the declaration of a node. What are the minimum size and the step of width, and why would a latent image need a step?
  3. Change the negative prompt to an empty string and queue the workflow. Which nodes run again, and why does CLIPTextEncode node 6 not?
Solution 1

The first run has "execution_cached": {"nodes": []}: every node ran. The second lists all seven nodes, SaveImage included, and writes no new file: its outputs name the same metronome_00001_.png as the first run. The third lists nodes 4, 5, 6 and 7: the checkpoint, the empty latent and both prompts are reused, and KSampler, VAEDecode and SaveImage run, because the seed is an input of KSampler and the other two come after it in the graph.

Solution 2
"width": ["INT", {"default": 512, "min": 16, "max": 16384, "step": 8, "tooltip": "The width of the latent images in pixels."}]

The minimum is 16 and the step 8. The node creates a tensor of width // 8 by height // 8 (nodes.py, line 1265): the VAE encodes 8 pixels into one latent position, so a width that isn’t a multiple of 8 would be cut. The server’s own max, 16384, is MAX_RESOLUTION; the /object_info answer gives the number, not the name.

Solution 3

Node 7, the negative prompt, runs again because its text input changed, and so do KSampler, VAEDecode and SaveImage, which depend on it. Node 6 has the same inputs as before, the same text and the same link to node 4’s CLIP, so the server reuses its cached output. A node’s cached result is reused when its own inputs, and those of the nodes it depends on, are unchanged.