comfyanonymous/ComfyUI
Custom-nodes system
How third-party nodes plug into ComfyUI. The custom-nodes mechanism is the reason there is a thriving extension ecosystem — and the reason the runtime has so much defensive code (hook savers, deprecation middleware, isolation helpers).
Purpose
Let anyone add nodes to ComfyUI by dropping a Python module under custom_nodes/. The system supports:
- Both V1 (
NODE_CLASS_MAPPINGS) and V3 (comfy_entrypoint() → ComfyExtension) styles. - Per-node prestartup scripts for native dependency setup.
- Whitelisting (
--whitelist-custom-nodes) and full disable (--disable-all-custom-nodes). - Optional management via
Comfy-Org/ComfyUI-Manager.
Discovery
folder_paths.get_folder_paths("custom_nodes") returns the configured custom-node directories (default <repo>/custom_nodes, plus anything from extra_model_paths.yaml).
Loading happens in two passes inside main.py:
- Prestartup —
execute_prestartup_scriptwalks each subdirectory and runsprestartup_script.pyif it exists. This is where C extensions get built and ML weights get downloaded — before anything else has imported torch. - Init —
nodes.init_extra_nodes(init_custom_nodes=True, init_api_nodes=...)(called fromstart_comfyui) imports each custom-node module and merges its registrations.
The Manager (when --enable-manager is on) hooks both passes — see comfyui_manager.prestartup() and comfyui_manager.start().
Anatomy of a custom node
A custom node is a Python module exposing one of:
V1 style
class MyNode:
@classmethod
def INPUT_TYPES(s):
return {"required": {"value": ("FLOAT", {"default": 1.0})}}
RETURN_TYPES = ("FLOAT",)
FUNCTION = "do_thing"
CATEGORY = "MyCategory"
def do_thing(self, value):
return (value * 2,)
NODE_CLASS_MAPPINGS = {"MyNode": MyNode}
NODE_DISPLAY_NAME_MAPPINGS = {"MyNode": "My Node"}V3 style
from comfy_api.latest import ComfyExtension, io
class MyNode(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="MyNode",
display_name="My Node",
category="MyCategory",
inputs=[io.Float.Input("value", default=1.0)],
outputs=[io.Float.Output()],
)
@classmethod
def execute(cls, value):
return io.NodeOutput(value * 2)
class MyExtension(ComfyExtension):
async def get_node_list(self):
return [MyNode]
async def comfy_entrypoint():
return MyExtension()The V3 style is the recommended one for new nodes. comfy_extras/ is the canonical reference — see comfy_extras/nodes_canny.py for a minimal example.
Registry
nodes.NODE_CLASS_MAPPINGS is the runtime registry: a dict[str, type] mapping class_type strings to node classes. nodes.NODE_DISPLAY_NAME_MAPPINGS is the dict[str, str] that controls the UI label.
init_extra_nodes does:
- For each
comfy_extras/nodes_*.pyand eachcustom_nodes/<dir>/: import the module. - If the module exports
NODE_CLASS_MAPPINGS, merge it into the global registry. - If the module exports
comfy_entrypoint, await it, then iterateawait ext.get_node_list()and register each node via the V3 schema → V1 shim. - Track timing per module and log a sorted list at startup for debugging.
server.PromptServer exposes the merged registry via GET /object_info.
Isolation and safety
The runtime takes several precautions because custom nodes execute arbitrary code:
- Hook breaker —
hook_breaker_ac10a0.pysaves references to a set of core functions before init and restores them between prompts, so a custom node can't permanently monkey-patch the runtime. - Deprecation middleware —
server.py'sdeprecation_warningmiddleware logs once per legacy frontend path. This catches old custom nodes hitting/scripts/uior/extensions/core/. - Origin-only middleware — blocks cross-site fetches when listening on loopback, so a malicious page can't trick the browser into queuing prompts on
127.0.0.1:8188. --disable-all-custom-nodes— useful when triaging a problem. Combine with--whitelist-custom-nodes <name>to selectively re-enable.
Frontend extensions
Custom-node packages can also ship a web/ subdirectory of JavaScript that the frontend loads. The frontend manager mounts it at /extensions/<name>/.... The legacy /scripts/ui and /extensions/core/ paths are deprecated; new extensions use the registered route.
ComfyUI-Manager interaction
When --enable-manager is set and comfyui-manager is installed (via manager_requirements.txt), the Manager:
- Discovers available custom nodes from a curated index.
- Manages installation, updates, and security checks.
- Provides a UI for browsing and one-click install.
should_be_disabled(module_path) from comfyui_manager is consulted during prestartup so disabled nodes are skipped. With --disable-manager-ui, the background features (security checks, scheduled installs) keep running but the UI/endpoints are hidden.
API node sub-system
comfy_api_nodes/ is technically a custom-node-shaped collection of built-in modules (see comfy_api_nodes package page). It is loaded via the same init_extra_nodes call but gated by --disable-api-nodes separately.
Public surface custom nodes import
The supported public surface is comfy_api.latest and the frozen comfy_api/v0_0_* versions. See comfy_api package page for what's exported.
Custom nodes also commonly import:
comfy.utils—load_torch_file, helperscomfy.model_management—get_torch_device,intermediate_device,intermediate_dtypefolder_paths— to register their own model directories
These are stable in practice but not officially versioned.
Integration points
- Loaded by
nodes.init_extra_nodesfromnodes.py, called fromstart_comfyuiinmain.py. - Registered routes are mounted on
PromptServerviaapp/custom_node_manager.py. - Hot-restored between prompts by
hook_breaker_ac10a0.py. - Optionally managed by ComfyUI-Manager.
Where to start a change
- Adding a new built-in node: drop a file in
comfy_extras/— that's the same loader path as a custom node. - Changing what custom nodes can import: update
comfy_api/latest/__init__.py. The__all__list there is the canonical surface. - New isolation primitive: add to
hook_breaker_ac10a0.pyor as a middleware inserver.py.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.