From comfyui-custom-nodes
Guides ComfyUI V3 custom node creation: inheriting ComfyNode, defining schemas with inputs/outputs, implementing execute methods. Use for new nodes or projects.
npx claudepluginhub jtydhr88/comfyui-custom-node-skills --plugin comfyui-custom-nodesThis skill uses the workspace's default tool permissions.
ComfyUI uses Python classes to define nodes. The **V3 API** is the current recommended approach. Nodes inherit from `io.ComfyNode` and define a schema + execute method.
Migrates ComfyUI V1 custom nodes to V3 API by converting INPUT_TYPES to define_schema, execution to @classmethod execute, returns to NodeOutput, and using ComfyExtension registration. Use for modernizing legacy nodes.
Installs, launches, and manages ComfyUI instances: custom nodes install/update/debug, models from CivitAI/HuggingFace, workspaces, API workflows, node conflict bisecting.
Details ComfyUI API workflow JSON format, node connections, class types, and tools like analyze_workflow for inspecting, loading, and saving workflows.
Share bugs, ideas, or general feedback.
ComfyUI uses Python classes to define nodes. The V3 API is the current recommended approach. Nodes inherit from io.ComfyNode and define a schema + execute method.
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 Custom Node",
category="my_category",
inputs=[
io.Image.Input("image"),
io.Float.Input("strength", default=1.0, min=0.0, max=1.0, step=0.01),
],
outputs=[
io.Image.Output("IMAGE"),
],
)
@classmethod
def execute(cls, image, strength):
result = image * strength
return io.NodeOutput(result)
Every V3 node requires:
io.ComfyNodedefine_schema(cls) - classmethod returning io.Schemaexecute(cls, ...) - classmethod performing the computationfrom typing_extensions import override
from comfy_api.latest import ComfyExtension, io
class ImageBrighten(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ImageBrighten", # unique identifier
display_name="Brighten Image", # shown in UI
category="image/adjust", # menu path
description="Adjusts image brightness",
inputs=[
io.Image.Input("image"),
io.Float.Input("factor", default=1.2, min=0.0, max=3.0, step=0.1),
],
outputs=[
io.Image.Output("IMAGE"),
],
)
@classmethod
def execute(cls, image, factor):
result = torch.clamp(image * factor, 0.0, 1.0)
return io.NodeOutput(result)
io.Schema(
node_id="UniqueNodeID", # required: unique string ID
display_name="Display Name", # optional: shown in UI menus
category="category/subcategory", # menu hierarchy (default "sd")
description="Node description", # optional: tooltip text
inputs=[...], # list of Input objects
outputs=[...], # list of Output objects
hidden=[...], # list of Hidden enum values
is_output_node=False, # True for nodes with side effects (save, preview)
is_experimental=False, # marks as experimental
is_deprecated=False, # marks as deprecated
is_dev_only=False, # hidden unless dev mode enabled
is_api_node=False, # marks as API-only node
is_input_list=False, # receive full lists instead of individual items
not_idempotent=False, # prevents caching
accept_all_inputs=False, # accept arbitrary inputs via **kwargs
enable_expand=False, # allow node expansion (subgraphs)
search_aliases=["alias1", "alias2"],# alternative search terms
essentials_category="Basic", # optional: Essentials tab category
price_badge=None, # optional: PriceBadge for API nodes
has_intermediate_output=False, # True for nodes with interactive UI that produce intermediate outputs
)
V3 nodes are registered via ComfyExtension and comfy_entrypoint():
from typing_extensions import override
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="my_nodes",
inputs=[io.String.Input("text", multiline=True)],
outputs=[io.String.Output()],
)
@classmethod
def execute(cls, text):
return io.NodeOutput(text.upper())
class MyExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [MyNode]
async def comfy_entrypoint() -> MyExtension:
return MyExtension()
The comfy_entrypoint() function must be defined at the module level (in the file directly imported by ComfyUI).
V1 nodes use class attributes and NODE_CLASS_MAPPINGS:
class MyNodeV1:
CATEGORY = "my_category"
FUNCTION = "execute"
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0}),
}
}
def execute(self, image, strength):
return (image * strength,)
NODE_CLASS_MAPPINGS = {"MyNodeV1": MyNodeV1}
NODE_DISPLAY_NAME_MAPPINGS = {"MyNodeV1": "My Node V1"}
| Aspect | V3 | V1 |
|---|---|---|
| Base class | io.ComfyNode | Plain class |
| Execute method | execute classmethod (fixed name) | Instance method (custom name via FUNCTION) |
| Inputs | io.Schema(inputs=[...]) | INPUT_TYPES() dict |
| Outputs | io.Schema(outputs=[...]) | RETURN_TYPES tuple |
| Return value | io.NodeOutput(...) | Plain tuple |
| Registration | ComfyExtension + comfy_entrypoint() | NODE_CLASS_MAPPINGS dict |
| State | No instance state (classmethods) | Instance state allowed |
| Hidden inputs | cls.hidden.prompt, etc. | kwargs from "hidden" dict |
node_id must be globally unique across all nodesexecute() parameters must match input IDs exactly@classmethod in V3 (no instance state)io.NodeOutput(val1, val2, ...) matching output count/ separator for hierarchy: "image/transform"_ to hide from menus: "_for_testing"comfyui-node-datatypes - Data types (IMAGE, LATENT, MASK, etc.)comfyui-node-inputs - Input configuration detailscomfyui-node-outputs - Output types and UI outputscomfyui-node-packaging - Project structure and packagingcomfyui-node-lifecycle - Execution lifecycle and caching