featured image

From Local Prototype to One-Click Install: Publishing a Custom ComfyUI Node Package

Learn how to publish your ComfyUI extension to the ComfyUI Manager, making it available to thousands of users with a single click.

Published

Mon Aug 18 2025

Technologies Used

Python ComfyUI
Intermediate 25 minutes

The Gap Between “Works on My Machine” and 10,000 Users

Your ComfyUI extension works locally. You could post a GitHub link and tell people to clone it into their custom_nodes folder manually. Most won’t bother. The ones who do will ask why nothing happens when they forget to restart ComfyUI. A few will open issues because they cloned into the wrong directory.

The ComfyUI Manager is the package manager that ships with the vast majority of ComfyUI installations. It gives users a searchable GUI for discovering, installing, updating, and removing custom node packages with one click. Getting listed in the Manager is the difference between ten GitHub stars and ten thousand active users.

This tutorial covers the complete publishing pipeline — the four contracts ComfyUI requires of every extension, how pyproject.toml maps to Manager metadata, and how to submit a pull request to the community registry.

Before You Open a PR

Knowledge you need:

  • Git: clone, branch, commit, push, pull request
  • Python packaging basics: what pyproject.toml is
  • How ComfyUI’s custom_nodes directory works

What you need ready:

  • A public GitHub repository with your extension
  • Your pyproject.toml configured (I’ll show what “correctly configured” means)
  • ComfyUI Manager source: github.com/ltdrdata/ComfyUI-Manager

The Four Contracts ComfyUI Requires

Every extension must satisfy four requirements. The first two are mandatory; the third and fourth are optional but nearly universal.

1. NODE_CLASS_MAPPINGS (mandatory)

A dict mapping class-type strings to Python class objects. This is the registration table:

# From __init__.py
NODE_CLASS_MAPPINGS = {
    "WorkflowIterator":  WorkflowIteratorNode,
    "WIParameterInt":    WIParameterInt,
    "WIParameterFloat":  WIParameterFloat,
    "WIParameterString": WIParameterString,
    "WIParameterCombo":  WIParameterCombo,
    "WIParameterSeed":   WIParameterSeed,
    "WISaveImage":       WISaveImage,
    "WIGridCompositor":  WIGridCompositor,
}

These string keys are permanent. They appear in saved workflow JSON files. If you rename a key in a future version, every workflow that used the old name will fail to load. Treat these keys as a public API.

Two extensions registering the same key causes silent failures — whichever loads last wins. Prefix your keys uniquely. This repo uses WI. Choose something unlikely to collide: your GitHub handle, project abbreviation, anything distinct. Also, never register a key that shadows a core ComfyUI node (SaveImage, KSampler, CLIPTextEncode) — that breaks workflows for any user who installs your extension.

2. NODE_DISPLAY_NAME_MAPPINGS (strongly recommended)

Friendly names shown in the ComfyUI node browser. Users search by these:

NODE_DISPLAY_NAME_MAPPINGS = {
    "WorkflowIterator": "WI Workflow Iterator",
    "WIParameterInt":   "WI Parameter (Int)",
    # ...
}

3. WEB_DIRECTORY (required if you have a JS frontend)

A relative path to a directory of JavaScript files ComfyUI will automatically load into the frontend:

WEB_DIRECTORY = "./web"

4. Startup hooks (optional — advanced)

Register handlers in the module body of __init__.py before ComfyUI finishes loading:

try:
    from server import PromptServer
    from .core.iteration_state import IterationStateManager

    state_manager = IterationStateManager.instance()

    def wi_onprompt(json_data):
        try:
            return state_manager.handle_prompt(json_data)
        except Exception as e:
            logger.error(f"Workflow Iterator onprompt error: {e}", exc_info=True)
            return json_data

    PromptServer.instance.add_on_prompt_handler(wi_onprompt)
    state_manager.register_routes(PromptServer.instance.routes)

except ImportError:
    pass  # Running outside ComfyUI (tests, CI)

The try/except around all PromptServer imports is mandatory, not optional. Your test suite runs outside ComfyUI, and from server import PromptServer would raise ModuleNotFoundError in that context. The except block lets pytest import your package without a running ComfyUI instance. This pattern is standard across all well-maintained extensions.

Structuring pyproject.toml for the ComfyUI Ecosystem

[project]
name        = "comfyui-workflow-iterator"
version     = "1.0.0"
description = "A/B testing and batch parameter comparison nodes for ComfyUI"
license     = { text = "MIT" }
requires-python = ">=3.9"
dependencies = ["Pillow>=9.0.0"]  # Manager's installer will pip-install these

