workflow-layout

$npx mdskill add artokun/comfyui-mcp/workflow-layout

Lays out ComfyUI workflows cleanly with dependency-layered node placement.

  • Organizes tangled workflows into readable left-to-right dataflow with no overlaps.
  • Uses panel_query_graph to read real node sizes, positions, groups, and rails.
  • Computes positions from dependency order and actual node dimensions.
  • Applies layout via panel_move_node, panel_set_node_color, and group boxes.

SKILL.md

.github/skills/workflow-layoutView on GitHub ↗
---
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_query_graph {fields:'detail', limit:200, max_chars:60000}`** — 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_query_graph` detail rows), 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_query_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_query_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_query_graph {fields:'detail', limit:200, max_chars:60000}` — 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.

More from artokun/comfyui-mcp

SkillDescription
ai-toolkit-trainerTrain custom LoRAs with ostris AI-Toolkit — covers WAN 2.2/2.1 (people, styles, video motion) and Z-Image (Turbo & Base, low-VRAM image LoRAs). Use when the user wants to train a WAN or Z-Image LoRA; covers local + RunPod setup, dataset prep, key params, and using the result in a ComfyUI workflow.
anima-baseAnime/illustration text-to-image (ANIMA 1.0, ~2B Cosmos DiT) — use for anime, manga, illustrated characters; accepts Danbooru tags + natural language; runs/trains on <6GB VRAM; includes anime inpainting via Anima-LLLite ControlNet
anima-lora-trainerTrain a custom anime LoRA on the ANIMA base model — Citron's local Gradio trainer (kohya sd-scripts), <6GB VRAM, character/style LoRAs; covers setup, dataset prep, training params, and using the result in the anima-base workflow
civitaiDiscover Civitai models with the BUILT-IN search_civitai_models tool and install/generate them locally — find a checkpoint/LoRA/embedding on Civitai, download it into ComfyUI, and use its trigger words. Optionally pair the official Civitai MCP for community features (images browsing, posting, collections).
color-correctionDiagnose and fix video/image color OBJECTIVELY with the analyze_color tool (scopes/stats — black/white points, contrast, saturation, clipping, cast) instead of eyeballing a contact sheet. Covers the "washed out" signature, why reference color-match (mkl/ColorMatch/ColorMatchAdobe) CAN'T add contrast a flat source lacks, the levels/contrast-stretch fix (core AdjustContrast / CurveEditor), the measure→fix→re-measure loop, the side-by-side sandbox pattern, and where to place the fix in a render graph (after decode, before save). Use when a render looks washed out / flat / dull / over-saturated / color-cast, or when deciding between a color-match and a contrast/levels fix.
comfyui-coreCore ComfyUI knowledge — workflow format, node types, pipeline patterns, and MCP tool usage
comfyui-frontend-extensionsAuthoring ComfyUI v2 frontend extensions with @comfyorg/extension-api — defineNode/defineExtension/defineWidget, shell UI (sidebar tabs, commands, hotkeys), typed events, and handles. Use when writing or editing ComfyUI web-UI extension code (custom node JS, sidebar panels, widgets).
comfyui-launch-flagsPick the right ComfyUI startup flags for VRAM, attention, caching, and speed — the full decision matrix for OOM (--novram / --cache-none / --disable-smart-memory), shared-VRAM creep on Windows (--reserve-vram N), model-switching with big text encoders (--cache-none), high-VRAM throughput (--gpu-only / --highvram), and attention-backend selection (--use-sage-attention for speed, --use-pytorch-cross-attention as the highest-quality / Z-Image-safe fallback). Also the acceleration-stack + Blackwell/RTX 5000 (sm_120) notes. Use when a graph OOMs (especially long video like LTX 2 / WAN), when the GPU spills into shared VRAM and slows to a crawl, when switching between models eats all RAM, when Z-Image produces black/garbled output under Sage, or when deciding which attention backend to launch with. Flag names verified against upstream comfy/cli_args.py — see Sources.
comfyui-node-registryAuthoring & publishing ComfyUI custom nodes to the Comfy Registry — node structure, pyproject.toml spec, comfy-cli publishing, and CI
debug-renderDebug a WRONG or imperfect render (not a hard error) by inspecting inputs and intermediate steps with run-to-node — render one branch up to an output, preview-tap latents/masks/preprocessor maps, localize the first bad stage, then fix. Use when a final image/video comes out wrong — artifacts, wrong subject/pose/composition/color, blur, a ControlNet/IPAdapter/mask/LoRA not taking, a two-stage refiner or upscale degrading the result — rather than the run failing with an error (for errors/OOM/missing nodes use the troubleshooting skill).