Skip to main content

Overview

Custom nodes in ComfyUI allow you to extend the platform with new functionality, from simple image processing operations to complex model integrations. This guide covers the complete workflow for creating, registering, and distributing custom nodes.

Basic Node Structure

Custom nodes are Python classes that inherit from io.ComfyNode and implement specific class methods:
from typing_extensions import override
from comfy_api.latest import ComfyExtension, io

class Example(io.ComfyNode):
    """
    An example node that demonstrates the basic structure
    """
    
    @classmethod
    def define_schema(cls) -> io.Schema:
        """Define node metadata, inputs, and outputs"""
        pass
    
    @classmethod
    def execute(cls, **kwargs) -> io.NodeOutput:
        """Execute the node's main logic"""
        pass

Defining Node Schema

The define_schema() method specifies the node’s identity and interface:
@classmethod
def define_schema(cls) -> io.Schema:
    return io.Schema(
        node_id="Example",
        display_name="Example Node",
        category="Example",
        inputs=[
            io.Image.Input("image"),
            io.Int.Input(
                "int_field",
                min=0,
                max=4096,
                step=64,
                display_mode=io.NumberDisplay.number,
                lazy=True,
            ),
            io.Float.Input(
                "float_field",
                default=1.0,
                min=0.0,
                max=10.0,
                step=0.01,
                round=0.001,
                display_mode=io.NumberDisplay.number,
                lazy=True,
            ),
            io.Combo.Input(
                "print_to_screen", 
                options=["enable", "disable"]
            ),
            io.String.Input(
                "string_field",
                multiline=False,
                default="Hello world!",
                lazy=True,
            )
        ],
        outputs=[
            io.Image.Output(),
        ],
    )
The node_id must be unique across all installed custom nodes. Use descriptive names to avoid conflicts.

Input Types

ComfyUI supports various input types through the io module:
io.Int.Input(
    "param_name",
    default=100,
    min=0,
    max=4096,
    step=1,
    display_mode=io.NumberDisplay.slider
)

Output Types

Outputs are defined similarly using the .Output() variant:
outputs=[
    io.Image.Output(),
    io.Model.Output(),
    io.String.Output(),
]
Multiple outputs are supported. Return values in the same order as defined in the schema.

Execute Method

The execute() method contains your node’s main logic:
@classmethod
def execute(cls, image, string_field, int_field, float_field, print_to_screen) -> io.NodeOutput:
    if print_to_screen == "enable":
        print(f"""Your input contains:
            string_field: {string_field}
            int_field: {int_field}
            float_field: {float_field}
        """)
    
    # Process the image (example: invert)
    image = 1.0 - image
    
    return io.NodeOutput(image)
Parameter names in execute() must exactly match the input names defined in define_schema().

Lazy Evaluation

Lazy inputs are only evaluated when needed, improving performance for conditional workflows.

Marking Inputs as Lazy

io.Int.Input(
    "expensive_param",
    lazy=True,  # Won't be evaluated unless needed
    default=0
)

Controlling Lazy Evaluation

Implement check_lazy_status() to specify which lazy inputs should be evaluated:
@classmethod
def check_lazy_status(cls, image, string_field, int_field, float_field, print_to_screen):
    """
    Return a list of input names that need to be evaluated.
    
    Evaluated inputs will be passed as arguments.
    Unevaluated inputs will have the value None.
    """
    if print_to_screen == "enable":
        return ["int_field", "float_field", "string_field"]
    else:
        return []
1

Initial Call

Called with non-lazy inputs evaluated, lazy inputs as None
2

Request Evaluation

Return list of input names that need evaluation
3

Subsequent Calls

Called again once requested inputs are available
4

Complete

When empty list returned, proceed to execute()

Fingerprinting (Cache Control)

Control when your node should re-execute using fingerprinting:
@classmethod
def fingerprint_inputs(cls, image, string_field, int_field, float_field, print_to_screen):
    """
    Return a value (string/number) that changes when the node should re-execute.
    
    Useful for nodes that depend on external state (files, timestamps, etc.)
    """
    import hashlib
    import os
    
    # Example: Re-execute if file changes
    if os.path.exists(string_field):
        file_hash = hashlib.md5(open(string_field, 'rb').read()).hexdigest()
        return file_hash
    
    return ""
The core LoadImage node uses fingerprinting to re-execute when the source image file changes on disk.

Creating an Extension

Wrap your nodes in a ComfyExtension to register them:
class ExampleExtension(ComfyExtension):
    @override
    async def get_node_list(self) -> list[type[io.ComfyNode]]:
        return [
            Example,
            AnotherNode,
            YetAnotherNode,
        ]

async def comfy_entrypoint() -> ExampleExtension:
    """ComfyUI calls this to load your extension and its nodes."""
    return ExampleExtension()

Adding Custom API Routes

Extend the ComfyUI server with custom endpoints:
from aiohttp import web
from server import PromptServer

@PromptServer.instance.routes.get("/hello")
async def get_hello(request):
    return web.json_response("hello")

