Skip to content

Delcado19/comfyui-flowforge

Repository files navigation

ComfyUI FlowForge

ComfyUI FlowForge

Automatically rearranges nodes in a ComfyUI workflow JSON file so that data flows left-to-right and connection lines stop crossing each other.


The Problem

ComfyUI workflows grow organically. Nodes get added wherever there is space, moved around during iteration, and groups get reorganised. The result is a canvas where connection lines criss-cross in every direction — hard to read and hard to debug.

FlowForge reads a workflow JSON, computes a clean left-to-right layout using a graph algorithm, and writes the result back. Only node positions, compact node sizes, and group bounding boxes are changed. Every connection, setting, model reference, and widget value is preserved exactly.


Requirements

  • uv — Python package manager (already installed)
  • Python ≥ 3.10 (installed automatically by uv when needed)
  • The repository pins the local development interpreter to Python 3.12 through .python-version

Installation

Prerequisites

Ensure uv is installed. If not, install it from astral.sh/uv.

Install from source

git clone https://github.com/Delcado19/comfyui-flowforge.git
cd comfyui-flowforge
uv sync

This installs all dependencies and the comfyui-flowforge package in development mode.

Quick Start

CLI Usage

# Start the API server
uv run flowforge serve

# Apply layout to a workflow file
uv run flowforge layout input.json output.json

# Optimize and layout a workflow
uv run flowforge layout input.json output.json --optimize

# Optimize only
uv run flowforge optimize input.json optimized.json

GUI Usage

# Start API server and open the web interface
uv run flowforge-gui

The GUI opens in your browser at the URL shown in the terminal. It starts at http://127.0.0.1:5173 and automatically picks the next available frontend port when that port is unavailable. The launcher also auto-selects an available backend API port and passes it through to the frontend so the browser session stays connected even when local ports are already in use. Use Optimize + Layout in the toolbar for the normal cleanup flow: insert Set/Get hubs for eligible high-fanout MODEL, CLIP, and VAE wiring, then run layout. Use Layout Only when the workflow should keep its original node graph. Optimizer rewrites are skipped when a fanout touches pinned nodes or nodes inside pinned groups, so pinned control areas keep their visible structure.

One-Click Launchers

Windows:

start-flowforge.bat

Linux:

sh start-flowforge.sh

Both launchers run from the repository root and start uv run flowforge-gui.

Features

  • Workflow JSON Roundtrip: Preserves ComfyUI workflow metadata while updating layout positions, compact node sizes, and group bounds for unpinned elements
  • Visual Workflow Canvas: See how nodes are positioned on a pan/zoom canvas, then drag and resize nodes directly in the workflow view
  • Mini Map and Groups: Navigate large workflows with a minimap, grouped background regions, group-aware dragging, resizable group containers, and group creation/rename/deletion controls
  • Interactive Controls: Open, optimize, layout, and save workflows with button clicks plus live X/Y spacing controls, pin/unpin controls, and toolbar buttons to create or clear groups
  • Before/After Comparison: Toggle ghost outlines of the previous node positions after a layout run
  • Color-Coded Nodes: Different node types are visually distinguished and rendered with ComfyUI-like widgets
  • Zoom & Pan: Mouse wheel zoom, plus/minus buttons, and scrollbars for navigation

How It Works

FlowForge implements a compact layout pipeline:

Phase 0 — Node Compaction

Before any graph geometry is derived, layout shrinks unpinned nodes that have saved larger dimensions down to a conservative compact size. Reroutes, collapsed nodes, and regular nodes use separate minimum dimensions. Existing nodes that are already smaller are left unchanged. LoadImage*, SaveImage*, Note, MarkdownNote, and label nodes keep their saved size because image previews, filename widgets, custom save controls, and annotation blocks often depend on the author's chosen rectangle. Nodes with ComfyUI flags.pinned: true, and nodes inside groups with flags.pinned: true, keep their saved position and size.

Phase 1 — Group Membership