[project.urls]
# This URL is what the Manager uses to clone your package
Repository = "https://github.com/YOUR_USERNAME/comfyui-workflow-iterator"

[tool.comfy]
PublisherId = ""          # Fill in after registering at registry.comfy.org
DisplayName = "Workflow Iterator"
Icon        = ""          # URL to a square PNG icon (optional)

dependencies is read by the Manager when installing — it runs pip install for each entry in the user’s ComfyUI environment. The [tool.comfy] section is read by the official ComfyUI CLI publishing tool.

Two Publishing Paths

Path 1: The Official Registry

This is the recommended approach. Your package gets listed in every user’s Manager search automatically.

Step 1: Register at registry.comfy.org and get a PublisherId. Fill it into pyproject.toml.

Step 2: Install the CLI:

pip install comfy-cli

Step 3: From your repository root:

comfy node publish

This validates your pyproject.toml, packages your code, and submits to the registry. Once published, a version number can’t be overwritten. If you publish 1.0.0 with a bug, you must publish 1.0.1. Plan your versioning before your first publish.

Path 2: The Legacy custom-node-list.json PR

Before the official registry existed, the Manager used a manually curated JSON file. Many extensions are listed here. This is a valid alternative to the registry.

Step 1: Fork the Manager repo and create a branch:

git clone https://github.com/ltdrdata/ComfyUI-Manager.git
cd ComfyUI-Manager
git checkout -b add-workflow-iterator

Step 2: Add your entry to custom-node-list.json in alphabetical order by title:

{
    "author":       "YourGitHubUsername",
    "title":        "Workflow Iterator",
    "id":           "workflow-iterator",
    "reference":    "https://github.com/YOUR_USERNAME/comfyui-workflow-iterator",
    "files": [
        "https://github.com/YOUR_USERNAME/comfyui-workflow-iterator"
    ],
    "install_type": "git-clone",
    "description":  "Systematically test parameter combinations (CFG, sampler, steps, prompts) and generate labeled comparison grids. Supports cartesian and linear sweep modes.",
    "tags": ["workflow", "testing", "comparison", "batch", "grid"]
}

install_type: "git-clone" means the Manager runs git clone <url> into the user’s custom_nodes directory, then installs dependencies from pyproject.toml. This works correctly for pure-Python packages.

Step 3: Open a PR against ltdrdata/ComfyUI-Manager. The maintainer merges manually. Response time is typically 1–7 days.

What Happens When a User Installs Your Package

sequenceDiagram
    participant User
    participant Manager
    participant GitHub
    participant ComfyUI

    User->>Manager: Click "Install" on Workflow Iterator
    Manager->>GitHub: git clone https://github.com/.../comfyui-workflow-iterator custom_nodes/comfyui-workflow-iterator
    GitHub-->>Manager: Repository cloned
    Manager->>Manager: Read pyproject.toml → find dependencies
    Manager->>Manager: pip install Pillow>=9.0.0 (into ComfyUI's venv)
    Manager->>User: "Restart ComfyUI to activate"
    User->>ComfyUI: Restart
    ComfyUI->>ComfyUI: Import custom_nodes/comfyui-workflow-iterator/__init__.py
    ComfyUI->>ComfyUI: Register NODE_CLASS_MAPPINGS
    ComfyUI->>ComfyUI: Load web/workflow_iterator.js
    ComfyUI-->>User: New nodes appear in node browser

The Manager clones your main (or master) branch by default. Whatever is on that branch is what users get. Merge only stable, tested code. Use feature branches for development. Tag releases with git tag v1.0.0 so users and the Manager can pin to specific versions.

Common Failure Modes

Missing or wrongly-placed exports. If NODE_CLASS_MAPPINGS is inside a function, a conditional block, or a try/except that fails silently, ComfyUI imports your module and finds nothing to register. Your nodes don’t appear. The fix: define registration dicts at module level, unconditionally.

Import errors crashing startup. If any import in __init__.py raises an uncaught exception, ComfyUI logs the error and skips your extension. Wrap unsafe imports (especially from server import PromptServer) in try/except blocks, as this repo does.

Dependency version conflicts. Your package requires Pillow>=9.0.0. Another extension requires Pillow==8.4.0. The Manager installs one version; someone’s extension breaks. There’s no perfect solution to this in pip’s flat dependency model. The practical mitigation: specify only minimum versions (>=) rather than exact pins, and test against a range of versions.

Class-type key collisions. Two extensions registering the same key — the second overwrites the first. Users get unpredictable behavior with no error. Choose a unique prefix and check custom-node-list.json for naming conventions before committing to yours.

Your extension is one pull request away from a significant user base.

We respect your privacy.

← View All Tutorials

Related Projects

    Ask me anything!