workflow-layout · git:20260627.9aacdee · 2026-06-27 · sha256 87bb41dbab350c1e
workflow-layout git:20260627.9aacdeeA
Immutable. This exact content is served forever at /api/v1/blob/87bb41dbab350c1e.
---
name: workflow-layout
description: Lay out and organize a ComfyUI workflow cleanly on the live panel canvas — dependency-layered node placement with no overlaps, subgraphs, colored group boxes, and subgraph rail alignment. Use when asked to tidy / clean up / organize / arrange a workflow, add groups or subgraphs, fix overlapping nodes, or build a workflow that should look good from the start.
---
# ComfyUI Workflow Layout & Organization
Turn a tangled graph into a clean **left-to-right dataflow** a human reads at a glance,
using the `panel_*` canvas tools. The golden rule: **never lay out blind** — read the
real node sizes and rail positions first, then compute positions from them.
## Primitives (panel tools)
- **`panel_get_graph`** — READ FIRST, every time. Returns, for the graph you're viewing:
- each node's **`pos` [x,y]**, **`size` [w,h]** (body only), and **`full_height`** (the
TRUE rendered footprint = title bar + body; use this for vertical stacking),
- all **`groups`** (`id`, `title`, `color`, `bounding [x,y,w,h]`),
- and — only when inside a subgraph — the **`rails`**: the `input` / `output` boundary
node positions. Everything below is computed from these numbers.
- `panel_move_node(node_id, [x,y])`, `panel_set_node_title(node_id, title)`,
`panel_set_node_collapsed(node_id, collapsed)` (minimize to title bar),
`panel_set_node_color(node_id, {preset|color|bgcolor})` (color-code stages —
presets: red/brown/green/blue/pale_blue/cyan/purple/yellow/black)
- **Groups** (colored boxes; nodes stay put): `panel_create_group` (pass `node_ids` to
auto-wrap, or `bounds [x,y,w,h]`; `color` hex), `panel_move_group`, `panel_edit_group`,
`panel_remove_group`.
- **Subgraphs** (nest nodes into one collapsible node): `panel_create_subgraph(node_ids)`,
`panel_enter_subgraph` / `panel_exit_subgraph`, `panel_get_subgraph`,
`panel_promote_widget`, **`panel_move_rail(rail, [x,y])`** (`rail` = `"input"|"output"`,
must be inside the subgraph), **`panel_expose_subgraph_output(from_node_id, from_output)`** /
**`panel_expose_subgraph_input(to_node_id, to_input)`** (expose an interior slot on the
boundary rail — must be inside the subgraph), and **`panel_unpack_subgraph(node_id)`**
(expand/dissolve a subgraph back into the parent — the inverse of `panel_create_subgraph`).
- `panel_canvas({action:"fit"})` to frame the result; `panel_save_workflow` to persist.
## The layout algorithm (dependency-layered, overlap-free)
1. **Read** the graph. Build a DAG from each input's `connected_from` (ignore unconnected
widget inputs — only node→node edges matter).
2. **Layer** every node by longest path from a source:
`layer(n) = 0` if it has no incoming node edges, else `1 + max(layer(of its sources))`.
Layers become columns, left → right.
3. **X by layer:** `x = X0 + layer * COL_PITCH`, where `COL_PITCH ≈ widest node.size[0] in
that column + ~80`.
4. **Y by FULL height (this is what stops overlaps):** stack a column top→down with
`y[i+1] = y[i] + node[i].full_height + ROW_GAP`. Use **`full_height`** (from
`panel_get_graph`), NOT `size[1]`. `size[1]` is the BODY only (slots + widgets); the
**title bar renders ~30px ABOVE `pos`** and is NOT in `size[1]`, so stacking by `size[1]`
overlaps every node by a header (the classic "headers eating the node above" bug).
`full_height` already includes that header (and is just the title height for a collapsed
node), so `y += full_height + ROW_GAP` lands an exact `ROW_GAP` gap between the previous
node's bottom and the next node's title. **Never** use a fixed row pitch — tall nodes
(KSampler, WanVideo Sampler, LoRA-select) are 480–600px and *will* overlap a 320 pitch.
(If `full_height` is ever absent, fall back to `size[1] + ~30` for the header.)
5. **Order within a column** to cut wire crossings: place each node near the average Y of its
connected nodes (a median/barycenter pass is plenty).
Reads-well constants: `COL_PITCH` 360–480, `ROW_GAP` 40. Because `full_height` already
accounts for the title bar, you don't add extra top headroom per node — `ROW_GAP` is the
clean gap you'll actually see.
## Subgraph interiors — move the rails!
A subgraph has two boundary **rails** (input left, output right). They **do not** follow the
inner nodes — move the nodes without moving the rails and you get a huge gap (a very common
mistake). For each subgraph: `panel_enter_subgraph` → lay out the inner nodes (algorithm
above) → then pin the rails to the node band:
- input rail → `panel_move_rail("input", [minNodeX - 180, bandTopY])`
- output rail → `panel_move_rail("output", [maxNodeX + 60, bandTopY])`
Keep rails at the same Y as the first row. Read current rail positions from
`panel_get_graph` (`rails`) before deciding. `panel_exit_subgraph` when done.
**Wiring interior nodes to the boundary (don't connect to a guessed rail id).** To expose
an interior node's output/input on the boundary so the PARENT graph can wire it, do NOT
`panel_connect` to a rail node id you guessed — use `panel_expose_subgraph_output(from_node_id,
from_output)` (interior output → output rail) and `panel_expose_subgraph_input(to_node_id,
to_input)` (interior input → input rail), both while inside the subgraph. `panel_get_graph`'s
`rails` shows which boundary slots already exist and which still need exposing. To expand/
dissolve a subgraph back into the parent (inline its inner nodes + rewire external links,
removing the wrapper — the inverse of `panel_create_subgraph`), use
`panel_unpack_subgraph(node_id)`. All undoable with Ctrl+Z.
## Groups vs subgraphs — choose deliberately
- **Group** (colored box): lightweight visual band; nodes stay in place and editable. Reach
for this **first** — to label regions of a flat graph or band stage-columns at the root.
- **Subgraph**: collapses a stage into one node. Powerful for large graphs, but it nests/hides
nodes and adds boundary ports. **Don't subgraph everything** — a 2–3 node stage rarely earns
it, and over-subgraphing hurts readability and complicates packaging/handoff.
- Clean recipe: lay the flat graph out well → wrap only the genuinely complex stages in
subgraphs → drop colored group bands around the columns at the root.
## Stage decomposition that fits most pipelines
`Loaders → Inputs → Preprocess (pose/controlnet/conditioning) → Embeds → Sample →
Decode/Output` — one concern per column/stage, strictly left-to-right.
## Always leave inputs & outputs exposed
The nodes a user touches first — the **input** (Load Image / Load Video) and the
**output** (save / video-combine / preview) — must stay **expanded and visible** so they
can jump straight in: drop in their media, hit run, watch the result. Collapse the
internal machinery (loaders, encoders, samplers) into chips to cut noise, but **never**
collapse the inputs or outputs. When they live inside subgraphs, keep those subgraph
nodes expanded — and consider `panel_promote_widget` to surface the one key widget
(prompt, seed) onto the subgraph node so it's editable without drilling in.
> Heads-up: input/output preview nodes (`LoadImage`, `VHS_LoadVideo`, video-combine)
> report their full `size[1]` but render short until media loads — size their group band
> to the `.size` (so it fits once filled), not to the empty-preview render.
## End-to-end workflow
1. `panel_get_graph` — capture `pos`/`size`/`groups` (and `rails` inside each subgraph).
2. Compute layers + overlap-free positions from the real sizes.
3. `panel_move_node` for each (batch the moves — they apply in order).
4. Per subgraph: enter → lay out inner nodes → `panel_move_rail` both rails → exit.
5. Optional: `panel_create_group` bands around the root columns (and `panel_remove_group`
any stranded empty groups left behind by earlier edits).
6. `panel_save_workflow`, then `panel_canvas({action:"fit"})`.
## Gotchas
- **Save before relying on it:** node positions/titles and groups persist only once saved; a
browser refresh reloads from the saved workflow (and re-binds nodes after installing packs).
- Converting nodes to a subgraph **preserves inner node ids**; the subgraph node gets a new id.
- Moving nodes inside a subgraph leaves the **rails behind** — always re-pin them (see above).
- Canvas edits never affect an in-flight render; but a ComfyUI **restart clears the queue**.
- **Image/video nodes under-report height:** `LoadImage`, `VHS_LoadVideo`, preview nodes
return a tiny `size[1]` because the image/video preview height isn't in `.size`. Leave
extra vertical room (≈250–300px) below them so the preview doesn't overlap the next node.
- Color-code by stage (e.g. all loaders `blue`, sampler `green`) and `collapsed` rarely-touched
loaders to cut visual noise — cheap wins once the positions are right.
- Don't over-tidy a graph that's mid-build for the user — confirm if a big reorg is wanted.