@PromptServer.instance.routes.post("/custom/process")
async def post_process(request):
    data = await request.json()
    # Process data
    return web.json_response({"status": "success"})
Custom routes are useful for integrating external tools, providing status endpoints, or enabling webhooks.

Frontend Extensions

Add custom JavaScript to enhance the UI:
# Set the web directory in your extension file
WEB_DIRECTORY = "./somejs"
Any .js file in that directory will be loaded by the frontend.

Example: Image Processing Node

Here’s a complete example of a custom image processing node:
from typing_extensions import override
from comfy_api.latest import ComfyExtension, io
import torch

class ImageBlur(io.ComfyNode):
    """Apply Gaussian blur to an image"""
    
    @classmethod
    def define_schema(cls) -> io.Schema:
        return io.Schema(
            node_id="ImageBlur",
            display_name="Image Blur",
            category="image/filter",
            inputs=[
                io.Image.Input("image"),
                io.Int.Input(
                    "kernel_size",
                    default=5,
                    min=1,
                    max=31,
                    step=2,  # Ensure odd numbers
                    display_mode=io.NumberDisplay.slider
                ),
                io.Float.Input(
                    "sigma",
                    default=1.0,
                    min=0.1,
                    max=10.0,
                    step=0.1
                ),
            ],
            outputs=[
                io.Image.Output(),
            ],
        )
    
    @classmethod
    def execute(cls, image, kernel_size, sigma) -> io.NodeOutput:
        import torchvision.transforms.functional as TF
        
        # Ensure kernel_size is odd
        if kernel_size % 2 == 0:
            kernel_size += 1
        
        # Apply Gaussian blur
        # Image shape: [batch, height, width, channels]
        # Convert to [batch, channels, height, width]
        image = image.permute(0, 3, 1, 2)
        blurred = TF.gaussian_blur(image, kernel_size, sigma)
        # Convert back
        blurred = blurred.permute(0, 2, 3, 1)
        
        return io.NodeOutput(blurred)

class ImageFilterExtension(ComfyExtension):
    @override
    async def get_node_list(self) -> list[type[io.ComfyNode]]:
        return [ImageBlur]

async def comfy_entrypoint() -> ImageFilterExtension:
    return ImageFilterExtension()

Example: Model Processing Node

Example of working with models:
class ModelMerger(io.ComfyNode):
    """Merge two models with a specified ratio"""
    
    @classmethod
    def define_schema(cls) -> io.Schema:
        return io.Schema(
            node_id="ModelMerger",
            display_name="Model Merger",
            category="model",
            inputs=[
                io.Model.Input("model1"),
                io.Model.Input("model2"),
                io.Float.Input(
                    "ratio",
                    default=0.5,
                    min=0.0,
                    max=1.0,
                    step=0.01,
                    display_mode=io.NumberDisplay.slider
                ),
            ],
            outputs=[
                io.Model.Output(),
            ],
        )
    
    @classmethod
    def execute(cls, model1, model2, ratio) -> io.NodeOutput:
        # Clone the first model
        merged_model = model1.clone()
        
        # Get state dicts
        state_dict1 = model1.model.state_dict()
        state_dict2 = model2.model.state_dict()
        
        # Merge weights
        merged_state = {}
        for key in state_dict1.keys():
            if key in state_dict2:
                merged_state[key] = (
                    state_dict1[key] * (1 - ratio) + 
                    state_dict2[key] * ratio
                )
            else:
                merged_state[key] = state_dict1[key]
        
        # Load merged weights
        merged_model.model.load_state_dict(merged_state)
        
        return io.NodeOutput(merged_model)

Best Practices

Type Hints

Use proper type hints for better IDE support and error detection

Error Handling

Add try-except blocks and provide meaningful error messages

Memory Management

Clean up large tensors and models when done processing

Documentation

Write clear docstrings and comments for complex logic

Lazy Inputs

Use lazy evaluation for expensive or conditional operations

Unique IDs

Ensure node_id is unique and descriptive

Debugging Tips

  • Check that comfy_entrypoint() is defined
  • Verify the extension is in the custom_nodes directory
  • Look for errors in the ComfyUI console on startup
  • Ensure node_id is unique
  • Add print statements to track execution flow
  • Check that parameter names match between schema and execute
  • Verify input/output tensor shapes and types
  • Use try-except to catch and log exceptions
  • Profile your code to find bottlenecks
  • Use lazy evaluation for expensive operations
  • Consider async operations for I/O
  • Monitor memory usage with torch.cuda.memory_summary()

Distribution

To distribute your custom node:
1

Create Repository

Create a Git repository with your extension code
2

Add README

Document installation instructions and usage
3

Include Requirements

Add requirements.txt for dependencies
4

Add Examples

Include example workflows demonstrating your nodes
5

Submit to Registry

Submit to ComfyUI custom node registry for discoverability

Memory Management

Learn about VRAM optimization for your custom nodes

Quantization

Work with quantized models in your nodes

Model Merging

Understand model merging techniques

Build docs developers (and LLMs) love