- Implement VerifierSelectionNode for creating quality verifiers - Implement InferenceScalingNode that wraps KSampler with quality checks - Add VAE decoding during sampling steps for quality assessment - Include quality verification logic using variance and edge detection - Fix noise/latent handling to match ComfyUI patterns - Add comprehensive error handling and logging - Include documentation and debugging notes
10 KiB
How to Write Nodes in ComfyUI-Inference-Scaling
This guide will walk you through creating custom nodes for ComfyUI using the modern API system.
Overview
In this ComfyUI project, nodes are created using the ComfyAPI system. Each node is a class that:
- Inherits from
IO.ComfyNode - Defines its schema (inputs, outputs, metadata) via
define_schema() - Implements its execution logic via
execute() - Is registered through a
ComfyExtensionclass
Basic Structure
1. Node Class
Every node is a class that inherits from IO.ComfyNode:
from comfy_api.latest import IO, ComfyExtension
from typing_extensions import override
from inspect import cleandoc
class MyCustomNode(IO.ComfyNode):
"""
Description of what your node does.
This docstring will appear in the UI.
"""
@classmethod
def define_schema(cls):
return IO.Schema(
node_id="MyCustomNode",
display_name="My Custom Node",
category="mycategory/subcategory",
description=cleandoc(cls.__doc__ or ""),
inputs=[
# Define inputs here
],
outputs=[
# Define outputs here
],
)
@classmethod
async def execute(cls, ...) -> IO.NodeOutput:
# Implementation here
pass
2. Schema Definition
The define_schema() method defines:
- node_id: Unique identifier (usually matches class name)
- display_name: Name shown in the UI
- category: Where it appears in the node menu (use
/for subcategories) - description: Tooltip/help text
- inputs: List of input definitions
- outputs: List of output definitions
- hidden: Optional hidden inputs (like auth tokens)
- is_api_node: Set to
Truefor API nodes
3. Input Types
Common input types available:
# String input
IO.String.Input(
"prompt",
default="",
multiline=True, # For longer text
tooltip="Help text shown on hover",
optional=True, # Makes it optional
)
# Integer input
IO.Int.Input(
"seed",
default=0,
min=0,
max=100,
step=1,
display_mode=IO.NumberDisplay.slider, # or .number
control_after_generate=True, # Shows randomize button
tooltip="Random seed",
)
# Float input
IO.Float.Input(
"strength",
default=0.5,
min=0.0,
max=1.0,
step=0.01,
tooltip="Strength value",
)
# Combo/Dropdown input
IO.Combo.Input(
"model",
options=["option1", "option2", "option3"],
default="option1",
tooltip="Select a model",
)
# Image input
IO.Image.Input(
"image",
tooltip="Input image",
optional=True,
)
# Mask input
IO.Mask.Input(
"mask",
tooltip="Mask for inpainting",
optional=True,
)
# Audio input
IO.Audio.Input(
"audio",
tooltip="Input audio",
optional=True,
)
# Video input
IO.Video.Input(
"video",
tooltip="Input video",
optional=True,
)
4. Output Types
Common output types:
# Image output
IO.Image.Output()
# Audio output
IO.Audio.Output()
# Video output
IO.Video.Output()
# String output
IO.String.Output()
# Integer output
IO.Int.Output()
# Float output
IO.Float.Output()
5. Execute Method
The execute() method is where your node's logic runs:
@classmethod
async def execute(
cls,
# Parameters match input names from define_schema
prompt: str,
seed: int = 0,
image: Optional[torch.Tensor] = None,
# ... other inputs
) -> IO.NodeOutput:
"""
Execute the node logic.
Args:
prompt: Text prompt
seed: Random seed
image: Optional image tensor (shape: [B, H, W, C])
...
Returns:
IO.NodeOutput with the result
"""
# Your implementation here
# For image outputs:
result_tensor = ... # torch.Tensor with shape [B, H, W, C]
return IO.NodeOutput(result_tensor)
# For multiple outputs:
return IO.NodeOutput(image=result_image, metadata=result_metadata)
Important Notes:
- The method is
async- useawaitfor async operations - Input parameters match the names from
define_schema() - Image tensors have shape
[Batch, Height, Width, Channels](usually RGBA) - Use
IO.NodeOutput()to return results
6. Extension Registration
To register your nodes, create an extension class:
class MyExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
return [
MyCustomNode,
AnotherNode,
# ... list all your nodes
]
async def comfy_entrypoint() -> MyExtension:
"""
Entry point function that ComfyUI calls to load your extension.
"""
return MyExtension()
Complete Example
Here's a complete example of a simple image processing node:
from io import BytesIO
import torch
import numpy as np
from PIL import Image
from typing import Optional
from typing_extensions import override
from inspect import cleandoc
from comfy_api.latest import IO, ComfyExtension
from comfy_api_nodes.util import validate_string
class ImageBrightnessNode(IO.ComfyNode):
"""
Adjusts the brightness of an input image.
"""
@classmethod
def define_schema(cls):
return IO.Schema(
node_id="ImageBrightnessNode",
display_name="Image Brightness",
category="image/processing",
description=cleandoc(cls.__doc__ or ""),
inputs=[
IO.Image.Input(
"image",
tooltip="Input image to adjust",
),
IO.Float.Input(
"brightness",
default=1.0,
min=0.0,
max=2.0,
step=0.1,
tooltip="Brightness multiplier (1.0 = no change)",
),
],
outputs=[
IO.Image.Output(),
],
)
@classmethod
async def execute(
cls,
image: torch.Tensor,
brightness: float = 1.0,
) -> IO.NodeOutput:
"""
Adjust image brightness.
Args:
image: Input image tensor [B, H, W, C]
brightness: Brightness multiplier
Returns:
Brightness-adjusted image
"""
# Ensure we have a batch dimension
if len(image.shape) == 3:
image = image.unsqueeze(0)
# Apply brightness adjustment
# Clamp values to [0, 1] range
adjusted = torch.clamp(image * brightness, 0.0, 1.0)
return IO.NodeOutput(adjusted)
class MyExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
return [
ImageBrightnessNode,
]
async def comfy_entrypoint() -> MyExtension:
return MyExtension()
API Node Example
For API nodes (nodes that call external APIs), you'll typically:
-
Use utility functions from
comfy_api_nodes.util:sync_op()- for synchronous API callspoll_op()- for polling async operationsvalidate_string()- for input validationtensor_to_bytesio()- convert image tensors to bytesbytesio_to_image_tensor()- convert bytes to image tensors
-
Include hidden inputs for authentication:
hidden=[
IO.Hidden.auth_token_comfy_org,
IO.Hidden.api_key_comfy_org,
IO.Hidden.unique_id,
],
is_api_node=True,
- Use
ApiEndpointfor API calls:
from comfy_api_nodes.util import ApiEndpoint, sync_op
response = await sync_op(
cls,
ApiEndpoint(path="/api/endpoint", method="POST"),
response_model=YourResponseModel,
data=YourRequestModel(...),
files={...}, # Optional file uploads
content_type="application/json",
)
File Organization
- Create your node file:
comfy_api_nodes/nodes_yourname.py - Follow naming conventions:
- Node classes:
YourNodeName(PascalCase) - Extension class:
YourExtension(PascalCase) - File:
nodes_yourname.py(snake_case)
- Node classes:
- Import required modules:
from comfy_api.latest import IO, ComfyExtensionfrom typing_extensions import overridefrom inspect import cleandoc
Testing Your Node
-
Start ComfyUI:
python main.py -
Check the node appears in the node menu under your specified category
-
Test the node by:
- Adding it to a workflow
- Connecting inputs
- Executing the workflow
Common Patterns
Working with Images
# Image tensor shape: [Batch, Height, Width, Channels]
# Channels are usually RGBA (4 channels)
# Convert PIL Image to tensor
pil_img = Image.open("image.png").convert("RGBA")
arr = np.asarray(pil_img).astype(np.float32) / 255.0
tensor = torch.from_numpy(arr).unsqueeze(0) # Add batch dimension
# Convert tensor to PIL Image
tensor = image.squeeze(0).cpu() # Remove batch, move to CPU
image_np = (tensor.numpy() * 255).astype(np.uint8)
pil_img = Image.fromarray(image_np)
Validation
from comfy_api_nodes.util import validate_string
# Validate string inputs
validate_string(prompt, strip_whitespace=False)
# Check optional inputs
if image is not None:
# Process image
pass
Error Handling
if some_condition:
raise Exception("Error message here")
Tips
- Use type hints - They help with IDE autocomplete and documentation
- Add tooltips - Help users understand what each input does
- Use
cleandoc()- Cleans up docstrings for display - Make inputs optional when appropriate - Improves usability
- Follow existing patterns - Look at
nodes_openai.pyornodes_stability.pyfor examples - Test thoroughly - Especially edge cases (None inputs, empty strings, etc.)
Resources
- Existing node examples:
comfy_api_nodes/nodes_*.py - ComfyAPI documentation:
comfy_api/latest/ - Utility functions:
comfy_api_nodes/util/
Next Steps
- Look at existing nodes for reference
- Start with a simple node
- Test incrementally
- Add more features as needed
Happy node writing! 🎨