| name | comfyui-compact-workflow |
| description | Collapse a sprawling ComfyUI graph into the fewest nodes that still do the job — tidy the layout, bus the fan-out through reroutes, or fold a whole repeated branch into a single custom node (image in → result out). Use when a workflow has grown to dozens/hundreds of nodes, has N near-identical branches, is unreadable spaghetti, or when someone asks to "simplify", "compact", "clean up", "make it fewer nodes", or "turn this into one node". Assumes ComfyUI at http://localhost:8188. |
Compacting a ComfyUI workflow
A graph gets big two ways: fan-out (one reference, N near-identical branches) and depth (a long fixed pipeline you re-run constantly). Both compact, but not the same way.
The acceptance test is identical pixels. A compaction is correct when the small graph, at the same seed, produces the same output as the big one — not merely a plausible one. Fix the seed, run both, diff. If you can't diff, you haven't verified anything.
Pick the level — cheapest first
| Level | What it does | Node count | Reusable? | Cost |
|---|
| 1. Layout | groups + collapsed nodes | unchanged | no | minutes |
| 2. Reroute bus | kills fan-out spaghetti | +N dots | no | minutes |
| 3. Subgraph | folds a branch into one UI node | fewer | yes, in the UI | one click |
| 4. Custom node | folds the whole thing into real code | 3-ish | yes, anywhere | an hour |
Do not skip to 4. Levels 1–2 are free and often enough. Level 4 is right when the graph is a recipe you'll re-run on new inputs — that's when the fan-out deserves to become a for loop.
Level 1 — Layout (cosmetic, zero risk)
In litegraph JSON: flags: {"collapsed": true} shrinks a node to its title bar, and a title you set is what shows. Collapse the machinery; leave open only what a human edits.
node["flags"] = {"collapsed": True}
node["title"] = "sample"
wf["groups"] = [{"id": 1, "title": "00 FRONT", "bounding": [x, y, w, h],
"color": "#353535", "font_size": 24, "flags": {}}]
Rule of thumb: one open node per group — the one knob that panel exists to expose.
Level 2 — Bus the fan-out with reroutes
If K shared signals (model, clip, vae, a reference latent, a negative) each feed N branches, that's K×N wires crossing the canvas. Rearranging nodes will not fix this. Give each signal a vertical lane of Reroute nodes, chained top to bottom, and have each branch tap the dot beside it.
{"id": 500, "type": "Reroute", "pos": [x, y], "size": [75, 26],
"flags": {}, "order": 0, "mode": 0,
"inputs": [{"name": "", "type": "*", "link": upstream_link}],
"outputs": [{"name": "", "type": "MODEL", "links": [], "slot_index": 0}],
"properties": {"showOutputText": False, "horizontal": False}}
Also put the branches in one column, not two — a second column forces every wire to reach sideways.
⚠️ Reroute has no backend class. It is frontend-only, resolved when the user hits Run. It will not appear in /object_info. Consequences:
- An API graph must never contain one. Only add reroutes to the litegraph/UI file.
- Any converter you write (litegraph → API) must follow links through reroutes to the real producer. Skipping the node without rewiring the link produces an API graph whose input points at a node id that doesn't exist — and
/prompt then fails with an error nowhere near the cause. Guard the dangling and cyclic cases.
Level 3 — Subgraph
Select the branch's nodes in the UI → right-click → Convert to Subgraph. One node, clonable.
Do not hand-author subgraph JSON. The definitions.subgraphs schema is frontend-version-specific; guessing it yields a workflow that loads with missing nodes. Let the UI write it, then read what it wrote.
Level 4 — Fold it into a custom node
This is the real compaction: LoadImage → YourNode → SaveImage.
Compose ComfyUI's own node classes. Never reimplement sampling. That is what makes the output identical instead of merely similar, and it's less code.
def _call(node, *args, **kwargs):
"""Invoke a stock node, whichever schema it's written against.
V1: declares FUNCTION, returns a plain tuple.
V3 (io.ComfyNode — the Flux nodes): classmethod `execute` returning a
NodeOutput, which is NOT indexable; the tuple is on `.result`.
"""
from nodes import NODE_CLASS_MAPPINGS as M
cls = M[node]
out = cls.execute(*args, **kwargs) if hasattr(cls, "define_schema") \
else getattr(cls(), cls.FUNCTION)(*args, **kwargs)
return getattr(out, "result", out)
Read the real signature before you call it — don't trust memory:
grep -n -A3 "class KSampler" nodes.py
grep -rn -A20 "class FluxGuidance" comfy_extras/
The four things that will bite you
-
Loaders bypass the cache. ComfyUI caches loader nodes at the graph level; calling UNETLoader().load_unet(...) directly does not. Without memoizing, every run re-reads gigabytes from disk. Cache on the filename tuple, and hold only one set — these are huge.
-
Never hallucinate a filename. Pull widget enums from the real loader classes at UI-load time, so the dropdown can only offer files that exist:
def _enum(node, field):
from nodes import NODE_CLASS_MAPPINGS as M
return M[node].INPUT_TYPES()["required"][field][0]
"unet_name": (_enum("UNETLoader", "unet_name"),),
-
A long internal loop must be interruptible and show progress, or the UI looks hung:
pbar = comfy.utils.ProgressBar(len(items))
for item in items:
comfy.model_management.throw_exception_if_processing_interrupted()
...
pbar.update(1)
-
ComfyUI only scans custom_nodes/ at startup. Edit the file, nothing changes, and you'll debug a ghost. Restart, then verify it registered — a clean log is not proof:
curl -s localhost:8188/object_info/YourNode | head -c 200
Extract the spec from the graph. Never retype it.
The big graph is the specification. Pull the prompts, seeds, guidance, steps and
stitch geometry out of it programmatically — walk KSampler → FluxGuidance → ReferenceLatent → CLIPTextEncode and read the values. Retyping them from memory or from
a screenshot is how a "pixel-identical" compaction quietly stops being identical.
Then reconstruct and assert before you spend any GPU time:
rebuilt = f"{head} {' '.join(locks)}".strip()
assert rebuilt == original_prompt_from_the_graph
Where the branches differ from each other is the real design. Diff them. A branch that
drops a clause the others carry is not noise — it's a deliberate exception, and a naive
compaction that applies one global setting to everything will silently destroy it.
(Concretely: garment panels omitted "identical outfit" because you cannot tell a model to
keep the outfit while telling it to change the clothes. A single global style_lock
re-applied it to every panel, and the wardrobe change lost. Eleven panels came back
bit-identical and three didn't — which is exactly what a per-branch diff reveals and a
glance at the sheet does not.)
Keep the node generic, not one-off
The fastest way to build a useless node is to bake this project's specifics into it. Split them:
- Node defaults → neutral. The reusable structure (the poses, the stages, the passes).
- Widgets → this project. The subject-specific text, locks, style.
- The saved workflow carries the specifics, not the node.
Then a new input just needs new widget text, not a new node.
Verify, or you've done nothing
Diff per unit, not per image. A whole-image RMSE tells you that something broke;
a per-panel diff tells you which branch and therefore why. Slice the output on the
known grid and report each cell — the failures are never evenly spread.
d = np.abs(big[y:y+PH, x:x+PW] - small[y:y+PH, x:x+PW])
print(name, "identical" if d.max() == 0 else f"DIFFERS max={d.max()}")
⚠️ Compare pixels, not file bytes. Two PNGs with identical pixel data routinely have
different sha256s (encoder metadata). sha256sum will tell you they differ when they
don't. Load them and compare arrays.
back, warns = litegraph_to_api(litegraph, object_info)
assert back == the_api_graph_that_actually_ran
Order of operations
- Read the big graph and find the repeated unit. If nothing repeats, you want depth-compaction (level 4 straight to a pipeline node), not fan-out.
- Fix the seed and capture the reference output.
- Compact at the lowest level that solves the actual complaint. "Messy" usually means levels 1–2; "I want three nodes" means level 4.
- Diff against the reference output. Same seed, same pixels.
- Restart, verify registration, run the small graph end to end, and look at the result.