AI & Creative Tools

Write Your First ComfyUI Custom Node — Using the Schema That Is Not Deprecated Yet

Almost every tutorial teaches the V1 schema with INPUT_TYPES and NODE_CLASS_MAPPINGS. ComfyUI's own docs now say new node features will only be added to V3. Here is a working node in both, and why you should write the second one.

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_TYPES returns a dict that must contain required, and may contain optional and hidden. Each input is a tuple: data type first, then a dict of parameters. It is a @classmethod specifically so dropdown contents — the list of available checkpoints, say — can be computed at run time.
  • RETURN_TYPES is 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 needs RETURN_TYPES = ().
  • RETURN_NAMES labels the outputs; omit it and they take the lowercased types.
  • CATEGORY is where it lands in the Add Node menu. Slashes make submenus: examples/trivial.
  • FUNCTION names 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:

  1. Inputs and outputs are objects, not dictionaries. They are declared in a Schema alongside the node id, display name and category — everything that was previously scattered across a classmethod and four class attributes now lives in one place.
  2. The execution method is fixed to execute, and it is a class method. No more FUNCTION pointing at an arbitrary name.
  3. comfy_entrypoint() replaces NODE_CLASS_MAPPINGS. It returns a ComfyExtension whose get_node_list declares your nodes.
  4. 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 on self, 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

  1. Make a directory in custom_nodes/ — say custom_nodes/my_nodes/
  2. Put your node class in a .py file there
  3. Add __init__.py with the V3 comfy_entrypoint (or the V1 mappings)
  4. Restart ComfyUI — it scans at startup, so a browser refresh does nothing
  5. Watch the console during startup for import errors
  6. 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