Every node is assigned to the group whose bounding box contains it (using the node's original position). Nodes outside every group form an implicit ungrouped set. When groups overlap, the first matching group in workflow order wins.

Phase 2 — Inter-Group Topology

A directed graph is built between groups: a group A gets an edge to group B whenever a link crosses from a node in A to a node in B. For freely movable groups, FlowForge also follows ungrouped bridge nodes so a path such as Reference group -> scaler -> ControlNet group still influences group columns even though the scaler is not inside either group. Bridge-derived dependencies are skipped when either endpoint group has fixed geometry from pins, so pinned or partially pinned author layouts are not stretched into long synthetic chains. Those pinned group surfaces still act as bridge-node anchors, so decoder or adapter nodes that feed a fixed output panel use the visible gap beside that panel instead of drifting beyond the movable source group. Groups are assigned to columns from left to right with a longest-path pass. Groups with no ordering relationship share the same column and are stacked vertically, sorted by their original vertical centroid to preserve the author's intended arrangement.

Phase 3 — Internal Layout (Sugiyama)

Within each group, independently:

  1. Layer assignment — each node receives a layer number equal to the longest path from any source node to it (layer = max(layer[predecessor]) + 1, with sources at layer 0). Uses a topological sort; nodes in cycles (rare in valid ComfyUI workflows) fall back to layer 0.
  2. Side-branch compaction — display/debug side branches such as MaskToImage -> PreviewImage, terminal text previews, and prompt switch/control panels do not force extra main columns when they can sit beside the data they inspect or control.
  3. Crossing minimisation — nodes within each layer are reordered using the barycenter heuristic: each node's score is the average position of its neighbours in the adjacent layer's current order. Two passes are run (forward then backward) to reduce edge crossings.
  4. Coordinate assignment — nodes are placed on a grid: X increases by layer, Y increases by position within the layer. Internal group columns use each layer's own widest node instead of the widest node in the whole group, so one large preview or save node does not stretch every column. Long internal layer chains wrap into additional rows once they would make a group too wide, so refinement groups can stay compact by using vertical space. Bypassed nodes (mode = 4) are sorted to the end of their layer so they don't interrupt the active flow. The public layout operation evaluates several spacing candidates and applies the best-scoring result, with penalties for layouts that become too wide compared to their height, leave large horizontal gaps, or add straight-line link crossings. Small workflows try 3 candidates, medium workflows 5, and larger workflows 7. Candidate search can stop early once later variants stop improving, and candidate order is biased by the workflow's overall aspect ratio.

Phase 4 — Global Positioning

The content size of every unpinned group is known after Phase 3. Column widths are determined by the widest group in each column. Unpinned groups are placed left-to-right by column and top-to-bottom within each column. Group rectangles are compacted to their laid-out node contents plus padding before the global placement is finalized. Node positions are translated from group-local coordinates to global canvas coordinates. Groups with ComfyUI flags.pinned: true keep their saved bounding rectangle, and their member nodes keep their saved positions and sizes. Linked ungrouped nodes are arranged in dataflow layers before being placed after the grouped layout, so source-to-target chains continue to move left-to-right instead of being packed only by original Y position. Ungrouped dataflow columns use each layer's own widest node instead of the widest node across the whole ungrouped set, preventing one large source or preview node from stretching every later column. Ungrouped bridge placement is reserved for true group-to-group bridge nodes: nodes that connect movable group columns, or nodes directly bridging to fixed group geometry. Source-only chains downstream of loader groups stay in normal dataflow layers instead of being stacked in one bridge slot. True bridge nodes are aligned to the connected source/target ports and given enough compact column spacing to avoid being pushed below the destination group. When a bridge node is incident to pinned group geometry, it uses the actual visible gap between its incident group surfaces even though the pinned group does not force group-layer movement. Ungrouped source nodes that feed grouped blocks are then pulled back into collision-free slots beside their target groups, preventing loader and model-source nodes from widening the workflow at the far right with right-to-left links. Primitive control nodes, such as seed, CFG, and string controls, and sampler setup sources such as EmptyLatentImage, are then pulled into collision-free local slots around the input side of their downstream consumer or sampler cluster so workflow control panels stay close to the nodes they drive without covering context nodes. Grouped control nodes only anchor to consumers inside the same group, so a loader/control group is not stretched toward an external sampler. Unlinked ungrouped nodes keep compact vertical packing. Pinned node and group geometry is treated as a hard obstacle: unpinned nodes and movable groups are moved out from under pinned surfaces, then local controls and virtual hubs are re-anchored against the final endpoint positions without covering pinned control areas. Virtual Set/Get hubs are excluded from regular group and dataflow placement. They behave as local source/destination adornments: FlowForge keeps them port-adjacent when possible, then tries same-side vertical slots, then places them directly above or below their physical endpoint before falling back to a sideways shift. Virtual hubs follow their physical endpoint even if their old position placed them inside a pinned group or their own pin flag is set, because otherwise they create long cross-workflow wires instead of acting as local anchors.

Decorative Nodes

Comment nodes (Note, MarkdownNote, Label) carry no dataflow edges and are excluded from the graph algorithm. FlowForge now places them first as a left-side annotation column, ordered by their original Y position, before the rest of the workflow is optimized.

Phase 5 — Bounding Box Update

Each group's bounding rectangle is reconciled with the final positions of its member nodes plus group padding and a reserved title/header band above the nodes. Layout compacts larger existing group rectangles when the contained nodes fit into a smaller layout without letting nodes intrude into the visible group title. Connected groups are placed in dataflow columns derived from direct inter-group links and eligible bridge-mediated links through ungrouped nodes, which keeps source, branch, and output groups closer to the direction of their wires. Workflows without inter-group links keep the bounded row packer and wrap downward when the next group would exceed the capped soft row width, so the layout uses Y space instead of growing into one long horizontal strip. As a final visible-geometry pass, unpinned groups move as whole units when their compacted rectangle would overlap another group, a pinned group surface, or a node outside the group.

Layout Spacing

Layout spacing is driven by two independent values:

Setting Default Range Description
node_x_distance 80 px 20-240 px Controls horizontal spacing, group width, and the left-to-right packing distance.
node_y_distance 80 px 20-240 px Controls vertical spacing, group height, and the top-to-bottom packing distance.

The API accepts a layout wrapper of the form {"workflow": ..., "layout": {"node_x_distance": 80, "node_y_distance": 80}}. Bare workflow JSON remains supported for backwards compatibility, and legacy min_node_distance input is still accepted as an alias for both axes. Each layout run tries several axis-spacing candidates and keeps the best-scoring result. The /layout response also includes a X-FlowForge-Layout-Stats header with the selected candidate and score breakdown.


Optimizer (optional)

Pass --optimize to run a pre-layout pass that converts high-fanout MODEL, CLIP, and VAE connections into Set/Get node pairs (comfyui-kjnodes must be installed in ComfyUI).

When to use it: workflows where a single loader node (e.g. VAELoader, UNETLoader, CLIPLoader) fans out to three or more downstream consumers typically have many crossing long-distance wires. Replacing these with Set/Get pairs:

  • Eliminates the long wires entirely, which reduces crossing counts after layout.
  • Breaks inter-group cycles that loader fan-out would otherwise create, allowing the layout algorithm to produce a strictly left-to-right result.

What it does: for every output of type MODEL, CLIP, or VAE with two or more downstream connections, or with a single long cross-group connection, FlowForge estimates the routing cost before and after a rewrite. Cross-group links are weighted slightly higher so broad inter-group fanouts are prioritized. Candidate savings are recomputed greedily after each rewrite so the optimizer always applies the best remaining candidate on the current graph. If the rewritten graph is cheaper, FlowForge inserts one SetNode immediately after the source and one GetNode before each target. Layout keeps each virtual hub beside its physical port endpoint and assigns it to that endpoint's group: SetNode next to the source output and GetNode next to the consumer input. FlowForge does not rewrite a fanout upstream of LoRA or same-type pass-through transformer nodes, because that would put a local GetNode in the source panel instead of putting the SetNode on the transformed output. The Set/Get pair uses the same widget value as its virtual connection key, so no physical SetNode -> GetNode link is added. Reroute chains are collapsed during detection, so fanout hidden behind reroute nodes is considered too. The original links are removed. The workflow runs identically in ComfyUI.

Requirement: comfyui-kjnodes must be installed in your ComfyUI instance, otherwise ComfyUI will show missing-node warnings on load.


Known Limitations

Residual back-edges after cycle breaking. When two groups genuinely exchange data bidirectionally (rare in well-structured workflows), the DFS cycle-breaker removes the minimum number of back-edges to produce a DAG. The removed edges become right-to-left wires in the output — typically fewer than 20 % of links in affected workflows. Using --optimize often eliminates the root cause by converting loader fan-outs to Set/Get pairs before layout runs.

Group overlap in the original workflow. If a node's original position lies inside multiple overlapping group bounding boxes, it is assigned to the first group in workflow order. This is the same behaviour as ComfyUI itself.

Sub-graph nodes. Nodes whose type is a UUID (ComfyUI inline sub-graphs) are treated as opaque black boxes. Their internal nodes are not rearranged.


Maintainer Notes

Repository-local agent rules live in AGENTS.md. Documentation synchronization is handled by the Documentation Maintenance Agent policy in docs/DOCUMENTATION_MAINTENANCE.md.

Release history is tracked in CHANGELOG.md. Release and deployment expectations are tracked in docs/RELEASE.md, with frontend asset packaging details in docs/PACKAGING.md. FlowForge currently supports source-checkout deployment with uv; broader distribution targets are still deferred. GitHub Actions CI runs backend tests, Ruff, Mypy, frontend typecheck, and frontend build on master, pull requests, and version tags. Run uv run pytest tests/test_example_workflows.py to exercise the read-only local example-workflows corpus through layout roundtrip checks plus layout-only and Optimize + Layout quality reports. The corpus is a local maintainer artifact and the test is skipped when the folder is absent. For local maintainer validation against read-only ComfyUI UI workflows, run uv run python tools/validate_local_workflows.py. For release readiness, run uv run python tools/check_release_ready.py; add --tag vX.Y.Z --github after tagging and publishing to verify remote refs, the GitHub Release, and Actions. For read-only layout quality metrics across workflow folders, run uv run python tools/report_layout_quality.py example-workflows. Add --optimize to measure the Set/Get optimizer before layout. The report includes aggregate crossing/right-to-left counts and link-category breakdowns for the laid-out result.


Project Structure

comfyui-flowforge/
├── .github/workflows/     # GitHub Actions CI
├── .python-version        # Default uv Python version for development and CI parity
├── AGENTS.md             # Repository-local agent rules
├── CHANGELOG.md          # Release history
├── docs/                 # Focused project documentation
├── flowforge/              # Python package
│   ├── __init__.py        # Package exports
│   ├── api.py             # aiohttp API server
│   ├── cli.py             # Command-line interface
│   ├── gui.py             # GUI launcher
│   ├── layout.py          # Layout algorithm (Sugiyama)
│   ├── logger.py          # Logging setup
│   ├── model.py           # Data models (Node, Link, Group, Workflow)
│   ├── optimizer.py       # High-fanout optimizer
│   └── parser.py          # ComfyUI JSON parser
├── frontend/              # Vue 3 frontend
│   ├── src/
│   │   ├── App.vue
│   │   ├── components/    # Vue components
│   │   └── stores/        # Pinia stores
│   └── ...
├── tests/                 # Test suite
│   ├── test_api.py
│   ├── test_layout.py
│   ├── test_optimizer.py
│   └── test_parser.py
├── tools/                 # Maintainer validation scripts
│   ├── build_package_assets.py
│   ├── report_layout_quality.py
│   └── validate_local_workflows.py
├── LICENSE               # MIT license
├── pyproject.toml        # Project configuration
├── start-flowforge.bat   # Windows launcher
├── start-flowforge.sh    # Linux launcher
└── README.md

License

MIT License — see LICENSE file for details.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors