On this page
- The Gap Between “Works on My Machine” and 10,000 Users
- Before You Open a PR
- The Four Contracts ComfyUI Requires
- Structuring pyproject.toml for the ComfyUI Ecosystem
- Two Publishing Paths
- Path 1: The Official Registry
- Path 2: The Legacy custom-node-list.json PR
- What Happens When a User Installs Your Package
- Common Failure Modes
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.tomlis - How ComfyUI’s
custom_nodesdirectory works
What you need ready:
- A public GitHub repository with your extension
- Your
pyproject.tomlconfigured (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.