On this page
- Your Nodes Work Locally — Now Make Them Available to 10,000 Users With a Single Click
- What You Need Before You Open a PR
- The Four-Layer Contract: What ComfyUI Requires of Every Extension
- Structuring pyproject.toml for the ComfyUI Ecosystem
- The Publishing Workflow: Four Paths, One Goal
- Path 1: The Official Registry (Recommended)
- Path 2: The Legacy custom-node-list.json PR (Manual but Widely Supported)
- What the Manager Does When a User Installs Your Package
- Common Failure Modes and How to Avoid Them
- You Are Now a ComfyUI Extension Publisher
Your Nodes Work Locally — Now Make Them Available to 10,000 Users With a Single Click
The Problem
You have built a ComfyUI extension. It works on your machine. You could share a GitHub repo link and tell users to clone it into their custom_nodes folder manually. Most won’t bother. The ones who do will ask you why nothing happens when they forget to restart ComfyUI. A few will post issues because they cloned into the wrong directory.
The ComfyUI Manager is a package manager used by the vast majority of ComfyUI installations. It provides a searchable GUI for discovering, installing, updating, and removing custom node packages with one click. Getting your package listed there is the difference between “10 GitHub stars” and “10,000 active users.”
The Solution
This tutorial walks through the complete publishing pipeline, starting from the structure established in this repository (pyproject.toml, __init__.py, NODE_CLASS_MAPPINGS) and ending with a merged entry in the ComfyUI Manager’s custom nodes registry. No shortcuts — every step that can fail is flagged.
You’ll understand the four non-negotiable contracts ComfyUI requires of every extension, how pyproject.toml maps to the Manager’s metadata, what the Manager’s registry JSON structure looks like, and how to submit a pull request to the community registry.
What You Need Before You Open a PR
Knowledge Prerequisites
- Git basics: clone, branch, commit, push, pull request
- Python packaging concepts: what
pyproject.tomlis and why it replacedsetup.py - How ComfyUI’s
custom_nodesdirectory works (drop a folder there, restart)
Environment / Tools
Git
GitHub account
Python >= 3.9 (as declared in pyproject.toml)
Your package's GitHub repository — must be public
ComfyUI Manager source:
github.com/ltdrdata/ComfyUI-Manager
pyproject.toml in your repo:
[tool.comfy]
PublisherId = "" ← you will fill this in
DisplayName = "Workflow Iterator"
Icon = "" ← optional but recommended
This tutorial assumes your nodes are already working locally. We are only covering the publication pipeline.
The Four-Layer Contract: What ComfyUI Requires of Every Extension
Architecture Diagram
flowchart TD
A["ComfyUI starts"] --> B["Scans custom_nodes/ for subdirectories"]
B --> C["Imports __init__.py from each"]
C --> D{Does __init__.py export\nNODE_CLASS_MAPPINGS?}
D -->|No| E["Extension silently ignored"]
D -->|Yes| F["Registers nodes under their class-type keys"]
F --> G{Does __init__.py export\nNODE_DISPLAY_NAME_MAPPINGS?}
G -->|Optional| H["Uses class key as display name"]
G -->|Yes| I["Uses friendly display name in node browser"]
F --> J{Does __init__.py export\nWEB_DIRECTORY?}
J -->|Optional| K["No frontend JS loaded"]
J -->|Yes| L["Loads JS files from that directory"]
F --> M["onprompt hook registered if present"]
The Four Contracts
Every ComfyUI extension must satisfy these four requirements. The first two are mandatory; the last two are optional but nearly universal:
1. NODE_CLASS_MAPPINGS (mandatory)
A dict mapping class-type strings (used internally and in prompt JSON) 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,
}
The 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.
🔴 Danger: Class-type key collisions between extensions cause silent failures — whichever extension loaded last wins. Prefix your keys uniquely. This repo uses WI as a prefix. Choose something unlikely to collide (your GitHub handle, project abbreviation, etc.).
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 (from your package root) to a directory of JavaScript files ComfyUI will automatically load.
WEB_DIRECTORY = "./web"
Any .js file in that directory is injected into the ComfyUI frontend. Files are loaded in alphabetical order.
4. Startup hooks (optional — advanced)
Register PromptServer.instance.add_on_prompt_handler() in the module body of __init__.py. ComfyUI calls this handler before every queued execution. This is how this extension intercepts prompts:
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:
# Graceful fallback: running outside ComfyUI (tests, CI)
pass
🔵 Deep Dive: The try/except around all PromptServer imports is mandatory, not optional. Your test suite runs outside ComfyUI — from server import PromptServer would raise ModuleNotFoundError. The except block lets pytest import your package without a running ComfyUI instance. This is the standard pattern used by all well-maintained extensions.
Structuring pyproject.toml for the ComfyUI Ecosystem
The pyproject.toml in this repo is already correctly structured. Here is what each field means for publishing:
[project]
name = "comfyui-workflow-iterator" # PyPI-compatible name (kebab-case)
version = "1.0.0" # Semantic versioning
description = "A/B testing and batch parameter comparison nodes for ComfyUI"
license = { text = "MIT" }
requires-python = ">=3.9"
# Runtime dependencies — Manager's installer will pip-install these
dependencies = ["Pillow>=9.0.0"]
[project.urls]
# This URL is what the Manager uses to clone your package.
# It must be your GitHub repository root.
Repository = "https://github.com/YOUR_USERNAME/comfyui-workflow-iterator"
[tool.comfy]
# PublisherId: Your ComfyUI Manager publisher account ID.
# Leave empty until you register at registry.comfy.org
PublisherId = ""
DisplayName = "Workflow Iterator" # Shown in Manager's package list
Icon = "" # URL to a square PNG icon (optional)
The [tool.comfy] section is read by the ComfyUI CLI publishing tool (comfy node publish). The dependencies array is read by the Manager when installing — it runs pip install for each entry in the user’s ComfyUI environment.
The Publishing Workflow: Four Paths, One Goal
Path 1: The Official Registry (Recommended)
The ComfyUI Manager maintains a community registry JSON file. Getting listed here means your package appears in every user’s Manager search.
Step 1: Register as a publisher
Visit registry.comfy.org and create a publisher account. This gives you a PublisherId. Fill it into pyproject.toml.
Step 2: Install the ComfyUI CLI
pip install comfy-cli
Step 3: Publish
From your repository root:
comfy node publish
This validates your pyproject.toml, packages your code, and submits it to the registry. The first submission creates a new listing. Subsequent calls (with a bumped version) create new releases.
🔴 Danger: Once published, a version number cannot be overwritten. If you publish 1.0.0 with a bug, you must publish 1.0.1. This mirrors PyPI’s immutability guarantee. Plan your versioning strategy before your first publish.
Path 2: The Legacy custom-node-list.json PR (Manual but Widely Supported)
Before the official registry existed, the Manager used a manually curated JSON file. Many older extensions are listed only here. Submitting here is a valid alternative if you want immediate listing without a publisher account.
Step 1: Fork the Manager repo
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
The file is an array of objects. Add yours 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"]
}
Field Reference:
| Field | Required | Notes |
|---|---|---|
author | Yes | Your GitHub username |
title | Yes | Human-readable name shown in Manager |
id | Yes | Unique kebab-case identifier |
reference | Yes | GitHub repo URL — used as the canonical link |
files | Yes | Array of URLs to clone. Usually just the repo root. |
install_type | Yes | Always "git-clone" for GitHub-hosted extensions |
description | Yes | Shown in search results. Keep under 200 characters. |
tags | No | Improves discoverability in Manager search |
Step 3: Commit and open a PR
git add custom-node-list.json
git commit -m "Add Workflow Iterator nodes for A/B parameter testing"
git push origin add-workflow-iterator
Then open a pull request against ltdrdata/ComfyUI-Manager. The maintainer reviews and merges manually. Response time is typically 1–7 days.
🔵 Deep Dive: The Manager’s install_type: "git-clone" means the Manager runs git clone <url> into the user’s custom_nodes directory, then calls pip install -r requirements.txt (or reads pyproject.toml dependencies). If your package has native extensions or complex build steps, you need to also provide a pip install type entry — but pure-Python packages like this one work perfectly with git-clone.
What the Manager Does When a User Installs Your Package
Understanding this helps you structure your package correctly:
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
Critical implication: The Manager clones your main (or master) branch by default. Whatever is on that branch is what users get. Merge only stable, tested code to main. 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 and How to Avoid Them
Failure 1: Missing __all__ or wrong exports
If NODE_CLASS_MAPPINGS is not at the module level of __init__.py — if it is inside a function, an if __name__ == "__main__" block, or a try/except that fails silently — ComfyUI will import your module and find nothing to register. Your nodes simply won’t appear. The fix: always define the registration dicts at module level, unconditionally.
Failure 2: Import errors crashing ComfyUI startup
If any import in __init__.py raises an uncaught exception, ComfyUI logs the error and skips your extension. More importantly, if your extension crashes badly enough, it can prevent other extensions from loading. Always wrap unsafe imports (especially from server import PromptServer) in try/except blocks, as this repo does.
Failure 3: Dependency conflicts
Your pyproject.toml says Pillow>=9.0.0. Another extension requires Pillow==8.4.0. The Manager installs one version; someone’s extension breaks. There is 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 Pillow versions.
Failure 4: Class-type key collisions
Two extensions registering the same key (e.g., both define "SaveImage") — the second one loaded overwrites the first. The user gets unpredictable behaviour with no error. The mitigation: choose a unique prefix. WI for WorkflowIterator, AC for ComfyUI-Manager’s own tools, etc. Check the existing custom-node-list.json for naming conventions before choosing yours.
🔴 Danger: Never register a class-type key that shadows a core ComfyUI node (SaveImage, KSampler, CLIPTextEncode, etc.). This will break workflows for users who install your extension alongside the core workflow. Always use a unique prefix.
You Are Now a ComfyUI Extension Publisher
The skills you’ve built in this tutorial:
-
The four ComfyUI extension contracts:
NODE_CLASS_MAPPINGS,NODE_DISPLAY_NAME_MAPPINGS,WEB_DIRECTORY, and the onprompt hook — and which are mandatory vs. optional. -
pyproject.toml for the ComfyUI ecosystem: How
[tool.comfy]fields map to Manager metadata, howdependenciesdrives pip installs, and why theRepositoryURL is the canonical identity of your package. -
Two publishing paths: The official registry via
comfy node publish(modern, recommended) and the legacycustom-node-list.jsonPR (widely supported, manual). -
Defensive
__init__.pystructure: Why every PromptServer import must be wrapped in try/except, and why your registration dicts must always be at module level. -
Common failure modes: Class-type key collisions, silent import failures, and dependency conflicts — and the mitigation strategy for each.
Your extension is now one pull request away from 10,000 potential users.