If you use ComfyUI seriously you eventually hit something the available nodes cannot do — an arithmetic step, a format conversion, a call out to a service, a tidier version of a six-node subgraph you keep rebuilding. At that point you need a custom node, and a custom node is a Python class with about twenty lines in it.
The complication is which twenty lines. ComfyUI has two node schemas. V1, with INPUT_TYPES and NODE_CLASS_MAPPINGS, is what essentially every tutorial and blog post teaches, and it is still fully supported. V3 is the modern one, and the official documentation states plainly that future extensions to node features will only be added to the V3 schema.
So: learn V1 well enough to read other people’s nodes, and write V3.
What you need
- A working ComfyUI install you can restart
- Python familiarity to the level of “I can write a class”
- A text editor
No build step and no packaging. ComfyUI loads your code from disk at startup.
How ComfyUI finds your code
On start, ComfyUI scans the custom_nodes directory for Python modules and tries to load each one. A module here means a directory containing an __init__.py.
The detail that catches people: under V1, the module is only treated as a custom node package if it exports NODE_CLASS_MAPPINGS. If it does not, or if your code raises on import, ComfyUI carries on starting and simply reports the module as failed.
That is why your node silently does not appear. Read the Python console. The error is there; it is just not in the web UI.
The V1 node, so you can read other people’s
This is the shape you will see everywhere. The docs’ own example, the Invert Image node:
class InvertImageNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image_in": ("IMAGE", {})
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image_out",)
CATEGORY = "examples"
FUNCTION = "invert"
def invert(self, image_in):
image_out = 1 - image_in
return (image_out,)
The pieces:
INPUT_TYPESreturns a dict that must containrequired, and may containoptionalandhidden. Each input is a tuple: data type first, then a dict of parameters. It is a@classmethodspecifically so dropdown contents — the list of available checkpoints, say — can be computed at run time.RETURN_TYPESis a tuple of type strings. Mind the trailing comma:("IMAGE",)is a tuple,("IMAGE")is a string, and that single character is one of the most common first-attempt failures. A node with no outputs still needsRETURN_TYPES = ().RETURN_NAMESlabels the outputs; omit it and they take the lowercased types.CATEGORYis where it lands in the Add Node menu. Slashes make submenus:examples/trivial.FUNCTIONnames the method to call.
And the registration, in __init__.py:
from .python_file import MyCustomNode
NODE_CLASS_MAPPINGS = {
"My Custom Node": MyCustomNode
}
__all__ = ["NODE_CLASS_MAPPINGS"]
NODE_CLASS_MAPPINGS keys must be unique across the whole Comfy install, not just your package — so prefix them with something of your own.
The V3 node, which is the one to write
Same node, modern schema:
from comfy_api.latest import ComfyExtension, io
class MyNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="MyNode",
display_name="My Node",
category="my_category",
inputs=[...],
outputs=[...],
)
@classmethod
def execute(cls, ...) -> io.NodeOutput:
return io.NodeOutput(result)
class MyExtension(ComfyExtension):
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [MyNode]
async def comfy_entrypoint() -> ComfyExtension:
return MyExtension()
Four differences that matter:
- Inputs and outputs are objects, not dictionaries. They are declared in a
Schemaalongside the node id, display name and category — everything that was previously scattered across a classmethod and four class attributes now lives in one place. - The execution method is fixed to
execute, and it is a class method. No moreFUNCTIONpointing at an arbitrary name. comfy_entrypoint()replacesNODE_CLASS_MAPPINGS. It returns aComfyExtensionwhoseget_node_listdeclares your nodes.- Nodes have no state.
__init__has no effect on what is exposed — every method is a class method, and the class is sanitised before execution. If you were planning to cache something onself, plan differently.
That last point is a real constraint, and it is deliberate: it makes node behaviour depend only on inputs, which is what lets ComfyUI cache and re-execute graph nodes safely.
Pinning the API version
V3 lives on a versioned Comfy API, which is the part worth understanding before you ship anything:
from comfy_api.latest import ComfyExtension, io, ui # tracks latest
from comfy_api.v0_0_2 import ComfyExtension, io, ui # pinned
comfy_api.latest points at the newest numbered API still under development. The version before latest is the one to consider stable. Right now v0_0_2 is both current and first, which means — per the docs — more changes will be made to it without warning. When it stabilises, v0_0_3 appears and latest moves to it.
So: latest while you are experimenting on your own machine, a pinned version for anything other people install. Future schema revisions on the versioned API are promised to be backwards compatible, which is the whole reason the versioning exists.
ComfyUI also ships custom_nodes/example_node.py.example written in V3 — read it before you write anything, because it is the one example guaranteed to match your installed version.
Getting it running
- Make a directory in
custom_nodes/— saycustom_nodes/my_nodes/ - Put your node class in a
.pyfile there - Add
__init__.pywith the V3comfy_entrypoint(or the V1 mappings) - Restart ComfyUI — it scans at startup, so a browser refresh does nothing
- Watch the console during startup for import errors
- Find your node in the Add Node menu under the category you set
Iterating means editing and restarting, every time. Keep the terminal visible; it is the only place that tells you the truth.
Where to go next
Once a trivial node runs, the real work is in the data types. Images in ComfyUI are tensors with a specific layout, masks have their own conventions, and getting those wrong is the second wave of errors after the import problems. The documentation’s pages on datatypes, tensors, and images and masks are the next three to read, and the snippets page is worth skimming for patterns.
Then there are the capabilities that have no V1 equivalent worth learning: lazy evaluation, node expansion, list handling. That is where the “new features only go to V3” sentence stops being a style preference and becomes the reason to have started there.
Sources
- Custom Node Properties — ComfyUI documentation
- V3 Migration guide — ComfyUI documentation
- ComfyUI Custom Node Lifecycle — ComfyUI documentation
- Custom node walkthrough — ComfyUI documentation
- ComfyUI: Making Your Own Custom Nodes — Professor Lich on YouTube