Code Organization
File Structure
Organize your extension with a clear directory structure:my-custom-nodes/
├── __init__.py # Extension entry point
├── nodes/
│ ├── __init__.py
│ ├── image_processing.py # Image nodes
│ └── text_processing.py # Text nodes
├── web/ # Frontend extensions
│ └── my_extension.js
├── example_workflows/ # Example workflows
│ └── example.json
├── locales/ # Translations
│ └── en/
│ └── main.json
├── requirements.txt # Python dependencies
└── README.md
Module Naming
Use descriptive, unique names to avoid conflicts:class MyExtension_ImageBlur(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="MyExtension_ImageBlur",
display_name="Image Blur",
category="MyExtension/Image",
...
)
class ImageBlur(io.ComfyNode): # Too generic
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="Blur", # Likely to conflict
display_name="Blur",
category="image", # Generic category
...
)
Performance Optimization
Use Lazy Inputs
Implement lazy evaluation for expensive operations:class ExpensiveNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="ExpensiveNode",
inputs=[
io.Image.Input("image"),
io.Boolean.Input("enable_processing"),
io.Int.Input(
"iterations",
lazy=True, # Only evaluate if needed
default=10
),
],
outputs=[io.Image.Output()],
)
@classmethod
def check_lazy_status(cls, image, enable_processing, iterations):
# Only request iterations if processing is enabled
if enable_processing:
return ["iterations"]
return []
@classmethod
def execute(cls, image, enable_processing, iterations):
if not enable_processing:
return io.NodeOutput(image)
# iterations is guaranteed to be available here
for _ in range(iterations):
image = process(image)
return io.NodeOutput(image)
Implement Fingerprinting
Avoid re-execution when inputs haven’t meaningfully changed:import hashlib
class ImageLoader(io.ComfyNode):
@classmethod
def fingerprint_inputs(cls, image_path):
# Only reload if file actually changed
import os
if os.path.exists(image_path):
stat = os.stat(image_path)
return f"{image_path}:{stat.st_mtime}:{stat.st_size}"
return image_path
Memory Management
Be mindful of GPU memory:import torch
class MemoryEfficientNode(io.ComfyNode):
@classmethod
def execute(cls, large_tensor):
# Process in smaller chunks
batch_size = 8
results = []
for i in range(0, len(large_tensor), batch_size):
batch = large_tensor[i:i+batch_size]
result = process_batch(batch)
results.append(result.cpu()) # Move to CPU
# Clear GPU memory
if torch.cuda.is_available():
torch.cuda.empty_cache()
return io.NodeOutput(torch.cat(results).cuda())
User Experience
Provide Helpful Tooltips
Add clear, descriptive tooltips:io.Float.Input(
"strength",
min=0.0,
max=2.0,
default=1.0,
tooltip="Controls the strength of the effect. "
"Values > 1.0 increase intensity, < 1.0 decrease it."
)
Use Appropriate Input Types
Choose the right input type and display mode:io.Float.Input(
"blur_radius",
min=0.0,
max=100.0,
step=0.1,
display_mode=io.NumberDisplay.slider, # Visual feedback
tooltip="Blur radius in pixels"
)
io.Int.Input(
"seed",
min=0,
max=2**32 - 1,
display_mode=io.NumberDisplay.number, # Precise entry
control_after_generate=io.ControlAfterGenerate.randomize
)
Sensible Defaults
Provide good default values:io.Schema(
node_id="ImageResize",
inputs=[
io.Image.Input("image"),
io.Int.Input(
"width",
default=512, # Common size
min=64,
max=4096,
step=64
),
io.Int.Input(
"height",
default=512,
min=64,
max=4096,
step=64
),
io.Combo.Input(
"interpolation",
options=["bicubic", "bilinear", "nearest"],
default="bicubic" # Best quality
),
],
outputs=[io.Image.Output()],
)
Error Handling
Validate Inputs
Check inputs and provide clear error messages:class SafeNode(io.ComfyNode):
@classmethod
def execute(cls, image, scale_factor):
if scale_factor <= 0:
raise ValueError(
f"scale_factor must be positive, got {scale_factor}"
)
if image.shape[0] == 0:
raise ValueError("Input image batch is empty")
# Validate image dimensions
if image.shape[1] < 8 or image.shape[2] < 8:
raise ValueError(
f"Image too small: {image.shape[1]}x{image.shape[2]}. "
f"Minimum size is 8x8"
)
result = scale_image(image, scale_factor)
return io.NodeOutput(result)
Handle Edge Cases
Consider edge cases in your implementation:@classmethod
def execute(cls, images, mask=None):
# Handle optional mask
if mask is None:
mask = torch.ones_like(images[:, :, :, 0])
# Handle dimension mismatch
if mask.shape[1:] != images.shape[1:3]:
import torch.nn.functional as F
mask = F.interpolate(
mask.unsqueeze(1),
size=(images.shape[1], images.shape[2]),
mode='bilinear'
).squeeze(1)
# Handle empty batch
if images.shape[0] == 0:
return io.NodeOutput(images)
result = apply_mask(images, mask)
return io.NodeOutput(result)
Documentation
Docstrings
Document your nodes with clear docstrings:class ImageBlend(io.ComfyNode):
"""
Blends two images together using various blend modes.
Supports multiple blend modes including normal, multiply, screen,
and overlay. The blend strength can be controlled with the opacity
parameter.
The images must have the same dimensions. If they differ, the second
image will be automatically resized to match the first.
"""
@classmethod
def define_schema(cls) -> io.Schema:
...
Example Workflows
Provide example workflows inexample_workflows/:
{
"last_node_id": 3,
"nodes": [
{
"id": 1,
"type": "LoadImage",
"outputs": {"IMAGE": {"links": [1]}}
},
{
"id": 2,
"type": "MyExtension_ImageBlur",
"inputs": {"image": {"link": 1}},
"widgets_values": [5.0]
}
]
}
README
Include comprehensive README:# My Custom Nodes
## Installation
```bash
cd ComfyUI/custom_nodes/
git clone https://github.com/username/my-custom-nodes
cd my-custom-nodes
pip install -r requirements.txt
Nodes
Image Blur
Blurs an image using Gaussian blur. Inputs:- image: Input image
- radius: Blur radius (0-100)
- image: Blurred image
## Testing
### Test Your Nodes
Create unit tests for your nodes:
```python
import torch
import pytest
from .nodes import ImageBlur
def test_image_blur():
# Create test image
image = torch.rand(1, 512, 512, 3)
# Execute node
result = ImageBlur.execute(image=image, radius=5.0)
# Verify output
assert result[0].shape == image.shape
assert result[0].min() >= 0.0
assert result[0].max() <= 1.0
def test_edge_cases():
# Test with minimal image
tiny = torch.rand(1, 8, 8, 3)
result = ImageBlur.execute(image=tiny, radius=1.0)
assert result[0].shape == tiny.shape
# Test with large radius
image = torch.rand(1, 256, 256, 3)
result = ImageBlur.execute(image=image, radius=100.0)
assert result[0].shape == image.shape
Versioning and Compatibility
Version Your Extension
Use semantic versioning:__version__ = "1.2.0"
class MyExtension(ComfyExtension):
async def on_load(self) -> None:
print(f"Loading My Extension v{__version__}")
Maintain Backwards Compatibility
When updating nodes, preserve compatibility:class ImageProcessor(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
inputs=[
io.Image.Input("image"),
# New parameter with default
io.Combo.Input(
"mode",
options=["fast", "quality"],
default="fast" # Maintains old behavior
),
],
...
)
Common Patterns
Progress Reporting
Report progress for long operations:from comfy_api.latest import ComfyAPISync
class LongRunningNode(io.ComfyNode):
@classmethod
def execute(cls, image, iterations):
api = ComfyAPISync()
results = []
for i in range(iterations):
result = process_iteration(image, i)
results.append(result)
# Update progress
api.execution.set_progress(
value=i + 1,
max_value=iterations,
preview_image=result # Show preview
)
return io.NodeOutput(torch.stack(results))
Batch Processing
Handle batch inputs efficiently:@classmethod
def execute(cls, images):
# Process entire batch at once (efficient)
results = batch_process(images)
# Or process individually if needed
# results = [process_single(img) for img in images]
# results = torch.stack(results)
return io.NodeOutput(results)
Always test your nodes with different batch sizes to ensure correct behavior.
Security Considerations
Validate File Paths
Never trust user-provided paths:import os
def safe_load_file(file_path, allowed_dir):
# Resolve to absolute path
abs_path = os.path.abspath(file_path)
abs_allowed = os.path.abspath(allowed_dir)
# Check if path is within allowed directory
if not abs_path.startswith(abs_allowed):
raise ValueError(f"Access denied: {file_path}")
return abs_path
Sanitize Inputs
Validate and sanitize all user inputs:import re
@classmethod
def execute(cls, filename, content):
# Sanitize filename
safe_name = re.sub(r'[^a-zA-Z0-9_.-]', '', filename)
if not safe_name:
raise ValueError("Invalid filename")
# Limit content size
max_size = 10 * 1024 * 1024 # 10MB
if len(content) > max_size:
raise ValueError("Content too large")
# Process safely
...
Follow these best practices to create nodes that are fast, reliable, and user-friendly!