- Added _load_model_database, _save_model_database, _update_model_database, and _get_model_name_from_database functions to storage_manager.py - Fixed ImportError that was preventing preset detection in Model Preset Loader - Model Preset Loader now correctly detects and displays available presets at ComfyUI startup - Both Preset Creator and Model Preset Loader nodes are now fully functional
372 KiB
ComfyUI Custom Node Development Guide
Overview
Custom nodes allow you to implement new features and share them with the wider community. A custom node is like any Comfy node: it takes input, does something to it, and produces an output. While some custom nodes perform highly complex tasks, many just do one thing.
Client-Server Model
Comfy runs in a client-server model:
- Server (Python): Handles all the real work - data-processing, models, image diffusion etc.
- Client (Javascript): Handles the user interface
- API Mode: Workflow sent to server by non-Comfy client (command line script, other UI)
Custom Node Categories
1. Server Side Only
- Majority of Custom Nodes run purely on the server side
- Define a Python class that specifies input/output types
- Provides a function to process inputs and produce output
2. Client Side Only
- Provide modification to the client UI
- Do not add core functionality
- May not even add new nodes to the system
3. Independent Client and Server
- Provide additional server features
- Additional (related) UI features (new widget for new data type)
- Communication handled by Comfy data flow control
4. Connected Client and Server
- Small number of cases
- UI features and server need to interact directly
Node Properties - Complete Reference
Simple Example: 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,)
Main Properties
INPUT_TYPES
- Required: Must contain
requiredkey - Optional: May include
optionaland/orhiddenkeys - Optional vs Required: Optional inputs can be left unconnected
- Format:
("TYPE", {"param": "value"})- tuple with type and parameters - Class Method: Must be
@classmethodfor runtime option computation
RETURN_TYPES
- Tuple of strings: Defines output data types
- Empty tuple:
RETURN_TYPES = ()for no outputs - Single output:
RETURN_TYPES = ("IMAGE",)- trailing comma required - Multiple outputs:
RETURN_TYPES = ("IMAGE", "STRING", "INT")
RETURN_NAMES
- Optional: Human-readable output labels
- Default: Uses RETURN_TYPES in lowercase if omitted
- Must match: Number of RETURN_TYPES
CATEGORY
- Menu location: Where node appears in Add Node menu
- Submenus: Use path format
"examples/trivial" - Custom categories:
"🤖 My Category"for emoji support
FUNCTION
- Method name: Python function called on execution
- Named arguments: Receives all required and connected optional inputs
- Default values: Provide defaults for optional inputs
- Return tuple: Must match RETURN_TYPES count
- Single output:
return (result,)- trailing comma required
Execution Control
OUTPUT_NODE
OUTPUT_NODE = True # Node is considered an output
- Default:
False(node is not an output) - Purpose: Marks nodes that produce final results
- Caching: Output nodes are always executed
IS_CHANGED
@classmethod
def IS_CHANGED(cls, input1, input2):
# Return any object for comparison
return hash(str(input1) + str(input2))
- Purpose: Control when node needs re-execution
- Default: Node changes if any input/widget changes
- Return type: Any Python object (not just bool!)
- Comparison:
is_changed != is_changed_old - Always change:
return float("NaN")(avoid if possible) - Example: File hash for LoadImage node
VALIDATE_INPUTS
@classmethod
def VALIDATE_INPUTS(cls, input1, input2):
if input1 < 0:
return "input1 must be positive"
return True
- Purpose: Validate inputs before execution
- Return:
Trueif valid, error message (str) if invalid - Constants only: Only receives constant inputs, not node outputs
- Skip validation: Use
**kwargsto receive all inputs
Advanced Validation
Validating Constants
class CustomNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": { "foo" : ("INT", {"min": 0, "max": 10}) },
}
@classmethod
def VALIDATE_INPUTS(cls, foo):
# Custom validation logic
return True
Validating Types
class AddNumbers:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input1" : ("INT,FLOAT", {"min": 0, "max": 1000}),
"input2" : ("INT,FLOAT", {"min": 0, "max": 1000})
},
}
@classmethod
def VALIDATE_INPUTS(cls, input_types):
if input_types["input1"] not in ("INT", "FLOAT"):
return "input1 must be an INT or FLOAT type"
if input_types["input2"] not in ("INT", "FLOAT"):
return "input2 must be an INT or FLOAT type"
return True
Other Attributes
INPUT_IS_LIST, OUTPUT_IS_LIST
- Purpose: Control sequential processing of data
- Advanced feature: For batch processing nodes
Hidden and Flexible Inputs
Hidden Inputs
Hidden inputs allow custom nodes to request server-side information without creating client-side widgets.
Basic Structure
@classmethod
def INPUT_TYPES(cls):
return {
"required": {...},
"optional": {...},
"hidden": {
"unique_id": "UNIQUE_ID",
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO",
}
}
UNIQUE_ID
def process(self, unique_id, ...):
# unique_id is the node's unique identifier
print(f"Node ID: {unique_id}")
return (result,)
- Purpose: Node's unique identifier
- Use case: Client-server communication
- Type: String
PROMPT
def process(self, prompt, ...):
# prompt contains the complete workflow prompt
print(f"Full prompt: {prompt}")
return (result,)
- Purpose: Complete workflow prompt
- Use case: Access to entire workflow context
- Type: Dictionary with workflow information
EXTRA_PNGINFO
def process(self, extra_pnginfo, ...):
# extra_pnginfo will be saved in PNG metadata
extra_pnginfo["custom_data"] = "my_value"
return (result,)
- Purpose: PNG metadata dictionary
- Use case: Storing custom data in saved images
- Note: Disabled if ComfyUI started with
--disable-metadata
DYNPROMPT (Advanced)
def process(self, dynprompt, ...):
# dynprompt may mutate during execution
# Only for advanced cases like loops
return (result,)
- Purpose: Dynamic prompt that can mutate
- Use case: Node expansion and loops
- Advanced: Only for complex workflows
Flexible Inputs
Custom Datatypes
# Define custom datatype
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"my_cheese": ("CHEESE", {"forceInput": True})
}
}
def process(self, my_cheese, ...):
# my_cheese can be any Python object
print(f"Cheese type: {type(my_cheese)}")
return (result,)
Key Points:
- Unique name: Use uppercase string (e.g., "CHEESE")
- Any object: Can be any Python object
- forceInput: Required to prevent widget creation
- Type safety: Client enforces type connections
Wildcard Inputs
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"anything": ("*", {})
}
}
@classmethod
def VALIDATE_INPUTS(cls, input_types):
# Skip backend validation for wildcard inputs
return True
def process(self, anything, ...):
# anything can be any type
print(f"Received: {type(anything)}")
return (result,)
Key Points:
- Universal input: Accepts any data type
- Client-side: Frontend allows any connection
- Backend validation: Use VALIDATE_INPUTS to skip
- Node responsibility: Handle any data type appropriately
Dynamically Created Inputs
class ContainsAnyDict(dict):
def __contains__(self, key):
return True
@classmethod
def INPUT_TYPES(cls):
return {
"required": {},
"optional": ContainsAnyDict()
}
def process(self, **kwargs):
# Dynamically created inputs are in kwargs
for key, value in kwargs.items():
print(f"{key}: {value}")
return (result,)
Key Points:
- Dynamic inputs: Client-side created inputs
- Arbitrary names: Any input name allowed
- kwargs capture: All dynamic inputs in **kwargs
- Pythonic trick: Credit to rgthree
Advanced Patterns
Conditional Inputs
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mode": (["simple", "advanced"], {}),
"input_data": ("*", {"forceInput": True})
}
}
def process(self, mode, input_data, **kwargs):
if mode == "simple":
# Handle simple mode
return (input_data,)
else:
# Handle advanced mode with dynamic inputs
return (kwargs.get("advanced_input", input_data),)
Metadata Handling
def process(self, image, extra_pnginfo, unique_id, **kwargs):
# Store custom metadata
if "custom_metadata" not in extra_pnginfo:
extra_pnginfo["custom_metadata"] = {}
extra_pnginfo["custom_metadata"][unique_id] = {
"processed_at": time.time(),
"node_type": self.__class__.__name__
}
return (image,)
Communication Between Nodes
# Node A: Sender
def process(self, data, extra_pnginfo, ...):
# Store data for other nodes
extra_pnginfo["shared_data"] = {
"processed_data": data,
"timestamp": time.time()
}
return (data,)
# Node B: Receiver
def process(self, image, extra_pnginfo, ...):
# Retrieve data from other nodes
shared_data = extra_pnginfo.get("shared_data", {})
if shared_data:
processed_data = shared_data.get("processed_data")
# Use the shared data
return (image,)
Best Practices
Input Validation
@classmethod
def VALIDATE_INPUTS(cls, input_types):
# Validate wildcard inputs
if "anything" in input_types:
if input_types["anything"] not in ["IMAGE", "LATENT"]:
return "anything must be IMAGE or LATENT"
return True
Error Handling
def process(self, **kwargs):
try:
# Handle dynamic inputs safely
dynamic_input = kwargs.get("dynamic_input")
if dynamic_input is not None:
# Process dynamic input
pass
except Exception as e:
print(f"Error processing dynamic input: {e}")
return (None,)
Documentation
class MyNode:
"""
My Custom Node with flexible inputs
Hidden Inputs:
unique_id: Node's unique identifier
prompt: Complete workflow prompt
extra_pnginfo: PNG metadata dictionary
Dynamic Inputs:
Any input created dynamically on the client
"""
Common Use Cases
Workflow Metadata
def process(self, image, extra_pnginfo, unique_id, ...):
# Add workflow information to metadata
extra_pnginfo["workflow_info"] = {
"node_id": unique_id,
"node_class": self.__class__.__name__,
"timestamp": time.time()
}
return (image,)
Inter-Node Communication
def process(self, data, extra_pnginfo, ...):
# Store data for downstream nodes
extra_pnginfo["upstream_data"] = data
return (data,)
Dynamic Processing
def process(self, **kwargs):
# Process any number of dynamic inputs
results = []
for key, value in kwargs.items():
if key.startswith("input_"):
processed = self.process_input(value)
results.append(processed)
return (results,)
Lazy Evaluation
Purpose
Lazy evaluation allows nodes to skip evaluating inputs that aren't needed, improving performance by avoiding unnecessary processing.
When to Use Lazy Evaluation
- Model merging: Skip loading models when ratio is 0.0 or 1.0
- Image interpolation: Skip images when mask is entirely 0.0 or 1.0
- Switch nodes: Skip inputs based on condition
- Conditional processing: Skip expensive operations when not needed
Creating Lazy Inputs
Step 1: Mark Inputs as Lazy
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image1": ("IMAGE", {"lazy": True}),
"image2": ("IMAGE", {"lazy": True}),
"mask": ("MASK", {}), # Always evaluated
}
}
Step 2: Define check_lazy_status
def check_lazy_status(self, mask, image1, image2):
# mask is always available, image1/image2 may be None
mask_min = mask.min()
mask_max = mask.max()
needed = []
# Need image1 if mask is not entirely 1.0
if image1 is None and (mask_min != 1.0 or mask_max != 1.0):
needed.append("image1")
# Need image2 if mask is not entirely 0.0
if image2 is None and (mask_min != 0.0 or mask_max != 0.0):
needed.append("image2")
return needed
Complete Example: LazyMixImages
class LazyMixImages:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image1": ("IMAGE", {"lazy": True}),
"image2": ("IMAGE", {"lazy": True}),
"mask": ("MASK", {}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "mix"
CATEGORY = "Examples"
def check_lazy_status(self, mask, image1, image2):
mask_min = mask.min()
mask_max = mask.max()
needed = []
if image1 is None and (mask_min != 1.0 or mask_max != 1.0):
needed.append("image1")
if image2 is None and (mask_min != 0.0 or mask_max != 0.0):
needed.append("image2")
return needed
def mix(self, mask, image1, image2):
mask_min = mask.min()
mask_max = mask.max()
# If mask is entirely 0.0, return image1
if mask_min == 0.0 and mask_max == 0.0:
return (image1,)
# If mask is entirely 1.0, return image2
elif mask_min == 1.0 and mask_max == 1.0:
return (image2,)
# Otherwise, interpolate
result = image1 * (1.0 - mask) + image2 * mask
return (result,)
Key Points
check_lazy_status Method
- Not a class method: Uses actual input values
- May be called multiple times: As more inputs become available
- Returns list: Names of lazy inputs still needed
- Empty list: When all required inputs are available
Lazy Input Behavior
- None values: Unavailable lazy inputs are None
- Available inputs: Non-lazy inputs always have values
- Multiple calls: Function may be called repeatedly
- Performance: Minimal cost, should be used when possible
Advanced Patterns
Conditional Model Loading
class LazyModelMerge:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model1": ("MODEL", {"lazy": True}),
"model2": ("MODEL", {"lazy": True}),
"ratio": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0}),
}
}
def check_lazy_status(self, ratio, model1, model2):
needed = []
# Only need model1 if ratio < 1.0
if model1 is None and ratio < 1.0:
needed.append("model1")
# Only need model2 if ratio > 0.0
if model2 is None and ratio > 0.0:
needed.append("model2")
return needed
def merge(self, ratio, model1, model2):
if ratio == 0.0:
return (model1,)
elif ratio == 1.0:
return (model2,)
else:
# Perform actual merging
return (merged_model,)
Switch Node Pattern
class LazySwitch:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"condition": ("BOOLEAN", {}),
"input_a": ("*", {"lazy": True}),
"input_b": ("*", {"lazy": True}),
}
}
def check_lazy_status(self, condition, input_a, input_b):
needed = []
if condition and input_a is None:
needed.append("input_a")
elif not condition and input_b is None:
needed.append("input_b")
return needed
def switch(self, condition, input_a, input_b):
return (input_a if condition else input_b,)
Execution Blocking
Purpose
Execution blocking allows nodes to prevent downstream execution when certain conditions aren't met.
ExecutionBlocker Usage
Silent Blocking
from comfy_execution.graph import ExecutionBlocker
def silent_passthrough(self, passthrough, blocked):
if blocked:
return (ExecutionBlocker(None),)
else:
return (passthrough,)
Error Message Blocking
def load_checkpoint(self, ckpt_name):
ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name)
model, clip, vae = load_checkpoint(ckpt_path)
if vae is None:
# Provide meaningful error message
vae = ExecutionBlocker(f"No VAE contained in the loaded model {ckpt_name}")
return (model, clip, vae)
ExecutionBlocker Properties
- Propagates forward: Cannot be stopped from propagating
- Silent blocking: Use
ExecutionBlocker(None) - Error messages: Use
ExecutionBlocker("message") - No backward compatibility: Use lazy evaluation instead
Best Practices
When to Use Lazy Evaluation
- Performance critical: Expensive operations that may be skipped
- Conditional logic: Inputs only needed under certain conditions
- Resource intensive: Model loading, large image processing
When to Use Execution Blocking
- Error conditions: Invalid configurations that should fail
- Missing dependencies: Required components not available
- Invalid states: Workflow configurations that won't work
Implementation Tips
def check_lazy_status(self, condition, expensive_input, ...):
# Always check if input is None first
if expensive_input is None:
# Check if input is actually needed
if condition_requires_input(condition):
return ["expensive_input"]
return [] # No more inputs needed
Common Patterns
Model Loading with Fallback
def load_model_with_fallback(self, model_name, fallback_model):
try:
model = load_model(model_name)
return (model,)
except Exception as e:
if fallback_model is not None:
return (fallback_model,)
else:
return (ExecutionBlocker(f"Failed to load {model_name}: {e}"),)
Conditional Processing
def conditional_process(self, input_data, condition, expensive_input):
if condition:
# Process with expensive input
return (process_with_expensive(input_data, expensive_input),)
else:
# Simple processing
return (simple_process(input_data),)
Node Expansion
Purpose
Node expansion allows nodes to return a subgraph of nodes that replaces the original node in the execution graph. This enables advanced features like loops and complex workflow generation.
When to Use Node Expansion
- Loops: Implementing iterative processing
- Complex workflows: Generating subgraphs dynamically
- Caching optimization: Separate caching for subgraph components
- Workflow generation: Creating workflows programmatically
Basic Node Expansion
Simple Example: Load and Merge Checkpoints
from comfy_execution.graph_utils import GraphBuilder
def load_and_merge_checkpoints(self, checkpoint_path1, checkpoint_path2, ratio):
graph = GraphBuilder()
# Create checkpoint loader nodes
checkpoint_node1 = graph.node("CheckpointLoaderSimple", checkpoint_path=checkpoint_path1)
checkpoint_node2 = graph.node("CheckpointLoaderSimple", checkpoint_path=checkpoint_path2)
# Create merge nodes
merge_model_node = graph.node("ModelMergeSimple",
model1=checkpoint_node1.out(0),
model2=checkpoint_node2.out(0),
ratio=ratio)
merge_clip_node = graph.node("ClipMergeSimple",
clip1=checkpoint_node1.out(1),
clip2=checkpoint_node2.out(1),
ratio=ratio)
return {
# Return outputs: (MODEL, CLIP, VAE)
"result": (merge_model_node.out(0), merge_clip_node.out(0), checkpoint_node1.out(2)),
"expand": graph.finalize(),
}
Requirements
Return Dictionary Format
def expand_node(self, ...):
return {
"result": (output1, output2, output3), # Tuple of outputs
"expand": graph.finalize(), # Finalized subgraph
}
Key Requirements
- result: Tuple of node outputs (can mix finalized values and node outputs)
- expand: Finalized graph for expansion
- Unique IDs: Node IDs must be unique across entire graph
- Deterministic IDs: IDs must be consistent between executions
GraphBuilder Usage
Basic Graph Creation
from comfy_execution.graph_utils import GraphBuilder
def create_subgraph(self, input_data):
graph = GraphBuilder()
# Create nodes
node1 = graph.node("NodeType1", input=input_data)
node2 = graph.node("NodeType2", input=node1.out(0))
node3 = graph.node("NodeType3", input=node2.out(0))
# Return expanded graph
return {
"result": (node3.out(0),),
"expand": graph.finalize(),
}
Advanced Graph Creation
def create_complex_subgraph(self, model, image, steps):
graph = GraphBuilder()
# Create multiple processing chains
chain1 = graph.node("ProcessA", input=image)
chain2 = graph.node("ProcessB", input=image)
# Merge chains
merge_node = graph.node("MergeNodes",
input1=chain1.out(0),
input2=chain2.out(0))
# Final processing
final_node = graph.node("FinalProcess",
input=merge_node.out(0),
model=model)
return {
"result": (final_node.out(0),),
"expand": graph.finalize(),
}
Manual Graph Creation (Without GraphBuilder)
Basic Manual Format
def manual_expansion(self, input_data):
# Generate unique prefix
prefix = GraphBuilder.alloc_prefix()
# Create graph manually
graph = {
f"{prefix}_node1": {
"class_type": "NodeType1",
"inputs": {"input": input_data}
},
f"{prefix}_node2": {
"class_type": "NodeType2",
"inputs": {"input": [f"{prefix}_node1", 0]}
}
}
return {
"result": ([f"{prefix}_node2", 0],),
"expand": graph,
}
Using GraphBuilder for ID Management
def hybrid_expansion(self, input_data):
# Use GraphBuilder for ID management
prefix = GraphBuilder.alloc_prefix()
# Load existing graph
existing_graph = load_graph_from_file("template.json")
# Add prefix to existing graph
from comfy.graph_utils import add_graph_prefix
prefixed_graph = add_graph_prefix(existing_graph, prefix)
return {
"result": ([f"{prefix}_output_node", 0],),
"expand": prefixed_graph,
}
Advanced Patterns
Loop Implementation
def loop_processor(self, input_data, iterations):
graph = GraphBuilder()
# Create initial node
current_node = graph.node("InitialProcess", input=input_data)
# Create loop nodes
for i in range(iterations):
current_node = graph.node("LoopProcess",
input=current_node.out(0),
iteration=i)
return {
"result": (current_node.out(0),),
"expand": graph.finalize(),
}
Conditional Subgraph
def conditional_expansion(self, input_data, condition):
graph = GraphBuilder()
if condition:
# Create path A
node1 = graph.node("ProcessA", input=input_data)
node2 = graph.node("ProcessA2", input=node1.out(0))
result_node = node2
else:
# Create path B
node1 = graph.node("ProcessB", input=input_data)
node2 = graph.node("ProcessB2", input=node1.out(0))
result_node = node2
return {
"result": (result_node.out(0),),
"expand": graph.finalize(),
}
Dynamic Workflow Generation
def generate_workflow(self, workflow_config):
graph = GraphBuilder()
# Parse configuration
steps = workflow_config.get("steps", [])
# Create initial node
current_node = graph.node("LoadImage", image=workflow_config["image"])
# Add each step
for step in steps:
step_type = step["type"]
step_params = step["params"]
step_params["input"] = current_node.out(0)
current_node = graph.node(step_type, **step_params)
return {
"result": (current_node.out(0),),
"expand": graph.finalize(),
}
Caching Optimization
Efficient Subgraph Caching
def optimized_expansion(self, model_link, image_link):
graph = GraphBuilder()
# Use rawLink for better caching
model_input = graph.raw_link(model_link)
image_input = graph.raw_link(image_link)
# Create processing nodes
process_node = graph.node("ProcessNode",
model=model_input,
image=image_input)
return {
"result": (process_node.out(0),),
"expand": graph.finalize(),
}
Input Declaration with rawLink
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL", {"rawLink": True}),
"image": ("IMAGE", {"rawLink": True}),
}
}
Best Practices
ID Management
def safe_expansion(self, input_data):
# Always use GraphBuilder for ID management
graph = GraphBuilder()
# Create nodes with unique IDs
node1 = graph.node("NodeType1", input=input_data)
node2 = graph.node("NodeType2", input=node1.out(0))
return {
"result": (node2.out(0),),
"expand": graph.finalize(),
}
Error Handling
def safe_expansion_with_error_handling(self, input_data):
try:
graph = GraphBuilder()
# Create subgraph
node1 = graph.node("NodeType1", input=input_data)
node2 = graph.node("NodeType2", input=node1.out(0))
return {
"result": (node2.out(0),),
"expand": graph.finalize(),
}
except Exception as e:
# Fallback to simple processing
return (simple_process(input_data),)
Performance Considerations
def performance_optimized_expansion(self, input_data):
# Use rawLink for inputs to improve caching
graph = GraphBuilder()
# Create efficient subgraph
node1 = graph.node("EfficientNode1", input=input_data)
node2 = graph.node("EfficientNode2", input=node1.out(0))
return {
"result": (node2.out(0),),
"expand": graph.finalize(),
}
Common Use Cases
Workflow Templates
def apply_workflow_template(self, image, template_name):
graph = GraphBuilder()
# Load template
template = load_workflow_template(template_name)
# Apply template to image
current_node = graph.node("LoadImage", image=image)
for step in template["steps"]:
current_node = graph.node(step["type"],
input=current_node.out(0),
**step["params"])
return {
"result": (current_node.out(0),),
"expand": graph.finalize(),
}
Batch Processing
def batch_processor(self, images, batch_size):
graph = GraphBuilder()
# Create batch processing nodes
batch_node = graph.node("BatchProcess",
images=images,
batch_size=batch_size)
return {
"result": (batch_node.out(0),),
"expand": graph.finalize(),
}
Data Lists and Sequential Processing
Purpose
ComfyUI internally represents data as lists, normally length 1. When multiple data instances need processing, ComfyUI can process them sequentially to avoid memory issues or handle different data sizes.
Length One Processing (Default)
Normal Operation
# ComfyUI internally wraps outputs in lists
def process(self, input_data):
result = process_data(input_data)
return (result,) # ComfyUI wraps this as [result]
Internal Representation
- Output:
(result,)→ ComfyUI wraps as[result] - Input: Next node receives
result(unwrapped) - Transparent: You don't need to worry about this wrapping
List Processing
Sequential Processing
When multiple data instances are processed:
- Sequential execution: Each item processed separately
- Padding: Shorter lists are padded by repeating last value
- Output lists: Same length as longest input list
- Memory efficient: Avoids VRAM issues with large batches
Example: Processing Multiple Images
# Input: [image1, image2, image3]
# ComfyUI processes each image separately
# Output: [processed_image1, processed_image2, processed_image3]
OUTPUT_IS_LIST
Purpose
Tells ComfyUI that returned lists should be treated as sequential data, not wrapped as single items.
Basic Usage
class MyListProcessor:
RETURN_TYPES = ("IMAGE", "STRING")
OUTPUT_IS_LIST = (True, False) # First output is list, second is not
def process(self, input_data):
results = []
for item in input_data:
processed = process_item(item)
results.append(processed)
return (results, "status") # results is treated as list
Key Points
- Tuple length: Must match RETURN_TYPES length
- Boolean values: True for list outputs, False for single outputs
- No wrapping: Lists are not wrapped in additional lists
- Sequential processing: Enables downstream sequential processing
INPUT_IS_LIST
Purpose
Allows nodes to receive entire lists in a single call instead of sequential processing.
Basic Usage
class MyListHandler:
INPUT_IS_LIST = True # All inputs receive lists
def process(self, images, batch_size):
# images is a list of image batches
# batch_size is a list (take first element: batch_size[0])
batch_size = batch_size[0]
# Process all images at once
return (processed_images,)
Key Points
- Node level: Affects all inputs
- List format: All inputs come as lists
- Widget values: Use
widget[0]to get actual value - Single call: Process entire list in one execution
Complete Example: ImageRebatch
class ImageRebatch:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
"batch_size": ("INT", {"default": 1, "min": 1, "max": 4096})
}
}
RETURN_TYPES = ("IMAGE",)
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
FUNCTION = "rebatch"
CATEGORY = "image/batch"
def rebatch(self, images, batch_size):
batch_size = batch_size[0] # Get actual value from list
output_list = []
all_images = []
# Flatten all image batches
for img in images: # Each img is a batch of images
for i in range(img.shape[0]): # Each i is a single image
all_images.append(img[i:i+1])
# Create new batches of requested size
for i in range(0, len(all_images), batch_size):
batch = torch.cat(all_images[i:i+batch_size], dim=0)
output_list.append(batch)
return (output_list,)
Advanced Patterns
Mixed List Processing
class MixedProcessor:
RETURN_TYPES = ("IMAGE", "STRING", "INT")
OUTPUT_IS_LIST = (True, False, True) # Mixed list/single outputs
def process(self, images, texts, numbers):
processed_images = []
status = "completed"
processed_numbers = []
for i, (img, text, num) in enumerate(zip(images, texts, numbers)):
# Process each item
processed_img = process_image(img)
processed_images.append(processed_img)
processed_numbers.append(num * 2)
return (processed_images, status, processed_numbers)
Conditional List Processing
class ConditionalListProcessor:
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
def process(self, images, condition):
condition = condition[0] # Get actual boolean value
if condition:
# Process all images
results = [process_image(img) for img in images]
else:
# Return original images
results = images
return (results,)
List Filtering
class ListFilter:
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
def process(self, images, filter_condition):
filter_condition = filter_condition[0]
if filter_condition:
# Filter images based on some criteria
filtered_images = [img for img in images if meets_criteria(img)]
else:
filtered_images = images
return (filtered_images,)
Best Practices
Memory Management
class MemoryEfficientProcessor:
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
def process(self, images, batch_size):
batch_size = batch_size[0]
results = []
# Process in smaller batches to avoid memory issues
for i in range(0, len(images), batch_size):
batch = images[i:i+batch_size]
processed_batch = process_batch(batch)
results.extend(processed_batch)
return (results,)
Error Handling
class RobustListProcessor:
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
def process(self, images, fallback_image):
fallback_image = fallback_image[0]
results = []
for img in images:
try:
processed = process_image(img)
results.append(processed)
except Exception as e:
print(f"Error processing image: {e}")
results.append(fallback_image)
return (results,)
Performance Optimization
class OptimizedListProcessor:
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
def process(self, images, use_gpu):
use_gpu = use_gpu[0]
if use_gpu and torch.cuda.is_available():
# Process on GPU
results = [process_image_gpu(img) for img in images]
else:
# Process on CPU
results = [process_image_cpu(img) for img in images]
return (results,)
Common Use Cases
Batch Size Management
class BatchManager:
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
def process(self, images, target_batch_size):
target_batch_size = target_batch_size[0]
results = []
# Process images in target batch sizes
for i in range(0, len(images), target_batch_size):
batch = images[i:i+target_batch_size]
processed_batch = process_batch(batch)
results.append(processed_batch)
return (results,)
Size Normalization
class SizeNormalizer:
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
def process(self, images, target_size):
target_size = target_size[0]
results = []
for img in images:
# Normalize to target size
normalized = resize_image(img, target_size)
results.append(normalized)
return (results,)
Quality Filtering
class QualityFilter:
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
def process(self, images, quality_threshold):
quality_threshold = quality_threshold[0]
results = []
for img in images:
if get_image_quality(img) >= quality_threshold:
results.append(img)
return (results,)
Annotated Examples - Practical Code Fragments
Images and Masks
Load an Image
# Load an image into a batch of size 1 (based on LoadImage source code)
from PIL import Image, ImageOps
import numpy as np
import torch
def load_image(image_path):
i = Image.open(image_path)
i = ImageOps.exif_transpose(i) # Handle EXIF orientation
# Convert 16-bit grayscale to 8-bit
if i.mode == 'I':
i = i.point(lambda i: i * (1 / 255))
# Convert to RGB
image = i.convert("RGB")
# Convert to numpy and normalize
image = np.array(image).astype(np.float32) / 255.0
# Convert to torch tensor and add batch dimension
image = torch.from_numpy(image)[None,] # [1, H, W, C]
return image
Save an Image Batch
# Save a batch of images (based on SaveImage source code)
from PIL import Image
import numpy as np
def save_image_batch(images, base_path):
for (batch_number, image) in enumerate(images):
# Convert tensor to numpy and scale to 0-255
i = 255. * image.cpu().numpy()
# Clip values and convert to uint8
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
# Create filepath with batch number
filepath = f"{base_path}_{batch_number:04d}.png"
img.save(filepath)
Invert a Mask
# Inverting a mask (masks are normalized to [0,1])
def invert_mask(mask):
return 1.0 - mask
Convert Mask to Image Shape
# Convert mask to [B, H, W, C] format with C=1
def mask_to_image_shape(mask):
if len(mask.shape) == 2: # [H, W]
mask = mask[None, :, :, None] # [1, H, W, 1]
elif len(mask.shape) == 3 and mask.shape[2] == 1: # [H, W, C]
mask = mask[None, :, :, :] # [1, H, W, 1]
elif len(mask.shape) == 3: # [B, H, W]
mask = mask[:, :, :, None] # [B, H, W, 1]
return mask
Using Masks as Transparency Layers
# Create RGBA image using mask as transparency layer
def create_rgba_image(rgb_image, mask):
# Invert mask back to original transparency layer
mask = 1.0 - mask
# Unsqueeze the C (channels) dimension
mask = mask.unsqueeze(-1)
# Concatenate along the C dimension
rgba_image = torch.cat((rgb_image, mask), dim=-1)
return rgba_image
Noise
Creating Noise Variations
# Create a noise object that mixes noise from two sources
class Noise_MixedNoise:
def __init__(self, noise1, noise2, weight2):
self.noise1 = noise1
self.noise2 = noise2
self.weight2 = weight2
@property
def seed(self):
return self.noise1.seed
def generate_noise(self, input_latent: torch.Tensor) -> torch.Tensor:
noise1 = self.noise1.generate_noise(input_latent)
noise2 = self.noise2.generate_noise(input_latent)
return noise1 * (1.0 - self.weight2) + noise2 * self.weight2
Advanced Image Processing Examples
Image Resizing
def resize_image(image, target_size):
# image: [B, H, W, C]
# target_size: (width, height)
# Convert to [B, C, H, W] for interpolation
image_chw = image.permute(0, 3, 1, 2)
# Resize using torch.nn.functional.interpolate
resized = torch.nn.functional.interpolate(
image_chw,
size=target_size,
mode='bilinear',
align_corners=False
)
# Convert back to [B, H, W, C]
return resized.permute(0, 2, 3, 1)
Image Cropping
def crop_image(image, x, y, width, height):
# image: [B, H, W, C]
# x, y: top-left corner
# width, height: crop dimensions
return image[:, y:y+height, x:x+width, :]
Image Padding
def pad_image(image, padding):
# image: [B, H, W, C]
# padding: (top, bottom, left, right)
return torch.nn.functional.pad(
image,
(padding[2], padding[3], padding[0], padding[1]),
mode='constant',
value=0
)
Mask Processing Examples
Mask Thresholding
def threshold_mask(mask, threshold=0.5):
# Convert continuous mask to binary
return (mask > threshold).float()
Mask Erosion
def erode_mask(mask, kernel_size=3):
# Simple erosion using max pooling
# Convert to [B, C, H, W] for convolution
mask_chw = mask.permute(0, 3, 1, 2)
# Apply max pooling (erosion)
eroded = torch.nn.functional.max_pool2d(
mask_chw,
kernel_size=kernel_size,
stride=1,
padding=kernel_size//2
)
# Convert back to [B, H, W, C]
return eroded.permute(0, 2, 3, 1)
Mask Dilation
def dilate_mask(mask, kernel_size=3):
# Simple dilation using min pooling on inverted mask
# Convert to [B, C, H, W] for convolution
mask_chw = mask.permute(0, 3, 1, 2)
# Invert mask for min pooling
inverted = 1.0 - mask_chw
# Apply min pooling (dilation on inverted)
dilated_inverted = torch.nn.functional.max_pool2d(
inverted,
kernel_size=kernel_size,
stride=1,
padding=kernel_size//2
)
# Invert back
dilated = 1.0 - dilated_inverted
# Convert back to [B, H, W, C]
return dilated.permute(0, 2, 3, 1)
Tensor Manipulation Examples
Batch Operations
def batch_mean(images):
# images: [B, H, W, C]
return torch.mean(images, dim=0, keepdim=True) # [1, H, W, C]
def batch_std(images):
# images: [B, H, W, C]
return torch.std(images, dim=0, keepdim=True) # [1, H, W, C]
Channel Operations
def channel_mean(image):
# image: [B, H, W, C]
return torch.mean(image, dim=-1, keepdim=True) # [B, H, W, 1]
def channel_std(image):
# image: [B, H, W, C]
return torch.std(image, dim=-1, keepdim=True) # [B, H, W, 1]
Normalization
def normalize_image(image, mean=0.5, std=0.5):
# Normalize image to [0, 1] range
return (image - mean) / std
def denormalize_image(image, mean=0.5, std=0.5):
# Denormalize image from [0, 1] range
return image * std + mean
Color Space Conversions
RGB to Grayscale
def rgb_to_grayscale(image):
# image: [B, H, W, 3]
# Standard RGB to grayscale conversion
weights = torch.tensor([0.299, 0.587, 0.114], device=image.device)
grayscale = torch.sum(image * weights, dim=-1, keepdim=True)
return grayscale # [B, H, W, 1]
Grayscale to RGB
def grayscale_to_rgb(image):
# image: [B, H, W, 1]
# Replicate grayscale channel to RGB
return image.repeat(1, 1, 1, 3) # [B, H, W, 3]
Memory Management Examples
Efficient Batch Processing
def process_large_batch(images, batch_size=4):
results = []
for i in range(0, len(images), batch_size):
batch = images[i:i+batch_size]
# Process batch
processed_batch = process_batch(batch)
results.append(processed_batch)
# Clear cache if needed
if torch.cuda.is_available():
torch.cuda.empty_cache()
return torch.cat(results, dim=0)
Memory-Efficient Resizing
def resize_large_image(image, target_size):
# Process in chunks to avoid memory issues
B, H, W, C = image.shape
if H * W > 1024 * 1024: # Large image
# Process in smaller chunks
chunk_size = 512
chunks = []
for y in range(0, H, chunk_size):
for x in range(0, W, chunk_size):
chunk = image[:, y:y+chunk_size, x:x+chunk_size, :]
resized_chunk = resize_image(chunk, target_size)
chunks.append(resized_chunk)
# Reconstruct image
return reconstruct_from_chunks(chunks, target_size)
else:
return resize_image(image, target_size)
Error Handling Examples
Safe Image Loading
def safe_load_image(image_path):
try:
image = load_image(image_path)
return image
except Exception as e:
print(f"Error loading image {image_path}: {e}")
# Return a default image
return torch.zeros((1, 512, 512, 3))
Safe Tensor Operations
def safe_tensor_operation(tensor, operation):
try:
result = operation(tensor)
return result
except Exception as e:
print(f"Error in tensor operation: {e}")
# Return original tensor or handle error
return tensor
Performance Optimization Examples
GPU Memory Management
def gpu_optimized_processing(image):
if torch.cuda.is_available():
# Move to GPU
image = image.cuda()
# Process on GPU
result = process_on_gpu(image)
# Move back to CPU
result = result.cpu()
# Clear GPU cache
torch.cuda.empty_cache()
return result
else:
return process_on_cpu(image)
Efficient Data Types
def optimize_data_types(image):
# Use appropriate data types for memory efficiency
if image.dtype == torch.float64:
image = image.float() # Convert to float32
# Use half precision if supported
if torch.cuda.is_available() and torch.cuda.is_bf16_supported():
image = image.half()
return image
Working with torch.Tensor
Purpose
All core number crunching in ComfyUI is done by PyTorch. Understanding torch.Tensor is essential for manipulating images, latents, and masks in custom nodes.
What is a Tensor?
Mathematical Definition
- Tensor: Mathematical generalization of vector or matrix to any number of dimensions
- Rank: Number of dimensions (vector=1, matrix=2)
- Shape: Size of each dimension
- Example: RGB image [H,W,3] → ComfyUI batch [B,H,W,C]
ComfyUI Tensor Conventions
# ComfyUI tensor shapes
image_tensor.shape # [B, H, W, C] - batch, height, width, channels
latent_tensor.shape # [B, C, H, W] - batch, channels, height, width
mask_tensor.shape # [B, H, W] or [H, W] - batch, height, width
Tensor Manipulation
squeeze, unsqueeze, and reshape
# Squeeze: Remove dimensions of size 1
tensor = torch.randn(1, 512, 512, 3) # [1, H, W, C]
squeezed = tensor.squeeze(0) # [H, W, C] - removes batch dimension
# Unsqueeze: Add dimensions of size 1
tensor = torch.randn(512, 512, 3) # [H, W, C]
unsqueezed = tensor.unsqueeze(0) # [1, H, W, C] - adds batch dimension
# Reshape: Change tensor shape
tensor = torch.randn(1, 512, 512, 3) # [1, H, W, C]
reshaped = tensor.reshape(1, -1, 3) # [1, H*W, C] - flatten height/width
Common Shape Operations
# Add batch dimension
def add_batch_dim(tensor):
if len(tensor.shape) == 3: # [H, W, C]
return tensor.unsqueeze(0) # [1, H, W, C]
return tensor
# Remove batch dimension
def remove_batch_dim(tensor):
if len(tensor.shape) == 4) and tensor.shape[0] == 1:
return tensor.squeeze(0) # [H, W, C]
return tensor
# Convert between channel formats
def channel_last_to_first(tensor):
# [B, H, W, C] -> [B, C, H, W]
return tensor.permute(0, 3, 1, 2)
def channel_first_to_last(tensor):
# [B, C, H, W] -> [B, H, W, C]
return tensor.permute(0, 2, 3, 1)
Important Notation
Slice Notation
# Basic slicing
tensor = torch.randn(2, 512, 512, 3) # [B, H, W, C]
# Get first batch
first_batch = tensor[0, :, :, :] # [H, W, C]
# Get specific region
region = tensor[:, 100:200, 100:200, :] # [B, 100, 100, C]
# Get specific channel
red_channel = tensor[:, :, :, 0] # [B, H, W]
Advanced Slicing
# None for dimension insertion
tensor = torch.randn(512, 512, 3) # [H, W, C]
expanded = tensor[:, None, :, :] # [H, 1, W, C] - insert dimension
# Ellipsis for unspecified dimensions
tensor = torch.randn(2, 512, 512, 3) # [B, H, W, C]
first_batch = tensor[0, ...] # [H, W, C] - same as [0, :, :, :]
# Reshape with -1 for automatic calculation
tensor = torch.randn(2, 512, 512, 3) # [B, H, W, C]
flattened = tensor.reshape(2, -1) # [B, H*W*C] - flatten spatial and channel dims
Elementwise Operations
Basic Operations
# Elementwise arithmetic
a = torch.tensor([1.0, 2.0])
b = torch.tensor([3.0, 2.0])
# Addition
result = a + b # tensor([4., 4.])
# Multiplication
result = a * b # tensor([3., 4.])
# Division
result = a / b # tensor([0.3333, 1.0000])
# Comparison
result = a == b # tensor([False, True])
result = a == 1 # tensor([True, False])
Broadcasting
# Scalar operations
tensor = torch.randn(2, 512, 512, 3)
scaled = tensor * 2.0 # Multiply all elements by 2
normalized = tensor / 255.0 # Normalize to [0, 1] range
# Shape broadcasting
a = torch.randn(2, 1, 1, 3) # [B, 1, 1, C]
b = torch.randn(512, 512, 3) # [H, W, C]
result = a + b # Broadcasts to [B, H, W, C]
Tensor Truthiness
Boolean Operations
# ❌ Wrong - ambiguous for multi-element tensors
tensor = torch.tensor([1.0, 2.0])
if tensor: # RuntimeError: Boolean value ambiguous
pass
# ✅ Correct - use .all() or .any()
tensor = torch.tensor([1.0, 2.0])
if tensor.all(): # All elements are truthy
pass
if tensor.any(): # Any element is truthy
pass
# ✅ Correct - check for None
if tensor is not None: # Check if tensor exists
pass
Practical Examples
def safe_tensor_operation(tensor):
# Check if tensor exists
if tensor is not None:
# Check if all values are valid
if not torch.isnan(tensor).any():
return process_tensor(tensor)
else:
print("Tensor contains NaN values")
return None
def mask_validation(mask):
# Check if mask is valid
if mask is not None and mask.any():
# Mask has some non-zero values
return True
return False
Common Tensor Operations
Image Processing
def normalize_image(image):
# Normalize to [0, 1] range
return (image - image.min()) / (image.max() - image.min())
def clamp_image(image, min_val=0.0, max_val=1.0):
# Clamp values to range
return torch.clamp(image, min_val, max_val)
def resize_tensor(tensor, target_size):
# Resize tensor using interpolation
return torch.nn.functional.interpolate(
tensor,
size=target_size,
mode='bilinear',
align_corners=False
)
Mask Operations
def threshold_mask(mask, threshold=0.5):
# Convert to binary mask
return (mask > threshold).float()
def invert_mask(mask):
# Invert mask values
return 1.0 - mask
def combine_masks(mask1, mask2, operation='add'):
if operation == 'add':
return torch.clamp(mask1 + mask2, 0, 1)
elif operation == 'multiply':
return mask1 * mask2
elif operation == 'max':
return torch.max(mask1, mask2)
Batch Operations
def batch_mean(images):
# Calculate mean across batch
return torch.mean(images, dim=0, keepdim=True)
def batch_std(images):
# Calculate standard deviation across batch
return torch.std(images, dim=0, keepdim=True)
def batch_concat(images):
# Concatenate images along batch dimension
return torch.cat(images, dim=0)
Memory Management
Efficient Tensor Operations
def memory_efficient_processing(tensor):
# Use in-place operations when possible
tensor.mul_(0.5) # In-place multiplication
# Clear cache if needed
if torch.cuda.is_available():
torch.cuda.empty_cache()
return tensor
def chunk_processing(tensor, chunk_size=512):
# Process large tensors in chunks
results = []
for i in range(0, tensor.shape[0], chunk_size):
chunk = tensor[i:i+chunk_size]
processed_chunk = process_chunk(chunk)
results.append(processed_chunk)
return torch.cat(results, dim=0)
Data Type Optimization
def optimize_tensor_dtype(tensor):
# Use appropriate data types
if tensor.dtype == torch.float64:
tensor = tensor.float() # Convert to float32
# Use half precision if supported
if torch.cuda.is_available() and torch.cuda.is_bf16_supported():
tensor = tensor.half()
return tensor
Error Handling
Safe Tensor Operations
def safe_tensor_operation(tensor, operation):
try:
# Check tensor validity
if tensor is None:
raise ValueError("Tensor is None")
if torch.isnan(tensor).any():
raise ValueError("Tensor contains NaN values")
# Perform operation
result = operation(tensor)
return result
except Exception as e:
print(f"Error in tensor operation: {e}")
return None
def validate_tensor_shape(tensor, expected_shape):
if tensor.shape != expected_shape:
raise ValueError(f"Expected shape {expected_shape}, got {tensor.shape}")
return True
Performance Tips
GPU Operations
def gpu_optimized_processing(tensor):
if torch.cuda.is_available():
# Move to GPU
tensor = tensor.cuda()
# Process on GPU
result = process_on_gpu(tensor)
# Move back to CPU
result = result.cpu()
return result
else:
return process_on_cpu(tensor)
Efficient Broadcasting
def efficient_broadcasting(tensor1, tensor2):
# Ensure compatible shapes
if tensor1.shape != tensor2.shape:
# Broadcast to compatible shapes
tensor1 = tensor1.unsqueeze(-1) # Add dimension
tensor2 = tensor2.unsqueeze(0) # Add dimension
return tensor1 + tensor2
Working with the UI - JavaScript Extensions
Purpose
ComfyUI can be extended through JavaScript to modify the client interface, add custom widgets, and enhance user experience.
Extending the Comfy Client
Three-Step Process
- Export WEB_DIRECTORY from Python module
- Place .js files in the directory
- Register extension using
app.registerExtension
Step 1: Exporting WEB_DIRECTORY
Python Module Setup
# __init__.py
from .my_node import MyCustomNode
NODE_CLASS_MAPPINGS = {
"My Custom Node": MyCustomNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"My Custom Node": "🎯 My Custom Node",
}
WEB_DIRECTORY = "./js" # JavaScript files directory
__all__ = [
"NODE_CLASS_MAPPINGS",
"NODE_DISPLAY_NAME_MAPPINGS",
"WEB_DIRECTORY"
]
Directory Structure
my_custom_node/
├── __init__.py # Python module entry point
├── my_node.py # Node implementations
├── requirements.txt # Dependencies
└── js/ # JavaScript extensions
├── my_extension.js # Main extension file
└── widgets.js # Custom widgets
Step 2: Including .js Files
File Loading
- Automatic loading: All .js files in WEB_DIRECTORY are loaded
- No specification needed: Files are loaded automatically
- Order matters: Files are loaded in alphabetical order
- Only .js files: Other resources must be added programmatically
Resource Access
// Access CSS files programmatically
const cssPath = "extensions/custom_node_subfolder/style.css";
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = cssPath;
document.head.appendChild(link);
Step 3: Registering an Extension
Basic Extension Structure
import { app } from "../../scripts/app.js";
app.registerExtension({
name: "my.unique.extension.name",
async setup() {
console.log("Extension setup complete!");
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// Called before node is registered
},
async afterRegisterNodeDef(nodeType, nodeData, app) {
// Called after node is registered
}
});
Complete Example
import { app } from "../../scripts/app.js";
app.registerExtension({
name: "comfyui.custom.extension",
async setup() {
console.log("Custom extension loaded!");
this.addCustomStyles();
this.setupEventListeners();
},
addCustomStyles() {
const style = document.createElement("style");
style.textContent = `
.custom-widget {
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
border-radius: 8px;
padding: 10px;
margin: 5px;
}
`;
document.head.appendChild(style);
},
setupEventListeners() {
// Add custom event listeners
document.addEventListener("customEvent", (event) => {
console.log("Custom event received:", event.detail);
});
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeType === "MyCustomNode") {
// Modify node before registration
nodeData.category = "Custom/My Category";
}
},
async afterRegisterNodeDef(nodeType, nodeData, app) {
if (nodeType === "MyCustomNode") {
// Add custom functionality after registration
this.addCustomWidget(nodeType, nodeData);
}
},
addCustomWidget(nodeType, nodeData) {
// Add custom widget functionality
console.log(`Added custom widget for ${nodeType}`);
}
});
Available Hooks
Extension Lifecycle Hooks
app.registerExtension({
name: "my.extension",
// Called when extension is loaded
async setup() {
console.log("Extension setup");
},
// Called before ComfyUI starts
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// Modify node before registration
},
// Called after ComfyUI starts
async afterRegisterNodeDef(nodeType, nodeData, app) {
// Add functionality after registration
},
// Called when workflow is executed
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// Pre-execution setup
},
// Called after workflow execution
async afterRegisterNodeDef(nodeType, nodeData, app) {
// Post-execution cleanup
}
});
Node-Specific Hooks
app.registerExtension({
name: "node.customizer",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeType === "MyCustomNode") {
// Customize node appearance
nodeData.category = "Custom/My Category";
nodeData.color = "#ff6b6b";
nodeData.title = "🎯 My Custom Node";
}
},
async afterRegisterNodeDef(nodeType, nodeData, app) {
if (nodeType === "MyCustomNode") {
// Add custom widgets
this.addCustomControls(nodeType, nodeData);
}
},
addCustomControls(nodeType, nodeData) {
// Add custom UI controls
const controls = document.createElement("div");
controls.className = "custom-controls";
controls.innerHTML = `
<button onclick="customFunction()">Custom Action</button>
`;
// Add to node interface
}
});
Custom Widgets
Creating Custom Widgets
app.registerExtension({
name: "custom.widgets",
async setup() {
this.addCustomWidgets();
},
addCustomWidgets() {
// Add custom widget types
LiteGraph.registerNodeType("CustomWidget", {
title: "Custom Widget",
category: "Custom",
onAdded(graph) {
this.addInput("input", "number");
this.addOutput("output", "number");
},
onExecute() {
// Custom widget logic
const input = this.getInputData(0);
this.setOutputData(0, input * 2);
}
});
}
});
Interactive Widgets
app.registerExtension({
name: "interactive.widgets",
async setup() {
this.createInteractiveWidgets();
},
createInteractiveWidgets() {
// Create interactive elements
const widget = document.createElement("div");
widget.className = "interactive-widget";
widget.innerHTML = `
<input type="range" min="0" max="100" value="50" id="slider">
<span id="value">50</span>
`;
const slider = widget.querySelector("#slider");
const value = widget.querySelector("#value");
slider.addEventListener("input", (e) => {
value.textContent = e.target.value;
// Trigger custom event
document.dispatchEvent(new CustomEvent("sliderChanged", {
detail: { value: e.target.value }
}));
});
// Add to ComfyUI interface
document.body.appendChild(widget);
}
});
Event Handling
Custom Events
app.registerExtension({
name: "event.handler",
async setup() {
this.setupEventHandling();
},
setupEventHandling() {
// Listen for custom events
document.addEventListener("customEvent", (event) => {
console.log("Custom event received:", event.detail);
this.handleCustomEvent(event.detail);
});
// Listen for ComfyUI events
app.addEventListener("nodeAdded", (event) => {
console.log("Node added:", event.detail);
});
app.addEventListener("workflowExecuted", (event) => {
console.log("Workflow executed:", event.detail);
});
},
handleCustomEvent(detail) {
// Handle custom event
console.log("Handling custom event:", detail);
}
});
Node Events
app.registerExtension({
name: "node.events",
async setup() {
this.setupNodeEvents();
},
setupNodeEvents() {
// Listen for node-specific events
document.addEventListener("nodeSelected", (event) => {
const node = event.detail;
this.highlightNode(node);
});
document.addEventListener("nodeDeselected", (event) => {
const node = event.detail;
this.unhighlightNode(node);
});
},
highlightNode(node) {
// Add highlighting to node
node.addClass("highlighted");
},
unhighlightNode(node) {
// Remove highlighting from node
node.removeClass("highlighted");
}
});
Advanced Features
Custom Node Categories
app.registerExtension({
name: "custom.categories",
async setup() {
this.addCustomCategories();
},
addCustomCategories() {
// Add custom category styles
const style = document.createElement("style");
style.textContent = `
.litegraph .lgraphcanvas .node[data-type="MyCustomNode"] {
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
border: 2px solid #ff6b6b;
border-radius: 10px;
}
.litegraph .lgraphcanvas .node[data-type="MyCustomNode"]:hover {
box-shadow: 0 0 20px rgba(255, 107, 107, 0.5);
}
`;
document.head.appendChild(style);
}
});
Dynamic UI Updates
app.registerExtension({
name: "dynamic.ui",
async setup() {
this.setupDynamicUpdates();
},
setupDynamicUpdates() {
// Update UI based on node changes
setInterval(() => {
this.updateNodeStatus();
}, 1000);
},
updateNodeStatus() {
// Update node status indicators
const nodes = document.querySelectorAll(".node");
nodes.forEach(node => {
const status = this.getNodeStatus(node);
this.updateNodeIndicator(node, status);
});
},
getNodeStatus(node) {
// Determine node status
return "active"; // or "inactive", "error", etc.
},
updateNodeIndicator(node, status) {
// Update visual indicator
node.setAttribute("data-status", status);
}
});
Best Practices
Error Handling
app.registerExtension({
name: "robust.extension",
async setup() {
try {
this.initializeExtension();
} catch (error) {
console.error("Extension initialization failed:", error);
}
},
initializeExtension() {
// Extension initialization code
console.log("Extension initialized successfully");
}
});
Performance Optimization
app.registerExtension({
name: "optimized.extension",
async setup() {
this.setupOptimizedExtension();
},
setupOptimizedExtension() {
// Use debouncing for frequent events
this.debouncedUpdate = this.debounce(this.updateUI, 100);
// Use event delegation for efficiency
document.addEventListener("click", (event) => {
if (event.target.matches(".custom-button")) {
this.handleButtonClick(event.target);
}
});
},
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
});
ComfyUI Hooks - Extension Lifecycle
Purpose
ComfyUI provides a comprehensive hook system that allows extensions to modify client behavior at various points during execution. Understanding these hooks is essential for creating powerful, well-integrated extensions.
Extension Hooks Overview
Hook Invocation
- Synchronous:
#invokeExtensions(method, ...args) - Asynchronous:
#invokeExtensionsAsync(method, ...args) - All extensions: Hooks are called on all registered extensions
- Method presence: Only called if the method exists on the extension
Commonly Used Hooks
beforeRegisterNodeDef()
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// Called once for each node type
// nodeType: Template for all nodes of this type
// nodeData: Node definition from Python code
// app: Main Comfy app object
}
Purpose: Modify node behavior before registration Usage: Most common hook for node customization Parameters:
nodeType: Node template (modify prototype for all instances)nodeData: Node metadata from Pythonapp: ComfyUI app reference
Method Hijacking Pattern
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeType.comfyClass === "MyNodeClass") {
// Store original method
const originalMethod = nodeType.prototype.onConnectionsChange;
// Replace with custom implementation
nodeType.prototype.onConnectionsChange = function(side, slot, connect, link_info, output) {
// Call original method first
const result = originalMethod?.apply(this, arguments);
// Add custom behavior
console.log("Connection changed!");
return result;
};
}
}
Key Points:
- Check nodeType.comfyClass: Target specific node types
- Store original method: Preserve existing functionality
- Use ?.apply: Safe method calling
- Return result: Maintain method contract
nodeCreated()
async nodeCreated(node) {
// Called when specific node instance is created
// node: Individual node instance
}
Purpose: Modify individual node instances Usage: Instance-specific customizations Parameters:
node: Specific node instance
Example:
async nodeCreated(node) {
if (node.comfyClass === "MyNodeClass") {
// Add instance-specific properties
node.customProperty = "value";
// Add event listeners
node.addEventListener("click", () => {
console.log("Node clicked!");
});
}
}
init()
async init() {
// Called when ComfyUI webpage loads
// Before any nodes are registered
// After graph object creation
}
Purpose: Modify core ComfyUI behavior Usage: Global modifications, core hijacking Warning: High impact, potential compatibility issues
Example:
async init() {
// Hijack core app methods
const originalMethod = app.someCoreMethod;
app.someCoreMethod = function(...args) {
console.log("Core method called");
return originalMethod.apply(this, args);
};
}
setup()
async setup() {
// Called at end of startup process
// Good for event listeners and menu additions
}
Purpose: Final setup after all initialization
Usage: Event listeners, menu additions, global setup
Note: Use afterConfigureGraph for workflow-specific setup
Example:
async setup() {
// Add global event listeners
document.addEventListener("keydown", (event) => {
if (event.ctrlKey && event.key === "s") {
this.saveWorkflow();
}
});
// Add to global menus
this.addToMainMenu();
}
Hook Call Sequences
Web Page Load Sequence
invokeExtensionsAsync init
invokeExtensionsAsync addCustomNodeDefs
invokeExtensionsAsync getCustomWidgets
invokeExtensionsAsync beforeRegisterNodeDef [repeated multiple times]
invokeExtensionsAsync registerCustomNodes
invokeExtensionsAsync beforeConfigureGraph
invokeExtensionsAsync nodeCreated
invokeExtensions loadedGraphNode
invokeExtensionsAsync afterConfigureGraph
invokeExtensionsAsync setup
Loading Workflow Sequence
invokeExtensionsAsync beforeConfigureGraph
invokeExtensionsAsync beforeRegisterNodeDef [zero, one, or multiple times]
invokeExtensionsAsync nodeCreated [repeated multiple times]
invokeExtensions loadedGraphNode [repeated multiple times]
invokeExtensionsAsync afterConfigureGraph
Adding New Node Sequence
invokeExtensionsAsync nodeCreated
Advanced Hook Patterns
Conditional Node Modification
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// Check if this is our custom node
if (nodeType.comfyClass === "MyCustomNode") {
// Add custom properties
nodeType.prototype.customMethod = function() {
console.log("Custom method called");
};
// Modify existing methods
this.hijackNodeMethod(nodeType, "onExecute");
}
}
hijackNodeMethod(nodeType, methodName) {
const originalMethod = nodeType.prototype[methodName];
nodeType.prototype[methodName] = function(...args) {
// Pre-execution logic
console.log(`Before ${methodName}`);
// Call original method
const result = originalMethod?.apply(this, args);
// Post-execution logic
console.log(`After ${methodName}`);
return result;
};
}
Inter-Extension Communication
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeType.comfyClass === "MyNodeClass") {
// Check if other extensions have modified this node
if (nodeType.prototype._extensionModified) {
console.log("Node already modified by another extension");
}
// Mark as modified
nodeType.prototype._extensionModified = true;
// Add extension-specific behavior
this.addExtensionBehavior(nodeType);
}
}
Safe Method Hijacking
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeType.comfyClass === "MyNodeClass") {
this.safeHijackMethod(nodeType, "onConnectionsChange", (originalMethod) => {
return function(side, slot, connect, link_info, output) {
// Pre-hook logic
console.log("Before connection change");
// Call original method
const result = originalMethod?.apply(this, arguments);
// Post-hook logic
console.log("After connection change");
return result;
};
});
}
}
safeHijackMethod(nodeType, methodName, newMethodFactory) {
const originalMethod = nodeType.prototype[methodName];
// Create new method
const newMethod = newMethodFactory(originalMethod);
// Store original for potential restoration
newMethod._originalMethod = originalMethod;
// Replace method
nodeType.prototype[methodName] = newMethod;
}
Best Practices
Hook Implementation Guidelines
app.registerExtension({
name: "best.practices.extension",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// 1. Always check if this is the right node type
if (nodeType.comfyClass !== "MyNodeClass") {
return;
}
// 2. Store original methods safely
const originalMethod = nodeType.prototype.onExecute;
// 3. Use defensive programming
if (typeof originalMethod !== "function") {
console.warn("Original method not found");
return;
}
// 4. Implement new method
nodeType.prototype.onExecute = function(...args) {
try {
// Pre-execution logic
this.preExecute();
// Call original method
const result = originalMethod.apply(this, args);
// Post-execution logic
this.postExecute();
return result;
} catch (error) {
console.error("Error in hijacked method:", error);
throw error;
}
};
},
async nodeCreated(node) {
// 1. Check node type
if (node.comfyClass !== "MyNodeClass") {
return;
}
// 2. Add instance-specific behavior
this.setupNodeInstance(node);
},
setupNodeInstance(node) {
// Add instance properties
node.extensionData = {
created: Date.now(),
modified: false
};
// Add event listeners
node.addEventListener("customEvent", (event) => {
this.handleNodeEvent(node, event);
});
}
});
Error Handling
async beforeRegisterNodeDef(nodeType, nodeData, app) {
try {
if (nodeType.comfyClass === "MyNodeClass") {
this.modifyNode(nodeType);
}
} catch (error) {
console.error("Error in beforeRegisterNodeDef:", error);
// Don't throw - let other extensions continue
}
}
modifyNode(nodeType) {
// Safe method modification
const originalMethod = nodeType.prototype.onExecute;
if (originalMethod) {
nodeType.prototype.onExecute = function(...args) {
try {
return originalMethod.apply(this, args);
} catch (error) {
console.error("Error in modified method:", error);
// Handle error gracefully
return null;
}
};
}
}
Performance Considerations
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// 1. Early return for irrelevant nodes
if (nodeType.comfyClass !== "MyNodeClass") {
return;
}
// 2. Cache expensive operations
if (!this._cachedData) {
this._cachedData = this.computeExpensiveData();
}
// 3. Use efficient method hijacking
this.efficientHijack(nodeType);
}
efficientHijack(nodeType) {
// Use arrow functions for better performance
const originalMethod = nodeType.prototype.onExecute;
nodeType.prototype.onExecute = (...args) => {
// Efficient implementation
return originalMethod?.apply(this, args);
};
}
ComfyUI Objects - Core Architecture
Purpose
Understanding ComfyUI's core objects is essential for creating sophisticated extensions and custom nodes. This section covers the main objects and their properties.
LiteGraph Foundation
Overview
- Base framework: ComfyUI is built on top of LiteGraph
- Documentation: Available at
doc/index.htmlin LiteGraph repository - Core functionality: Much of ComfyUI's functionality comes from LiteGraph
- Repository: https://github.com/jagenjo/litegraph.js
ComfyApp Object
Access and Properties
import { app } from "../../scripts/app.js";
// Key properties
app.canvas // LGraphCanvas object (UI)
app.canvasEl // DOM <canvas> element
app.graph // LGraph object (logical graph)
app.runningNodeId // Currently executing node
app.ui // UI elements (queue, menu, dialogs)
Important Functions
// Core functions
app.graphToPrompt() // Convert graph to prompt
app.loadGraphData() // Load a graph
app.queuePrompt() // Submit prompt to queue
app.registerExtension() // Add extension
Usage Examples
// Get current graph
const graph = app.graph;
// Get specific node
const node = app.graph._nodes_by_id(nodeId);
// Convert graph to prompt
const prompt = app.graphToPrompt();
// Submit workflow
app.queuePrompt(prompt);
LGraph Object
Purpose
- Logical representation: Current state of graph (nodes and links)
- Manipulation: Use LiteGraph functions for graph operations
- Documentation: Refer to LiteGraph documentation for functions
Node and Link Access
// Get node by ID
const node = app.graph._nodes_by_id(nodeId);
// Iterate through node inputs
node.inputs.forEach(input => {
const linkId = input.link;
if (linkId) {
const link = app.graph.links[linkId];
const upstreamNodeId = link.origin_id;
const upstreamSlot = link.origin_slot;
const downstreamSlot = link.target_slot;
const dataType = link.type;
}
});
// Get all links
const allLinks = app.graph.links;
LLink Object
Properties
const link = app.graph.links[linkId];
// Link properties
link.id // Unique link ID
link.origin_id // Upstream node ID
link.origin_slot // Upstream output slot
link.target_id // Downstream node ID
link.target_slot // Downstream input slot
link.type // Data type string
Usage
// Create links using LiteGraph functions
node.connect(outputSlot, targetNode, inputSlot);
// Avoid creating LLink objects directly
// Use LiteGraph functions instead
ComfyNode Object
Properties
const node = app.graph._nodes_by_id(nodeId);
// Core properties
node.bgcolor // Background color
node.comfyClass // Python class name
node.flags // Node state flags
node.graph // Reference to LGraph
node.id // Unique node ID
node.input_type // List of input types
node.inputs // List of inputs
node.mode // Node mode (0=normal, 2=muted, 4=bypassed)
node.order // Execution order
node.pos // [x, y] position
node.properties // Node properties
node.properties_info // Property types and defaults
node.size // [width, height] dimensions
node.title // Display title
node.type // Unique node class name
node.widgets // List of widgets
node.widgets_values // Current widget values
Important Functions
Inputs and Outputs
// Add inputs
node.addInput(name, type);
node.addInputs(inputArray);
// Find inputs
const slotIndex = node.findInputSlot(name);
const inputSlot = node.findInputSlotByType(type);
// Remove inputs
node.removeInput(slotIndex);
// Get connected nodes
const inputNode = node.getInputNode(inputSlot);
const outputNodes = node.getOutputNodes(outputSlot);
const inputLink = node.getInputLink(inputSlot);
Widgets
// Add widgets
node.addWidget(type, name, value, options);
node.addCustomWidget(widget);
node.addDOMWidget(element);
// Convert widget to input
node.convertWidgetToInput(widgetName);
Connections
// Connect nodes
node.connect(outputSlot, targetNode, inputSlot);
node.connectByType(outputSlot, targetNode, inputType);
node.connectByTypeOutput(inputSlot, targetNode, inputType);
// Disconnect
node.disconnectInput(inputName);
node.disconnectOutput(outputSlot, targetNode, inputSlot);
// Connection events
node.onConnectionChange(side, slot, connect, linkInfo, output);
node.onConnectInput(inputSlot, outputNode, outputSlot);
Display
// Redraw node
node.setDirtyCanvas(foreground, background);
// Custom drawing
node.onDrawBackground(ctx); // Background drawing
node.onDrawForeground(ctx); // Foreground drawing
// Node title
node.getTitle();
// Collapse state
node.collapse(boolean);
Other Functions
// Change node mode
node.changeMode(mode); // 0=normal, 2=muted, 4=bypassed
Inputs and Widgets
Input Structure
const input = node.inputs[inputIndex];
// Input properties
input.name // Input name
input.type // Input type
input.link // Connected LLink reference
input.widget // Reference to converted widget (if applicable)
Widget Structure
const widget = node.widgets[widgetIndex];
// Widget properties
widget.callback // Value change callback
widget.last_y // Vertical position
widget.name // Widget name
widget.options // Widget options
widget.type // Widget type
widget.value // Current value
Widget Types
// Built-in widget types
app.widgets.BOOLEAN // Boolean widget
app.widgets.INT // Integer widget
app.widgets.FLOAT // Float widget
app.widgets.STRING // String widget
app.widgets.COMBO // Dropdown widget
app.widgets.IMAGEUPLOAD // Image upload widget
// Custom widget types
// Add via getCustomWidgets hook
Linked Widgets
// Linked widget example
const linkedWidget = {
type: 'int:seed', // base_widget_type:base_widget_name
name: 'control_after_generate',
value: 42
};
Prompt Object
Structure
const prompt = app.graphToPrompt();
// Prompt properties
prompt.output // Node data mapping
prompt.workflow // Workflow structure
Output Object
// Node data structure
prompt.output[nodeId] = {
class_type: "NodeClassName",
inputs: {
inputName: "value", // Widget value
inputName: [nodeId, slot], // Connected input
inputName: undefined // Unconnected converted input
}
};
Workflow Object
prompt.workflow = {
config: {}, // Configuration options
extra: {
ds: {}, // View description
groups: [], // Workflow groups
last_link_id: 123, // Last link ID
last_node_id: 456 // Last node ID
},
links: [ // Link array
[linkId, originId, originSlot, targetId, targetSlot, type]
],
nodes: [ // Node array
{
flags: {},
id: 123,
inputs: [],
mode: 0,
order: 1,
pos: [100, 200],
properties: {},
size: [200, 100],
type: "NodeType",
widgets_values: [],
outputs: [ // If node has outputs
{
name: "output",
type: "IMAGE",
links: [1, 2, 3],
shape: 3,
slot_index: 0
}
]
}
],
version: 0.4 // LiteGraph version
};
Advanced Usage Examples
Node Manipulation
// Create custom node behavior
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeType.comfyClass === "MyCustomNode") {
// Add custom methods
nodeType.prototype.customMethod = function() {
console.log("Custom method called");
};
// Hijack existing methods
const originalOnExecute = nodeType.prototype.onExecute;
nodeType.prototype.onExecute = function() {
console.log("Before execution");
const result = originalOnExecute?.apply(this, arguments);
console.log("After execution");
return result;
};
}
}
Graph Traversal
// Traverse graph connections
function traverseGraph(startNodeId) {
const visited = new Set();
const queue = [startNodeId];
while (queue.length > 0) {
const nodeId = queue.shift();
if (visited.has(nodeId)) continue;
visited.add(nodeId);
const node = app.graph._nodes_by_id(nodeId);
// Process node
console.log(`Processing node: ${node.title}`);
// Add connected nodes to queue
node.inputs.forEach(input => {
if (input.link) {
const link = app.graph.links[input.link];
queue.push(link.origin_id);
}
});
}
}
Custom Widget Creation
// Add custom widget type
app.registerExtension({
name: "custom.widgets",
getCustomWidgets(nodeType, nodeData, app) {
return {
CUSTOM_SLIDER: {
name: "custom_slider",
draw: function(ctx, node, widgetWidth, y, widget) {
// Custom drawing code
},
mouse: function(event, pos, node) {
// Mouse interaction code
}
}
};
}
});
Best Practices
Object Access
// Safe object access
function safeGetNode(nodeId) {
try {
return app.graph._nodes_by_id(nodeId);
} catch (error) {
console.error("Error accessing node:", error);
return null;
}
}
// Validate node properties
function validateNode(node) {
if (!node || !node.comfyClass) {
console.warn("Invalid node object");
return false;
}
return true;
}
Performance Optimization
// Cache frequently accessed objects
const graph = app.graph;
const canvas = app.canvas;
// Use efficient traversal
function getConnectedNodes(nodeId) {
const node = app.graph._nodes_by_id(nodeId);
const connected = [];
node.inputs.forEach(input => {
if (input.link) {
const link = app.graph.links[input.link];
connected.push(link.origin_id);
}
});
return connected;
}
ComfyUI Settings - Extension Configuration
Purpose
ComfyUI provides a powerful settings system that allows extensions to add configuration options that appear in the ComfyUI settings panel. This enables users to customize extension behavior without modifying code.
Basic Operation
Adding a Setting
import { app } from "../../scripts/app.js";
app.registerExtension({
name: "My Extension",
settings: [
{
id: "example.boolean",
name: "Example boolean setting",
type: "boolean",
defaultValue: false,
},
],
});
Key Points:
- Unique ID: Must be unique across all extensions
- Category: ID split by
.determines category placement - No dots: Appears in "Other" category
- One dot: Left part = category, right part = section
- Multiple dots: Only first two parts used
Reading a Setting
import { app } from "../../scripts/app.js";
if (app.extensionManager.setting.get('example.boolean')) {
console.log("Setting is enabled.");
} else {
console.log("Setting is disabled.");
}
Reacting to Changes
{
id: "example.boolean",
name: "Example boolean setting",
type: "boolean",
defaultValue: false,
onChange: (newVal, oldVal) => {
console.log(`Setting was changed from ${oldVal} to ${newVal}`);
},
}
Note: onChange is called on every page load when extension is registered.
Writing a Setting
import { app } from "../../scripts/app.js";
try {
await app.extensionManager.setting.set("example.boolean", true);
} catch (error) {
console.error(`Error changing setting: ${error}`);
}
Setting Types
Boolean
{
id: "example.boolean",
name: "Example boolean setting",
type: "boolean",
defaultValue: false,
onChange: (newVal, oldVal) => {
console.log(`Setting was changed from ${oldVal} to ${newVal}`);
},
}
Features:
- Toggle switch: On/off toggle
- PrimeVue component: ToggleSwitch
- Use cases: Enable/disable features, show/hide elements
Text
{
id: "example.text",
name: "Example text setting",
type: "text",
defaultValue: "Foo",
onChange: (newVal, oldVal) => {
console.log(`Setting was changed from ${oldVal} to ${newVal}`);
},
}
Features:
- Freeform text: User can type anything
- PrimeVue component: InputText
- Use cases: File paths, custom strings, configuration values
Number
{
id: "example.number",
name: "Example number setting",
type: "number",
defaultValue: 42,
attrs: {
showButtons: true,
maxFractionDigits: 1,
},
onChange: (newVal, oldVal) => {
console.log(`Setting was changed from ${oldVal} to ${newVal}`);
},
}
Features:
- Numeric input: Numbers only
- PrimeVue component: InputNumber
- Attributes:
showButtons,maxFractionDigits - Use cases: Counts, dimensions, thresholds
Slider
{
id: "example.slider",
name: "Example slider setting",
type: "slider",
attrs: {
min: -10,
max: 10,
step: 0.5,
},
defaultValue: 0,
onChange: (newVal, oldVal) => {
console.log(`Setting was changed from ${oldVal} to ${newVal}`);
},
}
Features:
- Visual slider: Drag to set value
- Direct input: Type value directly
- PrimeVue component: Slider
- Attributes:
min,max,step - Use cases: Opacity, scale, intensity
Combo (Dropdown)
{
id: "example.combo",
name: "Example combo setting",
type: "combo",
defaultValue: "first",
options: [
{ text: "My first option", value: "first" },
"My second option",
],
attrs: {
editable: true,
filter: true,
},
onChange: (newVal, oldVal) => {
console.log(`Setting was changed from ${oldVal} to ${newVal}`);
},
}
Features:
- Dropdown selection: Choose from options
- PrimeVue component: Select
- Options: Objects with
text/valueor plain strings - Attributes:
editable,filter - Use cases: Mode selection, preset choices
Color
{
id: "example.color",
name: "Example color setting",
type: "color",
defaultValue: "ff0000",
onChange: (newVal, oldVal) => {
console.log(`Setting was changed from ${oldVal} to ${newVal}`);
},
}
Features:
- Color picker: Visual color selection
- Hex input: Type hex values directly
- PrimeVue component: ColorPicker
- Format: Six hex digits (no shorthand)
- Use cases: Theme colors, UI customization
Image
{
id: "example.image",
name: "Example image setting",
type: "image",
onChange: (newVal, oldVal) => {
console.log(`Setting was changed from ${oldVal} to ${newVal}`);
},
}
Features:
- Image upload: Upload image files
- PrimeVue component: FileUpload
- Storage: Saved as data URL
- Use cases: Logos, backgrounds, icons
Hidden
{
id: "example.hidden",
name: "Example hidden setting",
type: "hidden",
}
Features:
- Not displayed: Hidden from settings panel
- Programmatic access: Read/write from code
- Use cases: Internal state, cached data
Advanced Configuration
Categories
{
id: "example.boolean",
name: "Example boolean setting",
type: "boolean",
defaultValue: false,
category: ["Category name", "Section heading", "Setting label"],
}
Benefits:
- Custom organization: Override ID-based categorization
- Flexible naming: Change without losing user values
- Hierarchical structure: Multiple levels of organization
Tooltips
{
id: "example.boolean",
name: "Example boolean setting",
type: "boolean",
defaultValue: false,
tooltip: "This is some helpful information",
}
Features:
- Contextual help: ℹ️ icon with hover text
- User guidance: Explain setting purpose
- Better UX: Reduce confusion
PrimeVue Integration
Attributes
{
id: "example.number",
name: "Example number setting",
type: "number",
defaultValue: 0,
attrs: {
showButtons: true, // Show increment/decrement buttons
maxFractionDigits: 2, // Allow decimal places
min: 0, // Minimum value
max: 100, // Maximum value
step: 0.1, // Step size
},
}
Available Attributes:
- Number:
showButtons,maxFractionDigits,min,max,step - Slider:
min,max,step - Combo:
editable,filter - Text:
placeholder,maxLength
Complete Example
Extension with Settings
import { app } from "../../scripts/app.js";
app.registerExtension({
name: "My Extension",
settings: [
{
id: "my.extension.enabled",
name: "Enable Extension",
type: "boolean",
defaultValue: true,
tooltip: "Enable or disable this extension",
onChange: (newVal, oldVal) => {
console.log(`Extension ${newVal ? 'enabled' : 'disabled'}`);
},
},
{
id: "my.extension.mode",
name: "Operation Mode",
type: "combo",
defaultValue: "auto",
options: [
{ text: "Automatic", value: "auto" },
{ text: "Manual", value: "manual" },
{ text: "Disabled", value: "disabled" },
],
tooltip: "Choose how the extension operates",
onChange: (newVal, oldVal) => {
console.log(`Mode changed from ${oldVal} to ${newVal}`);
},
},
{
id: "my.extension.threshold",
name: "Threshold",
type: "slider",
defaultValue: 0.5,
attrs: {
min: 0,
max: 1,
step: 0.01,
},
tooltip: "Adjust the sensitivity threshold",
onChange: (newVal, oldVal) => {
console.log(`Threshold changed from ${oldVal} to ${newVal}`);
},
},
{
id: "my.extension.color",
name: "Theme Color",
type: "color",
defaultValue: "ff6b6b",
tooltip: "Choose the theme color for this extension",
onChange: (newVal, oldVal) => {
console.log(`Color changed from ${oldVal} to ${newVal}`);
this.updateThemeColor(newVal);
},
},
{
id: "my.extension.logo",
name: "Logo Image",
type: "image",
tooltip: "Upload a logo image for this extension",
onChange: (newVal, oldVal) => {
console.log(`Logo changed`);
this.updateLogo(newVal);
},
},
{
id: "my.extension.internal.state",
name: "Internal State",
type: "hidden",
},
],
async setup() {
// Initialize extension with settings
this.initializeWithSettings();
},
initializeWithSettings() {
// Check if extension is enabled
const enabled = app.extensionManager.setting.get('my.extension.enabled');
if (!enabled) {
console.log("Extension is disabled");
return;
}
// Get mode setting
const mode = app.extensionManager.setting.get('my.extension.mode');
console.log(`Extension mode: ${mode}`);
// Get threshold setting
const threshold = app.extensionManager.setting.get('my.extension.threshold');
console.log(`Threshold: ${threshold}`);
// Get color setting
const color = app.extensionManager.setting.get('my.extension.color');
this.updateThemeColor(color);
// Get logo setting
const logo = app.extensionManager.setting.get('my.extension.logo');
if (logo) {
this.updateLogo(logo);
}
},
updateThemeColor(color) {
// Apply theme color
document.documentElement.style.setProperty('--extension-color', `#${color}`);
},
updateLogo(logoDataUrl) {
// Update logo image
const logoElement = document.getElementById('extension-logo');
if (logoElement) {
logoElement.src = logoDataUrl;
}
},
async saveInternalState(state) {
try {
await app.extensionManager.setting.set('my.extension.internal.state', state);
} catch (error) {
console.error('Error saving internal state:', error);
}
},
getInternalState() {
return app.extensionManager.setting.get('my.extension.internal.state');
}
});
Best Practices
Setting Design
// Good: Clear, descriptive names
{
id: "my.extension.auto.save",
name: "Auto Save",
type: "boolean",
defaultValue: true,
tooltip: "Automatically save changes when nodes are modified",
}
// Bad: Vague, unclear names
{
id: "setting1",
name: "Option",
type: "boolean",
defaultValue: false,
}
Error Handling
async updateSetting(settingId, value) {
try {
await app.extensionManager.setting.set(settingId, value);
console.log(`Setting ${settingId} updated to ${value}`);
} catch (error) {
console.error(`Error updating setting ${settingId}:`, error);
// Handle error gracefully
}
}
Performance Considerations
// Cache frequently accessed settings
class ExtensionSettings {
constructor() {
this.cache = new Map();
this.loadSettings();
}
loadSettings() {
// Load all settings at once
const settings = [
'my.extension.enabled',
'my.extension.mode',
'my.extension.threshold',
];
settings.forEach(settingId => {
const value = app.extensionManager.setting.get(settingId);
this.cache.set(settingId, value);
});
}
get(settingId) {
return this.cache.get(settingId);
}
async set(settingId, value) {
try {
await app.extensionManager.setting.set(settingId, value);
this.cache.set(settingId, value);
} catch (error) {
console.error(`Error setting ${settingId}:`, error);
}
}
}
ComfyUI Dialog API - User Interaction
Purpose
ComfyUI provides a standardized Dialog API that works consistently across desktop and web environments. This allows extension authors to create user-friendly dialogs for input, confirmation, and other interactive elements.
Basic Usage
Prompt Dialog
// Show a prompt dialog
app.extensionManager.dialog.prompt({
title: "User Input",
message: "Please enter your name:",
defaultValue: "User"
}).then(result => {
if (result !== null) {
console.log(`Input: ${result}`);
}
});
Features:
- User input: Get text input from users
- Default value: Pre-fill input field
- Cancellation: Handle user cancellation
- Promise-based: Async/await compatible
Confirm Dialog
// Show a confirmation dialog
app.extensionManager.dialog.confirm({
title: "Confirm Action",
message: "Are you sure you want to continue?",
type: "default"
}).then(result => {
console.log(result ? "User confirmed" : "User cancelled");
});
Features:
- User confirmation: Get yes/no responses
- Multiple types: Different dialog styles
- Item lists: Show lists of items
- Hints: Additional context information
API Reference
Prompt Dialog
app.extensionManager.dialog.prompt({
title: string, // Dialog title
message: string, // Message/question to display
defaultValue?: string // Initial value in the input field (optional)
}).then((result: string | null) => {
// result is the entered text, or null if cancelled
});
Parameters:
- title: Dialog window title
- message: Question or instruction text
- defaultValue: Pre-filled input value (optional)
- result: User input string or null if cancelled
Confirm Dialog
app.extensionManager.dialog.confirm({
title: string, // Dialog title
message: string, // Message to display
type?: "default" | "overwrite" | "delete" | "dirtyClose" | "reinstall", // Dialog type (optional)
itemList?: string[], // List of items to display (optional)
hint?: string // Hint text to display (optional)
}).then((result: boolean | null) => {
// result is true if confirmed, false if denied, null if cancelled
});
Parameters:
- title: Dialog window title
- message: Confirmation message
- type: Dialog style (optional)
- itemList: Array of items to display (optional)
- hint: Additional context text (optional)
- result: true (confirmed), false (denied), null (cancelled)
Dialog Types
Default
app.extensionManager.dialog.confirm({
title: "Confirm Action",
message: "Are you sure you want to continue?",
type: "default"
}).then(result => {
console.log(result ? "User confirmed" : "User cancelled");
});
Use cases: General confirmations, simple yes/no questions
Overwrite
app.extensionManager.dialog.confirm({
title: "File Exists",
message: "The file already exists. Do you want to overwrite it?",
type: "overwrite",
itemList: ["existing_file.txt", "backup_file.txt"]
}).then(result => {
if (result) {
console.log("User chose to overwrite");
}
});
Use cases: File overwrite confirmations, destructive actions
Delete
app.extensionManager.dialog.confirm({
title: "Delete Items",
message: "Are you sure you want to delete these items?",
type: "delete",
itemList: ["item1.txt", "item2.txt", "item3.txt"],
hint: "This action cannot be undone"
}).then(result => {
if (result) {
console.log("User confirmed deletion");
}
});
Use cases: Deletion confirmations, permanent actions
Dirty Close
app.extensionManager.dialog.confirm({
title: "Unsaved Changes",
message: "You have unsaved changes. Do you want to save before closing?",
type: "dirtyClose"
}).then(result => {
if (result) {
console.log("User chose to save");
}
});
Use cases: Unsaved changes, data loss prevention
Reinstall
app.extensionManager.dialog.confirm({
title: "Reinstall Extension",
message: "Do you want to reinstall this extension?",
type: "reinstall",
hint: "This will remove all current settings"
}).then(result => {
if (result) {
console.log("User confirmed reinstall");
}
});
Use cases: Extension management, system changes
Advanced Usage Examples
Input Validation
async function getValidatedInput() {
let input = null;
let isValid = false;
while (!isValid) {
input = await app.extensionManager.dialog.prompt({
title: "Enter Email",
message: "Please enter a valid email address:",
defaultValue: input || ""
});
if (input === null) {
// User cancelled
return null;
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (emailRegex.test(input)) {
isValid = true;
} else {
// Show error and retry
await app.extensionManager.dialog.confirm({
title: "Invalid Email",
message: "Please enter a valid email address.",
type: "default"
});
}
}
return input;
}
Multi-Step Confirmation
async function confirmDestructiveAction() {
// First confirmation
const firstConfirm = await app.extensionManager.dialog.confirm({
title: "Warning",
message: "This action will permanently delete all data. Are you sure?",
type: "delete",
hint: "This cannot be undone"
});
if (!firstConfirm) {
return false;
}
// Second confirmation
const secondConfirm = await app.extensionManager.dialog.confirm({
title: "Final Confirmation",
message: "Are you absolutely sure? Type 'DELETE' to confirm:",
type: "default"
});
if (!secondConfirm) {
return false;
}
// Final text confirmation
const textConfirm = await app.extensionManager.dialog.prompt({
title: "Type to Confirm",
message: "Type 'DELETE' to confirm this action:",
defaultValue: ""
});
return textConfirm === "DELETE";
}
Batch Operations
async function confirmBatchOperation(items) {
const itemList = items.slice(0, 10); // Show first 10 items
const hasMore = items.length > 10;
const message = hasMore
? `This will affect ${items.length} items. Continue?`
: `This will affect the following items. Continue?`;
const hint = hasMore
? `... and ${items.length - 10} more items`
: undefined;
return await app.extensionManager.dialog.confirm({
title: "Batch Operation",
message: message,
type: "default",
itemList: itemList,
hint: hint
});
}
Settings Configuration
async function configureExtension() {
// Get extension name
const name = await app.extensionManager.dialog.prompt({
title: "Extension Configuration",
message: "Enter extension name:",
defaultValue: "My Extension"
});
if (name === null) return;
// Get operation mode
const mode = await app.extensionManager.dialog.confirm({
title: "Operation Mode",
message: "Enable automatic mode?",
type: "default"
});
if (mode === null) return;
// Get threshold value
const threshold = await app.extensionManager.dialog.prompt({
title: "Threshold Setting",
message: "Enter threshold value (0.0 - 1.0):",
defaultValue: "0.5"
});
if (threshold === null) return;
// Validate threshold
const thresholdNum = parseFloat(threshold);
if (isNaN(thresholdNum) || thresholdNum < 0 || thresholdNum > 1) {
await app.extensionManager.dialog.confirm({
title: "Invalid Input",
message: "Threshold must be a number between 0.0 and 1.0",
type: "default"
});
return;
}
// Save configuration
await app.extensionManager.setting.set('extension.name', name);
await app.extensionManager.setting.set('extension.autoMode', mode);
await app.extensionManager.setting.set('extension.threshold', thresholdNum);
console.log("Extension configured successfully");
}
Error Handling
Safe Dialog Usage
async function safePrompt(title, message, defaultValue = "") {
try {
const result = await app.extensionManager.dialog.prompt({
title: title,
message: message,
defaultValue: defaultValue
});
return result;
} catch (error) {
console.error("Dialog error:", error);
return null;
}
}
async function safeConfirm(title, message, type = "default") {
try {
const result = await app.extensionManager.dialog.confirm({
title: title,
message: message,
type: type
});
return result;
} catch (error) {
console.error("Dialog error:", error);
return null;
}
}
Timeout Handling
async function promptWithTimeout(title, message, timeout = 30000) {
const promptPromise = app.extensionManager.dialog.prompt({
title: title,
message: message
});
const timeoutPromise = new Promise((resolve) => {
setTimeout(() => resolve(null), timeout);
});
try {
const result = await Promise.race([promptPromise, timeoutPromise]);
return result;
} catch (error) {
console.error("Prompt timeout or error:", error);
return null;
}
}
Best Practices
Dialog Design
// Good: Clear, concise messages
app.extensionManager.dialog.confirm({
title: "Delete File",
message: "Are you sure you want to delete 'important_file.txt'?",
type: "delete",
hint: "This action cannot be undone"
});
// Bad: Vague, unclear messages
app.extensionManager.dialog.confirm({
title: "Action",
message: "Do you want to continue?",
type: "default"
});
User Experience
// Provide context and options
async function handleFileConflict(existingFile, newFile) {
const result = await app.extensionManager.dialog.confirm({
title: "File Conflict",
message: `The file '${existingFile}' already exists. What would you like to do?`,
type: "overwrite",
itemList: [existingFile, newFile],
hint: "Choose 'Yes' to overwrite, 'No' to skip, or 'Cancel' to abort"
});
if (result === true) {
// Overwrite
return "overwrite";
} else if (result === false) {
// Skip
return "skip";
} else {
// Cancel
return "cancel";
}
}
Performance Considerations
// Cache dialog results for repeated operations
class DialogCache {
constructor() {
this.cache = new Map();
}
async getCachedPrompt(key, title, message, defaultValue = "") {
if (this.cache.has(key)) {
return this.cache.get(key);
}
const result = await app.extensionManager.dialog.prompt({
title: title,
message: message,
defaultValue: defaultValue
});
if (result !== null) {
this.cache.set(key, result);
}
return result;
}
clearCache() {
this.cache.clear();
}
}
Integration with Extensions
Extension Dialog Service
app.registerExtension({
name: "My Extension",
async setup() {
this.dialogService = new ExtensionDialogService();
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeType.comfyClass === "MyCustomNode") {
// Add dialog functionality to nodes
nodeType.prototype.showConfigDialog = async function() {
return await this.dialogService.showNodeConfigDialog(this);
};
}
}
});
class ExtensionDialogService {
async showNodeConfigDialog(node) {
const config = await app.extensionManager.dialog.prompt({
title: "Node Configuration",
message: "Enter configuration for this node:",
defaultValue: node.config || ""
});
if (config !== null) {
node.config = config;
node.setDirtyCanvas(true);
}
return config;
}
async confirmNodeDeletion(node) {
return await app.extensionManager.dialog.confirm({
title: "Delete Node",
message: `Are you sure you want to delete '${node.title}'?`,
type: "delete",
hint: "This action cannot be undone"
});
}
}
ComfyUI Toast API - User Notifications
Purpose
ComfyUI provides a Toast API for displaying non-blocking notification messages to users. These toasts are useful for providing feedback without interrupting workflow execution.
Basic Usage
Simple Toast
// Display a simple info toast
app.extensionManager.toast.add({
severity: "info",
summary: "Information",
detail: "Operation completed successfully",
life: 3000
});
Features:
- Non-blocking: Doesn't interrupt user workflow
- Auto-dismiss: Automatically closes after specified time
- User closable: Users can manually close toasts
- Multiple types: Different severity levels
Toast Types
// Success toast
app.extensionManager.toast.add({
severity: "success",
summary: "Success",
detail: "Data saved successfully",
life: 3000
});
// Warning toast
app.extensionManager.toast.add({
severity: "warn",
summary: "Warning",
detail: "This action may cause problems",
life: 5000
});
// Error toast
app.extensionManager.toast.add({
severity: "error",
summary: "Error",
detail: "Failed to process request",
life: 5000
});
Severity Levels:
- success: Green, for successful operations
- info: Blue, for informational messages
- warn: Orange, for warnings
- error: Red, for errors
- secondary: Gray, for secondary information
- contrast: High contrast, for important messages
Alert Helper
// Shorthand for creating an alert toast
app.extensionManager.toast.addAlert("This is an important message");
Features:
- Quick creation: Simple one-line toast creation
- Default styling: Uses default alert styling
- Convenient: Perfect for simple notifications
API Reference
Toast Message
app.extensionManager.toast.add({
severity?: "success" | "info" | "warn" | "error" | "secondary" | "contrast", // Message severity level (default: "info")
summary?: string, // Short title for the toast
detail?: any, // Detailed message content
closable?: boolean, // Whether user can close the toast (default: true)
life?: number, // Duration in milliseconds before auto-closing
group?: string, // Group identifier for managing related toasts
styleClass?: any, // Style class of the message
contentStyleClass?: any // Style class of the content
});
Parameters:
- severity: Visual style and color of the toast
- summary: Short title displayed prominently
- detail: Detailed message content
- closable: Whether user can manually close
- life: Auto-close duration in milliseconds
- group: Group identifier for managing related toasts
- styleClass: Custom CSS class for the message
- contentStyleClass: Custom CSS class for the content
Alert Helper
app.extensionManager.toast.addAlert(message: string);
Parameters:
- message: The alert message to display
Additional Methods
// Remove a specific toast
app.extensionManager.toast.remove(toastMessage);
// Remove all toasts
app.extensionManager.toast.removeAll();
Advanced Usage Examples
Toast with Custom Styling
app.extensionManager.toast.add({
severity: "success",
summary: "Custom Styled Toast",
detail: "This toast has custom styling",
life: 5000,
styleClass: "custom-toast",
contentStyleClass: "custom-content"
});
Grouped Toasts
// Create toasts with the same group
app.extensionManager.toast.add({
severity: "info",
summary: "Processing",
detail: "Step 1 of 3 completed",
group: "batch-operation"
});
app.extensionManager.toast.add({
severity: "info",
summary: "Processing",
detail: "Step 2 of 3 completed",
group: "batch-operation"
});
app.extensionManager.toast.add({
severity: "success",
summary: "Complete",
detail: "All steps completed successfully",
group: "batch-operation"
});
Persistent Toasts
// Toast that doesn't auto-close
app.extensionManager.toast.add({
severity: "warn",
summary: "Important Notice",
detail: "Please review the settings before continuing",
life: 0, // 0 means no auto-close
closable: true
});
Toast with Rich Content
app.extensionManager.toast.add({
severity: "info",
summary: "File Upload",
detail: `
<div>
<strong>File:</strong> document.pdf<br>
<strong>Size:</strong> 2.5 MB<br>
<strong>Status:</strong> Upload complete
</div>
`,
life: 5000
});
Extension Integration
Toast Service Class
class ExtensionToastService {
constructor() {
this.toastGroup = `extension-${Date.now()}`;
}
// Success notification
success(message, detail = null) {
app.extensionManager.toast.add({
severity: "success",
summary: "Success",
detail: detail || message,
life: 3000,
group: this.toastGroup
});
}
// Error notification
error(message, detail = null) {
app.extensionManager.toast.add({
severity: "error",
summary: "Error",
detail: detail || message,
life: 5000,
group: this.toastGroup
});
}
// Warning notification
warning(message, detail = null) {
app.extensionManager.toast.add({
severity: "warn",
summary: "Warning",
detail: detail || message,
life: 4000,
group: this.toastGroup
});
}
// Info notification
info(message, detail = null) {
app.extensionManager.toast.add({
severity: "info",
summary: "Information",
detail: detail || message,
life: 3000,
group: this.toastGroup
});
}
// Progress notification
progress(step, total, message) {
app.extensionManager.toast.add({
severity: "info",
summary: "Progress",
detail: `${message} (${step}/${total})`,
life: 2000,
group: this.toastGroup
});
}
// Clear all extension toasts
clear() {
app.extensionManager.toast.removeAll();
}
}
Extension with Toast Integration
app.registerExtension({
name: "My Extension",
async setup() {
this.toastService = new ExtensionToastService();
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeType.comfyClass === "MyCustomNode") {
// Add toast functionality to nodes
nodeType.prototype.showSuccessToast = function(message) {
this.toastService.success(message);
};
nodeType.prototype.showErrorToast = function(message) {
this.toastService.error(message);
};
}
}
});
Error Handling and Feedback
Operation Feedback
async function performOperation() {
try {
// Show start notification
app.extensionManager.toast.add({
severity: "info",
summary: "Operation Started",
detail: "Processing your request...",
life: 2000
});
// Perform operation
const result = await someAsyncOperation();
// Show success notification
app.extensionManager.toast.add({
severity: "success",
summary: "Operation Complete",
detail: `Successfully processed ${result.count} items`,
life: 3000
});
return result;
} catch (error) {
// Show error notification
app.extensionManager.toast.add({
severity: "error",
summary: "Operation Failed",
detail: `Error: ${error.message}`,
life: 5000
});
throw error;
}
}
Validation Feedback
function validateInput(input) {
if (!input) {
app.extensionManager.toast.add({
severity: "error",
summary: "Validation Error",
detail: "Input cannot be empty",
life: 3000
});
return false;
}
if (input.length < 3) {
app.extensionManager.toast.add({
severity: "warn",
summary: "Validation Warning",
detail: "Input should be at least 3 characters long",
life: 3000
});
return false;
}
app.extensionManager.toast.add({
severity: "success",
summary: "Validation Passed",
detail: "Input is valid",
life: 2000
});
return true;
}
Batch Operation Feedback
async function processBatch(items) {
const total = items.length;
let processed = 0;
let errors = 0;
for (const item of items) {
try {
await processItem(item);
processed++;
// Show progress
app.extensionManager.toast.add({
severity: "info",
summary: "Processing",
detail: `Processed ${processed}/${total} items`,
life: 1000,
group: "batch-operation"
});
} catch (error) {
errors++;
console.error(`Error processing item ${item}:`, error);
}
}
// Show final result
if (errors === 0) {
app.extensionManager.toast.add({
severity: "success",
summary: "Batch Complete",
detail: `Successfully processed all ${total} items`,
life: 3000,
group: "batch-operation"
});
} else {
app.extensionManager.toast.add({
severity: "warn",
summary: "Batch Complete with Errors",
detail: `Processed ${processed}/${total} items, ${errors} errors`,
life: 5000,
group: "batch-operation"
});
}
}
Best Practices
Toast Design
// Good: Clear, actionable messages
app.extensionManager.toast.add({
severity: "success",
summary: "File Saved",
detail: "document.pdf has been saved to the Documents folder",
life: 3000
});
// Bad: Vague, unhelpful messages
app.extensionManager.toast.add({
severity: "info",
summary: "Done",
detail: "OK",
life: 3000
});
User Experience
// Provide context and next steps
app.extensionManager.toast.add({
severity: "warn",
summary: "Settings Changed",
detail: "Please restart the application for changes to take effect",
life: 5000
});
// Use appropriate severity levels
app.extensionManager.toast.add({
severity: "error",
summary: "Critical Error",
detail: "Unable to connect to server. Please check your internet connection.",
life: 0 // Don't auto-close critical errors
});
Performance Considerations
// Avoid toast spam
class ToastThrottler {
constructor() {
this.lastToast = new Map();
this.throttleTime = 1000; // 1 second
}
addToast(options) {
const key = `${options.severity}-${options.summary}`;
const now = Date.now();
const lastTime = this.lastToast.get(key) || 0;
if (now - lastTime < this.throttleTime) {
return; // Skip duplicate toast
}
this.lastToast.set(key, now);
app.extensionManager.toast.add(options);
}
}
Toast Management
// Manage toast lifecycle
class ToastManager {
constructor() {
this.activeToasts = new Set();
}
addToast(options) {
const toast = app.extensionManager.toast.add(options);
this.activeToasts.add(toast);
// Auto-remove from tracking when toast expires
if (options.life > 0) {
setTimeout(() => {
this.activeToasts.delete(toast);
}, options.life);
}
return toast;
}
clearAll() {
app.extensionManager.toast.removeAll();
this.activeToasts.clear();
}
getActiveCount() {
return this.activeToasts.size;
}
}
ComfyUI About Panel Badges - Extension Branding
Purpose
ComfyUI's About Panel Badges API allows extensions to add custom badges to the ComfyUI about page. These badges can display information about your extension and contain links to documentation, source code, or other resources.
Basic Usage
Simple Badge Registration
app.registerExtension({
name: "MyExtension",
aboutPageBadges: [
{
label: "Documentation",
url: "https://example.com/docs",
icon: "pi pi-file"
},
{
label: "GitHub",
url: "https://github.com/username/repo",
icon: "pi pi-github"
}
]
});
Features:
- Custom branding: Add your extension's links to the about page
- Professional appearance: Integrated with ComfyUI's about panel
- User accessibility: Easy access to extension resources
- Icon support: Visual indicators for different link types
Badge Configuration
Required Properties
{
label: string, // Text to display on the badge
url: string, // URL to open when badge is clicked
icon: string // Icon class (e.g., PrimeVue icon)
}
Properties:
- label: Display text for the badge
- url: Link destination when clicked
- icon: PrimeVue icon class for visual representation
Icon Options
Commonly Used Icons
// Documentation icons
"pi pi-file" // File icon
"pi pi-book" // Book icon
// Platform icons
"pi pi-github" // GitHub icon
"pi pi-discord" // Discord icon
"pi pi-globe" // Website icon
// Action icons
"pi pi-external-link" // External link icon
"pi pi-download" // Download icon
"pi pi-heart" // Donate/Heart icon
"pi pi-info-circle" // Information icon
"pi pi-home" // Home icon
Icon Sources:
- PrimeVue Icons: Complete icon set available
- Consistent styling: Matches ComfyUI's design system
- Scalable: Vector-based icons for all screen sizes
Complete Example
Extension with Multiple Badges
app.registerExtension({
name: "BadgeExample",
aboutPageBadges: [
{
label: "Website",
url: "https://example.com",
icon: "pi pi-home"
},
{
label: "Donate",
url: "https://example.com/donate",
icon: "pi pi-heart"
},
{
label: "Documentation",
url: "https://example.com/docs",
icon: "pi pi-book"
},
{
label: "GitHub",
url: "https://github.com/username/repo",
icon: "pi pi-github"
},
{
label: "Discord",
url: "https://discord.gg/example",
icon: "pi pi-discord"
}
]
});
Best Practices
Badge Design
// Good: Clear, descriptive labels
{
label: "Documentation",
url: "https://example.com/docs",
icon: "pi pi-book"
}
// Bad: Vague, unclear labels
{
label: "Link",
url: "https://example.com",
icon: "pi pi-external-link"
}
URL Management
// Use HTTPS for security
{
label: "GitHub",
url: "https://github.com/username/repo",
icon: "pi pi-github"
}
// Avoid HTTP for security
{
label: "Website",
url: "http://example.com", // Bad: HTTP
icon: "pi pi-globe"
}
Icon Selection
// Choose appropriate icons
{
label: "Documentation",
url: "https://example.com/docs",
icon: "pi pi-book" // Good: Book for documentation
}
{
label: "Documentation",
url: "https://example.com/docs",
icon: "pi pi-heart" // Bad: Heart doesn't represent docs
}
ComfyUI Bottom Panel Tabs - Custom Panels
Purpose
ComfyUI's Bottom Panel Tabs API allows extensions to add custom tabs to the bottom panel of the ComfyUI interface. This is useful for adding features like logs, debugging tools, or custom panels.
Basic Usage
Simple Tab Registration
app.registerExtension({
name: "MyExtension",
bottomPanelTabs: [
{
id: "customTab",
title: "Custom Tab",
type: "custom",
render: (el) => {
el.innerHTML = '<div>This is my custom tab content</div>';
}
}
]
});
Features:
- Custom panels: Add your own interface elements
- Integrated UI: Seamlessly integrated with ComfyUI
- Flexible content: HTML, React, or any DOM content
- Event handling: Full JavaScript event support
Tab Configuration
Required Properties
{
id: string, // Unique identifier for the tab
title: string, // Display title shown on the tab
type: string, // Tab type (usually "custom")
icon?: string, // Icon class (optional)
render: (element) => void // Function that populates the tab content
}
Properties:
- id: Unique identifier for the tab
- title: Display title shown on the tab
- type: Tab type (usually "custom")
- icon: Optional icon class
- render: Function that populates the tab content
Interactive Elements
Buttons and Controls
app.registerExtension({
name: "InteractiveTabExample",
bottomPanelTabs: [
{
id: "controlsTab",
title: "Controls",
type: "custom",
render: (el) => {
el.innerHTML = `
<div style="padding: 10px;">
<button id="runBtn">Run Workflow</button>
<button id="clearBtn">Clear Queue</button>
<input type="text" id="textInput" placeholder="Enter text...">
</div>
`;
// Add event listeners
el.querySelector('#runBtn').addEventListener('click', () => {
app.queuePrompt();
});
el.querySelector('#clearBtn').addEventListener('click', () => {
app.clearQueue();
});
el.querySelector('#textInput').addEventListener('input', (e) => {
console.log('Text input:', e.target.value);
});
}
}
]
});
Using React Components
React Integration
// Import React dependencies in your extension
import React from "react";
import ReactDOM from "react-dom/client";
// Simple React component
function TabContent() {
const [count, setCount] = React.useState(0);
const [text, setText] = React.useState("");
return (
<div style={{ padding: "10px" }}>
<h3>React Component</h3>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<br />
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Enter text..."
/>
<p>You typed: {text}</p>
</div>
);
}
// Register the extension with React content
app.registerExtension({
name: "ReactTabExample",
bottomPanelTabs: [
{
id: "reactTab",
title: "React Tab",
type: "custom",
render: (el) => {
const container = document.createElement("div");
container.id = "react-tab-container";
el.appendChild(container);
// Mount React component
ReactDOM.createRoot(container).render(
<React.StrictMode>
<TabContent />
</React.StrictMode>
);
}
}
]
});
Standalone Registration
Independent Tab Registration
app.extensionManager.registerBottomPanelTab({
id: "standAloneTab",
title: "Stand-Alone Tab",
type: "custom",
render: (el) => {
el.innerHTML = '<div>This tab was registered independently</div>';
}
});
Benefits:
- Flexible registration: Register tabs outside of extensions
- Dynamic creation: Create tabs at runtime
- Modular approach: Separate tab logic from extension logic
Advanced Examples
Log Viewer Tab
app.registerExtension({
name: "LogViewer",
bottomPanelTabs: [
{
id: "logViewer",
title: "Log Viewer",
type: "custom",
render: (el) => {
el.innerHTML = `
<div style="padding: 10px;">
<h3>Application Logs</h3>
<div id="logContainer" style="height: 300px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; background: #f5f5f5;">
<div>Log entries will appear here...</div>
</div>
<button id="clearLogs">Clear Logs</button>
<button id="exportLogs">Export Logs</button>
</div>
`;
const logContainer = el.querySelector('#logContainer');
const clearBtn = el.querySelector('#clearLogs');
const exportBtn = el.querySelector('#exportLogs');
// Add log entry
function addLogEntry(message, type = 'info') {
const entry = document.createElement('div');
entry.style.marginBottom = '5px';
entry.style.padding = '5px';
entry.style.borderRadius = '3px';
entry.style.backgroundColor = type === 'error' ? '#ffebee' : '#e8f5e8';
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
logContainer.appendChild(entry);
logContainer.scrollTop = logContainer.scrollHeight;
}
// Clear logs
clearBtn.addEventListener('click', () => {
logContainer.innerHTML = '<div>Logs cleared</div>';
});
// Export logs
exportBtn.addEventListener('click', () => {
const logs = logContainer.textContent;
const blob = new Blob([logs], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'comfyui-logs.txt';
a.click();
URL.revokeObjectURL(url);
});
// Simulate log entries
setInterval(() => {
addLogEntry('System check completed', 'info');
}, 5000);
}
}
]
});
Settings Panel Tab
app.registerExtension({
name: "SettingsPanel",
bottomPanelTabs: [
{
id: "settingsPanel",
title: "Settings",
type: "custom",
render: (el) => {
el.innerHTML = `
<div style="padding: 10px;">
<h3>Extension Settings</h3>
<div style="margin-bottom: 10px;">
<label>
<input type="checkbox" id="autoSave"> Auto Save
</label>
</div>
<div style="margin-bottom: 10px;">
<label>
Theme:
<select id="themeSelect">
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="auto">Auto</option>
</select>
</label>
</div>
<div style="margin-bottom: 10px;">
<label>
Refresh Rate:
<input type="range" id="refreshRate" min="1" max="10" value="5">
<span id="refreshValue">5</span> seconds
</label>
</div>
<button id="saveSettings">Save Settings</button>
<button id="resetSettings">Reset to Defaults</button>
</div>
`;
// Load saved settings
const autoSave = el.querySelector('#autoSave');
const themeSelect = el.querySelector('#themeSelect');
const refreshRate = el.querySelector('#refreshRate');
const refreshValue = el.querySelector('#refreshValue');
const saveBtn = el.querySelector('#saveSettings');
const resetBtn = el.querySelector('#resetSettings');
// Load settings
autoSave.checked = localStorage.getItem('autoSave') === 'true';
themeSelect.value = localStorage.getItem('theme') || 'auto';
refreshRate.value = localStorage.getItem('refreshRate') || '5';
refreshValue.textContent = refreshRate.value;
// Update refresh rate display
refreshRate.addEventListener('input', (e) => {
refreshValue.textContent = e.target.value;
});
// Save settings
saveBtn.addEventListener('click', () => {
localStorage.setItem('autoSave', autoSave.checked);
localStorage.setItem('theme', themeSelect.value);
localStorage.setItem('refreshRate', refreshRate.value);
alert('Settings saved!');
});
// Reset settings
resetBtn.addEventListener('click', () => {
autoSave.checked = false;
themeSelect.value = 'auto';
refreshRate.value = '5';
refreshValue.textContent = '5';
localStorage.clear();
alert('Settings reset to defaults!');
});
}
}
]
});
Best Practices
Tab Design
// Good: Clear, descriptive titles
{
id: "logViewer",
title: "Log Viewer",
type: "custom",
render: (el) => { /* ... */ }
}
// Bad: Vague, unclear titles
{
id: "tab1",
title: "Tab",
type: "custom",
render: (el) => { /* ... */ }
}
Content Organization
// Organize content with proper styling
render: (el) => {
el.innerHTML = `
<div style="padding: 10px;">
<h3>Panel Title</h3>
<div style="margin-bottom: 10px;">
<label>Setting 1:</label>
<input type="text" id="setting1">
</div>
<div style="margin-bottom: 10px;">
<label>Setting 2:</label>
<select id="setting2">
<option value="option1">Option 1</option>
<option value="option2">Option 2</option>
</select>
</div>
<button id="saveBtn">Save</button>
</div>
`;
}
Event Handling
// Clean up event listeners
render: (el) => {
// Store references for cleanup
const button = document.createElement('button');
button.textContent = 'Click Me';
const handleClick = () => {
console.log('Button clicked');
};
button.addEventListener('click', handleClick);
el.appendChild(button);
// Cleanup function (if needed)
el.cleanup = () => {
button.removeEventListener('click', handleClick);
};
}
ComfyUI Sidebar Tabs - Custom Sidebar Panels
Purpose
ComfyUI's Sidebar Tabs API allows extensions to add custom tabs to the sidebar of the ComfyUI interface. This is useful for adding features that require persistent visibility and quick access.
Basic Usage
Simple Sidebar Tab Registration
app.extensionManager.registerSidebarTab({
id: "customSidebar",
icon: "pi pi-compass",
title: "Custom Tab",
tooltip: "My Custom Sidebar Tab",
type: "custom",
render: (el) => {
el.innerHTML = '<div>This is my custom sidebar content</div>';
}
});
Features:
- Persistent visibility: Always accessible in the sidebar
- Quick access: Easy to reach from anywhere in the interface
- Custom content: Full control over tab content
- Icon support: Visual indicators for different tabs
Tab Configuration
Required Properties
{
id: string, // Unique identifier for the tab
icon: string, // Icon class for the tab button
title: string, // Title text for the tab
tooltip?: string, // Tooltip text on hover (optional)
type: string, // Tab type (usually "custom")
render: (element) => void // Function that populates the tab content
}
Properties:
- id: Unique identifier for the tab
- icon: Icon class for the tab button
- title: Title text for the tab
- tooltip: Optional tooltip text on hover
- type: Tab type (usually "custom")
- render: Function that populates the tab content
Icon Options
Icon Libraries
// PrimeVue icons
"pi pi-home" // Home icon
"pi pi-compass" // Compass icon
"pi pi-list" // List icon
"pi pi-chart-line" // Chart icon
"pi pi-cog" // Settings icon
// Material Design icons
"mdi mdi-robot" // Robot icon
"mdi mdi-react" // React icon
"mdi mdi-github" // GitHub icon
"mdi mdi-database" // Database icon
// Font Awesome icons
"fa-solid fa-star" // Star icon
"fa-regular fa-heart" // Heart icon
"fa-brands fa-github" // GitHub icon
Icon Requirements:
- Library loading: Ensure corresponding icon library is loaded
- Consistent styling: Use icons that match ComfyUI's design
- Appropriate selection: Choose icons that represent tab purpose
Stateful Tab Example
Notes Tab with Persistence
app.extensionManager.registerSidebarTab({
id: "statefulTab",
icon: "pi pi-list",
title: "Notes",
type: "custom",
render: (el) => {
// Create elements
const container = document.createElement('div');
container.style.padding = '10px';
const notepad = document.createElement('textarea');
notepad.style.width = '100%';
notepad.style.height = '200px';
notepad.style.marginBottom = '10px';
notepad.placeholder = 'Enter your notes here...';
const saveBtn = document.createElement('button');
saveBtn.textContent = 'Save Notes';
saveBtn.style.marginRight = '10px';
const clearBtn = document.createElement('button');
clearBtn.textContent = 'Clear Notes';
// Load saved content if available
const savedContent = localStorage.getItem('comfyui-notes');
if (savedContent) {
notepad.value = savedContent;
}
// Auto-save content
notepad.addEventListener('input', () => {
localStorage.setItem('comfyui-notes', notepad.value);
});
// Manual save
saveBtn.addEventListener('click', () => {
localStorage.setItem('comfyui-notes', notepad.value);
alert('Notes saved!');
});
// Clear notes
clearBtn.addEventListener('click', () => {
if (confirm('Are you sure you want to clear all notes?')) {
notepad.value = '';
localStorage.removeItem('comfyui-notes');
}
});
// Assemble the UI
container.appendChild(notepad);
container.appendChild(saveBtn);
container.appendChild(clearBtn);
el.appendChild(container);
}
});
Using React Components
React Integration
// Import React dependencies in your extension
import React from "react";
import ReactDOM from "react-dom/client";
// Register sidebar tab with React content
app.extensionManager.registerSidebarTab({
id: "reactSidebar",
icon: "mdi mdi-react",
title: "React Tab",
type: "custom",
render: (el) => {
const container = document.createElement("div");
container.id = "react-sidebar-container";
el.appendChild(container);
// Define a simple React component
function SidebarContent() {
const [count, setCount] = React.useState(0);
const [text, setText] = React.useState("");
return (
<div style={{ padding: "10px" }}>
<h3>React Sidebar</h3>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<br />
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Enter text..."
style={{ marginTop: "10px", width: "100%" }}
/>
<p>You typed: {text}</p>
</div>
);
}
// Mount React component
ReactDOM.createRoot(container).render(
<React.StrictMode>
<SidebarContent />
</React.StrictMode>
);
}
});
Dynamic Content Updates
Stats Tab with Real-time Updates
app.extensionManager.registerSidebarTab({
id: "dynamicSidebar",
icon: "pi pi-chart-line",
title: "Stats",
type: "custom",
render: (el) => {
const container = document.createElement('div');
container.style.padding = '10px';
el.appendChild(container);
// Function to update stats
function updateStats() {
const stats = {
nodes: app.graph._nodes.length,
connections: Object.keys(app.graph.links).length,
groups: app.graph._groups ? app.graph._groups.length : 0
};
container.innerHTML = `
<h3>Workflow Stats</h3>
<ul>
<li>Nodes: ${stats.nodes}</li>
<li>Connections: ${stats.connections}</li>
<li>Groups: ${stats.groups}</li>
</ul>
<button id="refreshStats">Refresh</button>
`;
// Add refresh button functionality
const refreshBtn = container.querySelector('#refreshStats');
if (refreshBtn) {
refreshBtn.addEventListener('click', updateStats);
}
}
// Initial update
updateStats();
// Listen for graph changes
const api = app.api;
api.addEventListener("graphChanged", updateStats);
// Clean up listeners when tab is destroyed
return () => {
api.removeEventListener("graphChanged", updateStats);
};
}
});
Advanced Examples
File Manager Sidebar
app.extensionManager.registerSidebarTab({
id: "fileManager",
icon: "pi pi-folder",
title: "Files",
type: "custom",
render: (el) => {
const container = document.createElement('div');
container.style.padding = '10px';
el.appendChild(container);
// File list container
const fileList = document.createElement('div');
fileList.style.height = '300px';
fileList.style.overflowY = 'auto';
fileList.style.border = '1px solid #ccc';
fileList.style.padding = '10px';
fileList.style.marginBottom = '10px';
// Controls
const controls = document.createElement('div');
controls.style.marginBottom = '10px';
const uploadBtn = document.createElement('button');
uploadBtn.textContent = 'Upload File';
uploadBtn.style.marginRight = '10px';
const refreshBtn = document.createElement('button');
refreshBtn.textContent = 'Refresh';
// File input (hidden)
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.style.display = 'none';
fileInput.multiple = true;
// Load files
function loadFiles() {
// Simulate file loading
const files = [
{ name: 'image1.jpg', size: '2.5 MB', type: 'image' },
{ name: 'document.pdf', size: '1.2 MB', type: 'document' },
{ name: 'data.json', size: '0.5 MB', type: 'data' }
];
fileList.innerHTML = files.map(file => `
<div style="padding: 5px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between;">
<span>${file.name}</span>
<span style="color: #666;">${file.size}</span>
</div>
`).join('');
}
// Upload files
uploadBtn.addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', (e) => {
const files = Array.from(e.target.files);
files.forEach(file => {
console.log('Uploaded file:', file.name);
});
loadFiles();
});
// Refresh files
refreshBtn.addEventListener('click', loadFiles);
// Assemble UI
controls.appendChild(uploadBtn);
controls.appendChild(refreshBtn);
container.appendChild(controls);
container.appendChild(fileList);
container.appendChild(fileInput);
// Load initial files
loadFiles();
}
});
Debug Console Sidebar
app.extensionManager.registerSidebarTab({
id: "debugConsole",
icon: "pi pi-bug",
title: "Debug",
type: "custom",
render: (el) => {
const container = document.createElement('div');
container.style.padding = '10px';
el.appendChild(container);
// Console output
const consoleOutput = document.createElement('div');
consoleOutput.style.height = '300px';
consoleOutput.style.overflowY = 'auto';
consoleOutput.style.border = '1px solid #ccc';
consoleOutput.style.padding = '10px';
consoleOutput.style.backgroundColor = '#1e1e1e';
consoleOutput.style.color = '#ffffff';
consoleOutput.style.fontFamily = 'monospace';
consoleOutput.style.fontSize = '12px';
// Controls
const controls = document.createElement('div');
controls.style.marginBottom = '10px';
const clearBtn = document.createElement('button');
clearBtn.textContent = 'Clear';
clearBtn.style.marginRight = '10px';
const logBtn = document.createElement('button');
logBtn.textContent = 'Test Log';
logBtn.style.marginRight = '10px';
const errorBtn = document.createElement('button');
errorBtn.textContent = 'Test Error';
// Log function
function log(message, type = 'log') {
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.style.marginBottom = '2px';
logEntry.style.padding = '2px';
const color = type === 'error' ? '#ff6b6b' :
type === 'warn' ? '#ffa726' : '#4caf50';
logEntry.innerHTML = `
<span style="color: #666;">[${timestamp}]</span>
<span style="color: ${color};">[${type.toUpperCase()}]</span>
<span>${message}</span>
`;
consoleOutput.appendChild(logEntry);
consoleOutput.scrollTop = consoleOutput.scrollHeight;
}
// Clear console
clearBtn.addEventListener('click', () => {
consoleOutput.innerHTML = '';
});
// Test log
logBtn.addEventListener('click', () => {
log('This is a test log message');
});
// Test error
errorBtn.addEventListener('click', () => {
log('This is a test error message', 'error');
});
// Assemble UI
controls.appendChild(clearBtn);
controls.appendChild(logBtn);
controls.appendChild(errorBtn);
container.appendChild(controls);
container.appendChild(consoleOutput);
// Initial log
log('Debug console initialized');
}
});
Best Practices
Tab Design
// Good: Clear, descriptive titles and icons
app.extensionManager.registerSidebarTab({
id: "fileManager",
icon: "pi pi-folder",
title: "File Manager",
tooltip: "Manage your files and assets",
type: "custom",
render: (el) => { /* ... */ }
});
// Bad: Vague, unclear titles
app.extensionManager.registerSidebarTab({
id: "tab1",
icon: "pi pi-circle",
title: "Tab",
type: "custom",
render: (el) => { /* ... */ }
});
Content Organization
// Organize content with proper styling and structure
render: (el) => {
const container = document.createElement('div');
container.style.padding = '10px';
// Header
const header = document.createElement('h3');
header.textContent = 'Panel Title';
header.style.marginTop = '0';
header.style.marginBottom = '15px';
// Content area
const content = document.createElement('div');
content.style.marginBottom = '10px';
// Controls
const controls = document.createElement('div');
controls.style.marginTop = '10px';
// Assemble
container.appendChild(header);
container.appendChild(content);
container.appendChild(controls);
el.appendChild(container);
}
Event Handling and Cleanup
// Proper event handling and cleanup
render: (el) => {
const container = document.createElement('div');
el.appendChild(container);
// Store event listeners for cleanup
const eventListeners = [];
const button = document.createElement('button');
button.textContent = 'Click Me';
const handleClick = () => {
console.log('Button clicked');
};
button.addEventListener('click', handleClick);
eventListeners.push({ element: button, event: 'click', handler: handleClick });
container.appendChild(button);
// Return cleanup function
return () => {
eventListeners.forEach(({ element, event, handler }) => {
element.removeEventListener(event, handler);
});
};
}
Performance Considerations
// Efficient content updates
function createEfficientTab() {
let updateTimeout;
return {
id: "efficientTab",
icon: "pi pi-chart-line",
title: "Efficient Tab",
type: "custom",
render: (el) => {
const container = document.createElement('div');
el.appendChild(container);
// Debounced update function
function updateContent() {
clearTimeout(updateTimeout);
updateTimeout = setTimeout(() => {
// Perform expensive operations here
container.innerHTML = `<div>Updated: ${new Date().toLocaleTimeString()}</div>`;
}, 100);
}
// Listen for changes
app.api.addEventListener("graphChanged", updateContent);
// Initial update
updateContent();
// Cleanup
return () => {
clearTimeout(updateTimeout);
app.api.removeEventListener("graphChanged", updateContent);
};
}
};
}
ComfyUI Selection Toolbox - Context-Sensitive Actions
Purpose
ComfyUI's Selection Toolbox API allows extensions to add custom action buttons that appear when nodes are selected on the canvas. This provides quick access to context-sensitive commands for selected items (nodes, groups, etc.).
Basic Usage
Simple Selection Toolbox Registration
app.registerExtension({
name: "MyExtension",
commands: [
{
id: "my-extension.duplicate-special",
label: "Duplicate Special",
icon: "pi pi-copy",
function: (selectedItem) => {
// Your command logic here
console.log("Duplicating selected nodes with special behavior");
}
}
],
getSelectionToolboxCommands: (selectedItem) => {
// Return array of command IDs to show in the toolbox
return ["my-extension.duplicate-special"];
}
});
Features:
- Context-sensitive: Commands appear based on selection
- Quick access: Immediate action buttons for selected items
- Dynamic visibility: Commands can change based on selection
- Standard interface: Uses ComfyUI's command system
Command Definition
Required Properties
{
id: string, // Unique identifier for the command
label: string, // Display text for the button tooltip
icon?: string, // Icon class for the button (optional)
function: (selectedItem) => void // Function executed when clicked
}
Properties:
- id: Unique identifier for the command
- label: Display text for the button tooltip
- icon: Optional icon class for the button
- function: Function executed when clicked, receives selected item(s)
Icon Options
Supported Icon Libraries
// PrimeVue icons
"pi pi-copy" // Copy icon
"pi pi-align-left" // Align left icon
"pi pi-cog" // Settings icon
"pi pi-info-circle" // Information icon
"pi pi-hashtag" // Number icon
"pi pi-star" // Star icon
// Material Design icons
"mdi mdi-content-copy" // Copy icon
"mdi mdi-align-horizontal-left" // Align icon
"mdi mdi-cog" // Settings icon
"mdi mdi-information" // Information icon
Dynamic Command Visibility
Context-Sensitive Commands
app.registerExtension({
name: "ContextualCommands",
commands: [
{
id: "my-ext.align-nodes",
label: "Align Nodes",
icon: "pi pi-align-left",
function: () => {
// Align multiple nodes
const selectedItems = Array.from(app.canvas.selectedItems || []);
console.log(`Aligning ${selectedItems.length} nodes`);
}
},
{
id: "my-ext.configure-single",
label: "Configure",
icon: "pi pi-cog",
function: () => {
// Configure single node
const selectedItems = Array.from(app.canvas.selectedItems || []);
if (selectedItems.length === 1) {
console.log(`Configuring node: ${selectedItems[0].type}`);
}
}
},
{
id: "my-ext.group-selection",
label: "Group Selection",
icon: "pi pi-objects-column",
function: () => {
// Group multiple items
const selectedItems = Array.from(app.canvas.selectedItems || []);
console.log(`Grouping ${selectedItems.length} items`);
}
}
],
getSelectionToolboxCommands: (selectedItem) => {
const selectedItems = app.canvas.selectedItems;
const itemCount = selectedItems ? selectedItems.size : 0;
if (itemCount > 1) {
// Show alignment and grouping commands for multiple items
return ["my-ext.align-nodes", "my-ext.group-selection"];
} else if (itemCount === 1) {
// Show configuration for single item
return ["my-ext.configure-single"];
}
return [];
}
});
Working with Selected Items
Selection Information Access
app.registerExtension({
name: "SelectionInfo",
commands: [
{
id: "my-ext.show-info",
label: "Show Selection Info",
icon: "pi pi-info-circle",
function: () => {
const selectedItems = app.canvas.selectedItems;
if (selectedItems && selectedItems.size > 0) {
console.log(`Selected ${selectedItems.size} items`);
// Iterate through selected items
selectedItems.forEach(item => {
if (item.type) {
console.log(`Item: ${item.type} (ID: ${item.id})`);
}
});
}
}
},
{
id: "my-ext.get-positions",
label: "Get Positions",
icon: "pi pi-map-marker",
function: () => {
const selectedItems = Array.from(app.canvas.selectedItems || []);
const positions = selectedItems.map(item => ({
id: item.id,
type: item.type,
pos: item.pos
}));
console.log("Selected item positions:", positions);
}
}
],
getSelectionToolboxCommands: () => ["my-ext.show-info", "my-ext.get-positions"]
});
Complete Example
Selection Tools Extension
app.registerExtension({
name: "SelectionTools",
commands: [
{
id: "selection-tools.count",
label: "Count Selection",
icon: "pi pi-hashtag",
function: () => {
const count = app.canvas.selectedItems?.size || 0;
app.extensionManager.toast.add({
severity: "info",
summary: "Selection Count",
detail: `You have ${count} item${count !== 1 ? 's' : ''} selected`,
life: 3000
});
}
},
{
id: "selection-tools.copy-ids",
label: "Copy IDs",
icon: "pi pi-copy",
function: () => {
const items = Array.from(app.canvas.selectedItems || []);
const ids = items.map(item => item.id).filter(id => id !== undefined);
if (ids.length > 0) {
navigator.clipboard.writeText(ids.join(', '));
app.extensionManager.toast.add({
severity: "success",
summary: "Copied",
detail: `Copied ${ids.length} IDs to clipboard`,
life: 2000
});
}
}
},
{
id: "selection-tools.log-types",
label: "Log Types",
icon: "pi pi-info-circle",
function: () => {
const items = Array.from(app.canvas.selectedItems || []);
const typeCount = {};
items.forEach(item => {
const type = item.type || 'unknown';
typeCount[type] = (typeCount[type] || 0) + 1;
});
console.log("Selection types:", typeCount);
}
},
{
id: "selection-tools.duplicate",
label: "Duplicate Selection",
icon: "pi pi-clone",
function: () => {
const selectedItems = Array.from(app.canvas.selectedItems || []);
console.log(`Duplicating ${selectedItems.length} items`);
// Implement duplication logic here
selectedItems.forEach(item => {
console.log(`Duplicating: ${item.type} (ID: ${item.id})`);
});
}
},
{
id: "selection-tools.delete",
label: "Delete Selection",
icon: "pi pi-trash",
function: () => {
const selectedItems = Array.from(app.canvas.selectedItems || []);
const count = selectedItems.length;
if (confirm(`Are you sure you want to delete ${count} item${count !== 1 ? 's' : ''}?`)) {
console.log(`Deleting ${count} items`);
// Implement deletion logic here
}
}
}
],
getSelectionToolboxCommands: (selectedItem) => {
const selectedItems = app.canvas.selectedItems;
const itemCount = selectedItems ? selectedItems.size : 0;
if (itemCount === 0) return [];
const commands = ["selection-tools.count", "selection-tools.log-types"];
// Only show copy command if items have IDs
const hasIds = Array.from(selectedItems).some(item => item.id !== undefined);
if (hasIds) {
commands.push("selection-tools.copy-ids");
}
// Show duplication and deletion for multiple items
if (itemCount > 1) {
commands.push("selection-tools.duplicate", "selection-tools.delete");
}
return commands;
}
});
Advanced Examples
Node Alignment Tools
app.registerExtension({
name: "NodeAlignment",
commands: [
{
id: "align.left",
label: "Align Left",
icon: "pi pi-align-left",
function: () => {
const selectedItems = Array.from(app.canvas.selectedItems || []);
if (selectedItems.length < 2) return;
// Find leftmost position
const leftmost = Math.min(...selectedItems.map(item => item.pos[0]));
// Align all items to leftmost position
selectedItems.forEach(item => {
item.pos[0] = leftmost;
item.setDirtyCanvas(true);
});
app.extensionManager.toast.add({
severity: "success",
summary: "Aligned",
detail: "Nodes aligned to left",
life: 2000
});
}
},
{
id: "align.center",
label: "Align Center",
icon: "pi pi-align-center",
function: () => {
const selectedItems = Array.from(app.canvas.selectedItems || []);
if (selectedItems.length < 2) return;
// Calculate center position
const centerX = selectedItems.reduce((sum, item) => sum + item.pos[0], 0) / selectedItems.length;
// Align all items to center
selectedItems.forEach(item => {
item.pos[0] = centerX;
item.setDirtyCanvas(true);
});
app.extensionManager.toast.add({
severity: "success",
summary: "Aligned",
detail: "Nodes aligned to center",
life: 2000
});
}
},
{
id: "align.distribute",
label: "Distribute Evenly",
icon: "pi pi-objects-column",
function: () => {
const selectedItems = Array.from(app.canvas.selectedItems || []);
if (selectedItems.length < 3) return;
// Sort by X position
selectedItems.sort((a, b) => a.pos[0] - b.pos[0]);
// Calculate spacing
const leftmost = selectedItems[0].pos[0];
const rightmost = selectedItems[selectedItems.length - 1].pos[0];
const spacing = (rightmost - leftmost) / (selectedItems.length - 1);
// Distribute items
selectedItems.forEach((item, index) => {
item.pos[0] = leftmost + (spacing * index);
item.setDirtyCanvas(true);
});
app.extensionManager.toast.add({
severity: "success",
summary: "Distributed",
detail: "Nodes distributed evenly",
life: 2000
});
}
}
],
getSelectionToolboxCommands: (selectedItem) => {
const selectedItems = app.canvas.selectedItems;
const itemCount = selectedItems ? selectedItems.size : 0;
if (itemCount < 2) return [];
const commands = ["align.left", "align.center"];
if (itemCount >= 3) {
commands.push("align.distribute");
}
return commands;
}
});
Node Grouping Tools
app.registerExtension({
name: "NodeGrouping",
commands: [
{
id: "group.create",
label: "Create Group",
icon: "pi pi-objects-column",
function: () => {
const selectedItems = Array.from(app.canvas.selectedItems || []);
if (selectedItems.length < 2) return;
// Calculate group bounds
const positions = selectedItems.map(item => item.pos);
const minX = Math.min(...positions.map(pos => pos[0]));
const minY = Math.min(...positions.map(pos => pos[1]));
const maxX = Math.max(...positions.map(pos => pos[0]));
const maxY = Math.max(...positions.map(pos => pos[1]));
// Create group
const group = {
id: `group_${Date.now()}`,
title: "New Group",
pos: [minX, minY],
size: [maxX - minX + 200, maxY - minY + 100],
nodes: selectedItems.map(item => item.id)
};
console.log("Created group:", group);
app.extensionManager.toast.add({
severity: "success",
summary: "Group Created",
detail: `Grouped ${selectedItems.length} items`,
life: 2000
});
}
},
{
id: "group.ungroup",
label: "Ungroup",
icon: "pi pi-objects-column",
function: () => {
const selectedItems = Array.from(app.canvas.selectedItems || []);
const groups = selectedItems.filter(item => item.type === 'group');
if (groups.length === 0) return;
console.log(`Ungrouping ${groups.length} group${groups.length !== 1 ? 's' : ''}`);
app.extensionManager.toast.add({
severity: "success",
summary: "Ungrouped",
detail: `Ungrouped ${groups.length} group${groups.length !== 1 ? 's' : ''}`,
life: 2000
});
}
}
],
getSelectionToolboxCommands: (selectedItem) => {
const selectedItems = app.canvas.selectedItems;
const itemCount = selectedItems ? selectedItems.size : 0;
if (itemCount === 0) return [];
const commands = [];
// Show create group for multiple items
if (itemCount >= 2) {
commands.push("group.create");
}
// Show ungroup for groups
const hasGroups = Array.from(selectedItems).some(item => item.type === 'group');
if (hasGroups) {
commands.push("group.ungroup");
}
return commands;
}
});
Best Practices
Command Design
// Good: Clear, descriptive labels and appropriate icons
{
id: "my-ext.align-nodes",
label: "Align Nodes",
icon: "pi pi-align-left",
function: () => { /* ... */ }
}
// Bad: Vague, unclear labels
{
id: "my-ext.action1",
label: "Action",
icon: "pi pi-circle",
function: () => { /* ... */ }
}
Selection Handling
// Proper selection validation
getSelectionToolboxCommands: (selectedItem) => {
const selectedItems = app.canvas.selectedItems;
const itemCount = selectedItems ? selectedItems.size : 0;
// Early return for empty selection
if (itemCount === 0) return [];
// Validate selection types
const hasNodes = Array.from(selectedItems).some(item => item.type && item.type !== 'group');
const hasGroups = Array.from(selectedItems).some(item => item.type === 'group');
const commands = [];
if (hasNodes) {
commands.push("node-commands");
}
if (hasGroups) {
commands.push("group-commands");
}
return commands;
}
Performance Considerations
// Efficient selection processing
getSelectionToolboxCommands: (selectedItem) => {
const selectedItems = app.canvas.selectedItems;
const itemCount = selectedItems ? selectedItems.size : 0;
// Cache expensive operations
if (itemCount > 0 && !this._cachedSelection) {
this._cachedSelection = {
count: itemCount,
types: Array.from(selectedItems).map(item => item.type),
hasIds: Array.from(selectedItems).some(item => item.id !== undefined)
};
}
// Use cached data for command decisions
if (this._cachedSelection) {
const { count, types, hasIds } = this._cachedSelection;
const commands = [];
if (count > 1) {
commands.push("multi-selection-command");
}
if (hasIds) {
commands.push("id-based-command");
}
return commands;
}
return [];
}
Important Notes
Settings Requirement
// The selection toolbox must be enabled in settings
// Comfy.Canvas.SelectionToolbox must be enabled
Command Registration
// Commands must be defined in the commands array before being referenced
app.registerExtension({
name: "MyExtension",
commands: [
// Define commands first
{
id: "my-command",
label: "My Command",
function: () => { /* ... */ }
}
],
getSelectionToolboxCommands: (selectedItem) => {
// Then reference them here
return ["my-command"];
}
});
Selection Access
// Use app.canvas.selectedItems for all selected items
const selectedItems = app.canvas.selectedItems; // Set of all selected items
// For backward compatibility, app.canvas.selected_nodes still exists
const selectedNodes = app.canvas.selected_nodes; // Only nodes (deprecated)
}
ComfyUI Commands and Keybindings - Keyboard Shortcuts
Purpose
ComfyUI's Commands and Keybindings API allows extensions to register custom commands and associate them with keyboard shortcuts. This enables users to quickly trigger actions without using the mouse.
Basic Usage
Simple Command and Keybinding Registration
app.registerExtension({
name: "MyExtension",
// Register commands
commands: [
{
id: "myCommand",
label: "My Command",
function: () => {
console.log("Command executed!");
}
}
],
// Associate keybindings with commands
keybindings: [
{
combo: { key: "k", ctrl: true },
commandId: "myCommand"
}
]
});
Features:
- Keyboard shortcuts: Quick access to extension functions
- Command system: Standardized command interface
- User productivity: Faster workflow execution
- Customizable: Users can modify keybindings
Command Configuration
Required Properties
{
id: string, // Unique identifier for the command
label: string, // Display name for the command
function: () => void // Function to execute when command is triggered
}
Properties:
- id: Unique identifier for the command
- label: Display name for the command
- function: Function to execute when command is triggered
Keybinding Configuration
Required Properties
{
combo: { // Key combination
key: string, // The main key (single character or special key)
ctrl?: boolean, // Require Ctrl key (optional)
shift?: boolean, // Require Shift key (optional)
alt?: boolean, // Require Alt key (optional)
meta?: boolean // Require Meta/Command key (optional)
},
commandId: string // ID of the command to trigger
}
Properties:
- combo: Key combination object
- commandId: ID of the command to trigger
- Modifier keys: ctrl, shift, alt, meta (optional)
Special Keys
Arrow Keys
"ArrowUp" // Up arrow
"ArrowDown" // Down arrow
"ArrowLeft" // Left arrow
"ArrowRight" // Right arrow
Function Keys
"F1" through "F12" // Function keys F1-F12
Other Special Keys
"Escape" // Escape key
"Tab" // Tab key
"Enter" // Enter key
"Backspace" // Backspace key
"Delete" // Delete key
"Home" // Home key
"End" // End key
"PageUp" // Page Up key
"PageDown" // Page Down key
Command Examples
Workflow Management Commands
app.registerExtension({
name: "WorkflowCommands",
commands: [
{
id: "runWorkflow",
label: "Run Workflow",
function: () => {
app.queuePrompt();
}
},
{
id: "clearWorkflow",
label: "Clear Workflow",
function: () => {
if (confirm("Clear the workflow?")) {
app.graph.clear();
}
}
},
{
id: "saveWorkflow",
label: "Save Workflow",
function: () => {
app.graphToPrompt().then(workflow => {
const blob = new Blob([JSON.stringify(workflow)], {type: "application/json"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "workflow.json";
a.click();
URL.revokeObjectURL(url);
});
}
},
{
id: "loadWorkflow",
label: "Load Workflow",
function: () => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const workflow = JSON.parse(e.target.result);
app.loadGraphData(workflow);
} catch (error) {
console.error("Error loading workflow:", error);
}
};
reader.readAsText(file);
}
};
input.click();
}
}
]
});
Node Management Commands
app.registerExtension({
name: "NodeCommands",
commands: [
{
id: "duplicateSelected",
label: "Duplicate Selected",
function: () => {
const selectedItems = Array.from(app.canvas.selectedItems || []);
if (selectedItems.length > 0) {
console.log(`Duplicating ${selectedItems.length} items`);
// Implement duplication logic
}
}
},
{
id: "deleteSelected",
label: "Delete Selected",
function: () => {
const selectedItems = Array.from(app.canvas.selectedItems || []);
if (selectedItems.length > 0) {
if (confirm(`Delete ${selectedItems.length} item${selectedItems.length !== 1 ? 's' : ''}?`)) {
console.log(`Deleting ${selectedItems.length} items`);
// Implement deletion logic
}
}
}
},
{
id: "selectAll",
label: "Select All",
function: () => {
const nodes = app.graph._nodes;
nodes.forEach(node => {
node.setSelected(true);
});
app.canvas.setDirty(true);
}
},
{
id: "deselectAll",
label: "Deselect All",
function: () => {
const selectedItems = Array.from(app.canvas.selectedItems || []);
selectedItems.forEach(item => {
item.setSelected(false);
});
app.canvas.setDirty(true);
}
}
]
});
Keybinding Examples
Workflow Keybindings
app.registerExtension({
name: "WorkflowKeybindings",
commands: [
/* Commands defined above */
],
keybindings: [
// Ctrl+R to run workflow
{
combo: { key: "r", ctrl: true },
commandId: "runWorkflow"
},
// Ctrl+Shift+C to clear workflow
{
combo: { key: "c", ctrl: true, shift: true },
commandId: "clearWorkflow"
},
// Ctrl+S to save workflow
{
combo: { key: "s", ctrl: true },
commandId: "saveWorkflow"
},
// Ctrl+O to load workflow
{
combo: { key: "o", ctrl: true },
commandId: "loadWorkflow"
},
// F5 to run workflow (alternative)
{
combo: { key: "F5" },
commandId: "runWorkflow"
}
]
});
Node Management Keybindings
app.registerExtension({
name: "NodeKeybindings",
commands: [
/* Commands defined above */
],
keybindings: [
// Ctrl+D to duplicate selected
{
combo: { key: "d", ctrl: true },
commandId: "duplicateSelected"
},
// Delete key to delete selected
{
combo: { key: "Delete" },
commandId: "deleteSelected"
},
// Ctrl+A to select all
{
combo: { key: "a", ctrl: true },
commandId: "selectAll"
},
// Escape to deselect all
{
combo: { key: "Escape" },
commandId: "deselectAll"
}
]
});
Advanced Examples
Extension Management Commands
app.registerExtension({
name: "ExtensionManager",
commands: [
{
id: "toggleExtension",
label: "Toggle Extension",
function: () => {
const extensionName = "MyExtension";
const isEnabled = app.extensionManager.isEnabled(extensionName);
if (isEnabled) {
app.extensionManager.disable(extensionName);
app.extensionManager.toast.add({
severity: "info",
summary: "Extension Disabled",
detail: `${extensionName} has been disabled`,
life: 3000
});
} else {
app.extensionManager.enable(extensionName);
app.extensionManager.toast.add({
severity: "success",
summary: "Extension Enabled",
detail: `${extensionName} has been enabled`,
life: 3000
});
}
}
},
{
id: "reloadExtensions",
label: "Reload Extensions",
function: () => {
app.extensionManager.reload();
app.extensionManager.toast.add({
severity: "info",
summary: "Extensions Reloaded",
detail: "All extensions have been reloaded",
life: 3000
});
}
},
{
id: "showExtensionInfo",
label: "Show Extension Info",
function: () => {
const extensions = app.extensionManager.getExtensions();
console.log("Loaded extensions:", extensions);
app.extensionManager.toast.add({
severity: "info",
summary: "Extension Info",
detail: `${extensions.length} extensions loaded`,
life: 3000
});
}
}
],
keybindings: [
// Ctrl+Shift+E to toggle extension
{
combo: { key: "e", ctrl: true, shift: true },
commandId: "toggleExtension"
},
// Ctrl+Shift+R to reload extensions
{
combo: { key: "r", ctrl: true, shift: true },
commandId: "reloadExtensions"
},
// F12 to show extension info
{
combo: { key: "F12" },
commandId: "showExtensionInfo"
}
]
});
Canvas Navigation Commands
app.registerExtension({
name: "CanvasNavigation",
commands: [
{
id: "zoomIn",
label: "Zoom In",
function: () => {
app.canvas.zoomTo(1.2);
}
},
{
id: "zoomOut",
label: "Zoom Out",
function: () => {
app.canvas.zoomTo(0.8);
}
},
{
id: "zoomFit",
label: "Zoom to Fit",
function: () => {
app.canvas.zoomToFit();
}
},
{
id: "panLeft",
label: "Pan Left",
function: () => {
const currentOffset = app.canvas.offset;
app.canvas.offset = [currentOffset[0] - 100, currentOffset[1]];
}
},
{
id: "panRight",
label: "Pan Right",
function: () => {
const currentOffset = app.canvas.offset;
app.canvas.offset = [currentOffset[0] + 100, currentOffset[1]];
}
},
{
id: "panUp",
label: "Pan Up",
function: () => {
const currentOffset = app.canvas.offset;
app.canvas.offset = [currentOffset[0], currentOffset[1] - 100];
}
},
{
id: "panDown",
label: "Pan Down",
function: () => {
const currentOffset = app.canvas.offset;
app.canvas.offset = [currentOffset[0], currentOffset[1] + 100];
}
}
],
keybindings: [
// Ctrl+Plus to zoom in
{
combo: { key: "=", ctrl: true },
commandId: "zoomIn"
},
// Ctrl+Minus to zoom out
{
combo: { key: "-", ctrl: true },
commandId: "zoomOut"
},
// Ctrl+0 to zoom to fit
{
combo: { key: "0", ctrl: true },
commandId: "zoomFit"
},
// Arrow keys for panning
{
combo: { key: "ArrowLeft" },
commandId: "panLeft"
},
{
combo: { key: "ArrowRight" },
commandId: "panRight"
},
{
combo: { key: "ArrowUp" },
commandId: "panUp"
},
{
combo: { key: "ArrowDown" },
commandId: "panDown"
}
]
});
Best Practices
Command Design
// Good: Clear, descriptive labels and unique IDs
{
id: "my-extension.run-workflow",
label: "Run Workflow",
function: () => { /* ... */ }
}
// Bad: Vague, unclear labels
{
id: "cmd1",
label: "Action",
function: () => { /* ... */ }
}
Keybinding Design
// Good: Intuitive, memorable key combinations
{
combo: { key: "r", ctrl: true },
commandId: "runWorkflow"
}
// Bad: Confusing, hard to remember combinations
{
combo: { key: "q", ctrl: true, shift: true, alt: true },
commandId: "runWorkflow"
}
Error Handling
{
id: "safeCommand",
label: "Safe Command",
function: () => {
try {
// Perform potentially dangerous operation
app.graph.clear();
app.extensionManager.toast.add({
severity: "success",
summary: "Success",
detail: "Operation completed successfully",
life: 3000
});
} catch (error) {
console.error("Command failed:", error);
app.extensionManager.toast.add({
severity: "error",
summary: "Error",
detail: "Operation failed: " + error.message,
life: 5000
});
}
}
}
User Confirmation
{
id: "destructiveCommand",
label: "Destructive Command",
function: () => {
if (confirm("This action cannot be undone. Continue?")) {
// Perform destructive operation
console.log("Destructive operation executed");
}
}
}
Important Notes and Limitations
Core Keybindings
// Keybindings defined in ComfyUI core cannot be overwritten
// Check these source files for core keybindings:
// - Core Commands
// - Core Menu Commands
// - Core Keybindings
Reserved Key Combinations
// Some key combinations are reserved by the browser
// Examples of reserved combinations:
// - Ctrl+F: Browser search
// - Ctrl+R: Browser refresh
// - Ctrl+T: New tab
// - Ctrl+W: Close tab
// - Ctrl+N: New window
// - Ctrl+Shift+N: New incognito window
Extension Conflicts
// If multiple extensions register the same keybinding, behavior is undefined
// Use unique key combinations to avoid conflicts
{
combo: { key: "r", ctrl: true, shift: true }, // Unique combination
commandId: "myUniqueCommand"
}
Keybinding Validation
// Validate key combinations before registration
function validateKeybinding(combo) {
const reserved = [
{ key: "f", ctrl: true },
{ key: "r", ctrl: true },
{ key: "t", ctrl: true },
{ key: "w", ctrl: true },
{ key: "n", ctrl: true }
];
return !reserved.some(reserved =>
reserved.key === combo.key &&
reserved.ctrl === combo.ctrl
);
}
ComfyUI Topbar Menu - Custom Menu Items
Purpose
ComfyUI's Topbar Menu API allows extensions to add custom menu items to the ComfyUI's top menu bar. This is useful for providing access to advanced features or less frequently used commands.
Basic Usage
Simple Menu Registration
app.registerExtension({
name: "MyExtension",
// Define commands
commands: [
{
id: "myCommand",
label: "My Command",
function: () => { alert("Command executed!"); }
}
],
// Add commands to menu
menuCommands: [
{
path: ["Extensions", "My Extension"],
commands: ["myCommand"]
}
]
});
Features:
- Menu integration: Add items to ComfyUI's top menu bar
- Command system: Uses the same command system as keybindings
- Menu hierarchy: Create nested menu structures
- Multiple locations: Add commands to multiple menu locations
Command Configuration
Required Properties
{
id: string, // Unique identifier for the command
label: string, // Display name for the command
function: () => void // Function to execute when command is triggered
}
Properties:
- id: Unique identifier for the command
- label: Display name for the command
- function: Function to execute when command is triggered
Menu Configuration
Required Properties
{
path: string[], // Array representing menu hierarchy
commands: string[] // Array of command IDs to add at this location
}
Properties:
- path: Array representing menu hierarchy
- commands: Array of command IDs to add at this location
Menu Examples
Adding to Existing Menus
app.registerExtension({
name: "MenuExamples",
commands: [
{
id: "saveAsImage",
label: "Save as Image",
function: () => {
// Code to save canvas as image
const canvas = app.canvas.canvas;
const dataURL = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = 'workflow.png';
link.href = dataURL;
link.click();
}
},
{
id: "exportWorkflow",
label: "Export Workflow",
function: () => {
// Code to export workflow
app.graphToPrompt().then(workflow => {
const blob = new Blob([JSON.stringify(workflow)], {type: "application/json"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "workflow.json";
a.click();
URL.revokeObjectURL(url);
});
}
},
{
id: "importWorkflow",
label: "Import Workflow",
function: () => {
// Code to import workflow
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const workflow = JSON.parse(e.target.result);
app.loadGraphData(workflow);
} catch (error) {
console.error("Error loading workflow:", error);
}
};
reader.readAsText(file);
}
};
input.click();
}
}
],
menuCommands: [
// Add to File menu
{
path: ["File"],
commands: ["saveAsImage", "exportWorkflow", "importWorkflow"]
}
]
});
Creating Submenu Structure
app.registerExtension({
name: "SubmenuExample",
commands: [
{
id: "option1",
label: "Option 1",
function: () => { console.log("Option 1"); }
},
{
id: "option2",
label: "Option 2",
function: () => { console.log("Option 2"); }
},
{
id: "suboption1",
label: "Sub-option 1",
function: () => { console.log("Sub-option 1"); }
},
{
id: "suboption2",
label: "Sub-option 2",
function: () => { console.log("Sub-option 2"); }
},
{
id: "advancedOption",
label: "Advanced Option",
function: () => { console.log("Advanced Option"); }
}
],
menuCommands: [
// Create a nested menu structure
{
path: ["Extensions", "My Tools"],
commands: ["option1", "option2"]
},
{
path: ["Extensions", "My Tools", "Advanced"],
commands: ["suboption1", "suboption2"]
},
{
path: ["Extensions", "My Tools", "Advanced", "Expert"],
commands: ["advancedOption"]
}
]
});
Multiple Menu Locations
app.registerExtension({
name: "MultiLocationExample",
commands: [
{
id: "helpCommand",
label: "Get Help",
function: () => { window.open("https://docs.example.com", "_blank"); }
},
{
id: "documentation",
label: "Documentation",
function: () => { window.open("https://docs.example.com", "_blank"); }
},
{
id: "support",
label: "Support",
function: () => { window.open("https://support.example.com", "_blank"); }
}
],
menuCommands: [
// Add to Help menu
{
path: ["Help"],
commands: ["helpCommand", "documentation", "support"]
},
// Also add to Extensions menu
{
path: ["Extensions"],
commands: ["helpCommand"]
}
]
});
Advanced Examples
Workflow Management Menu
app.registerExtension({
name: "WorkflowManager",
commands: [
{
id: "newWorkflow",
label: "New Workflow",
function: () => {
if (confirm("Create a new workflow? Unsaved changes will be lost.")) {
app.graph.clear();
}
}
},
{
id: "saveWorkflow",
label: "Save Workflow",
function: () => {
app.graphToPrompt().then(workflow => {
const blob = new Blob([JSON.stringify(workflow)], {type: "application/json"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "workflow.json";
a.click();
URL.revokeObjectURL(url);
});
}
},
{
id: "loadWorkflow",
label: "Load Workflow",
function: () => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const workflow = JSON.parse(e.target.result);
app.loadGraphData(workflow);
} catch (error) {
console.error("Error loading workflow:", error);
}
};
reader.readAsText(file);
}
};
input.click();
}
},
{
id: "runWorkflow",
label: "Run Workflow",
function: () => {
app.queuePrompt();
}
},
{
id: "clearWorkflow",
label: "Clear Workflow",
function: () => {
if (confirm("Clear the workflow?")) {
app.graph.clear();
}
}
}
],
menuCommands: [
{
path: ["File"],
commands: ["newWorkflow", "saveWorkflow", "loadWorkflow"]
},
{
path: ["Run"],
commands: ["runWorkflow", "clearWorkflow"]
}
]
});
Node Management Menu
app.registerExtension({
name: "NodeManager",
commands: [
{
id: "selectAll",
label: "Select All",
function: () => {
const nodes = app.graph._nodes;
nodes.forEach(node => {
node.setSelected(true);
});
app.canvas.setDirty(true);
}
},
{
id: "deselectAll",
label: "Deselect All",
function: () => {
const selectedItems = Array.from(app.canvas.selectedItems || []);
selectedItems.forEach(item => {
item.setSelected(false);
});
app.canvas.setDirty(true);
}
},
{
id: "duplicateSelected",
label: "Duplicate Selected",
function: () => {
const selectedItems = Array.from(app.canvas.selectedItems || []);
if (selectedItems.length > 0) {
console.log(`Duplicating ${selectedItems.length} items`);
// Implement duplication logic
}
}
},
{
id: "deleteSelected",
label: "Delete Selected",
function: () => {
const selectedItems = Array.from(app.canvas.selectedItems || []);
if (selectedItems.length > 0) {
if (confirm(`Delete ${selectedItems.length} item${selectedItems.length !== 1 ? 's' : ''}?`)) {
console.log(`Deleting ${selectedItems.length} items`);
// Implement deletion logic
}
}
}
}
],
menuCommands: [
{
path: ["Edit"],
commands: ["selectAll", "deselectAll", "duplicateSelected", "deleteSelected"]
}
]
});
Extension Management Menu
app.registerExtension({
name: "ExtensionManager",
commands: [
{
id: "reloadExtensions",
label: "Reload Extensions",
function: () => {
app.extensionManager.reload();
app.extensionManager.toast.add({
severity: "info",
summary: "Extensions Reloaded",
detail: "All extensions have been reloaded",
life: 3000
});
}
},
{
id: "showExtensionInfo",
label: "Show Extension Info",
function: () => {
const extensions = app.extensionManager.getExtensions();
console.log("Loaded extensions:", extensions);
app.extensionManager.toast.add({
severity: "info",
summary: "Extension Info",
detail: `${extensions.length} extensions loaded`,
life: 3000
});
}
},
{
id: "toggleExtension",
label: "Toggle Extension",
function: () => {
const extensionName = "MyExtension";
const isEnabled = app.extensionManager.isEnabled(extensionName);
if (isEnabled) {
app.extensionManager.disable(extensionName);
app.extensionManager.toast.add({
severity: "info",
summary: "Extension Disabled",
detail: `${extensionName} has been disabled`,
life: 3000
});
} else {
app.extensionManager.enable(extensionName);
app.extensionManager.toast.add({
severity: "success",
summary: "Extension Enabled",
detail: `${extensionName} has been enabled`,
life: 3000
});
}
}
}
],
menuCommands: [
{
path: ["Extensions"],
commands: ["reloadExtensions", "showExtensionInfo", "toggleExtension"]
}
]
});
Canvas Navigation Menu
app.registerExtension({
name: "CanvasNavigation",
commands: [
{
id: "zoomIn",
label: "Zoom In",
function: () => {
app.canvas.zoomTo(1.2);
}
},
{
id: "zoomOut",
label: "Zoom Out",
function: () => {
app.canvas.zoomTo(0.8);
}
},
{
id: "zoomFit",
label: "Zoom to Fit",
function: () => {
app.canvas.zoomToFit();
}
},
{
id: "resetView",
label: "Reset View",
function: () => {
app.canvas.zoomTo(1.0);
app.canvas.offset = [0, 0];
}
}
],
menuCommands: [
{
path: ["View"],
commands: ["zoomIn", "zoomOut", "zoomFit", "resetView"]
}
]
});
Best Practices
Menu Organization
// Good: Logical menu organization
menuCommands: [
{
path: ["File"],
commands: ["saveWorkflow", "loadWorkflow"]
},
{
path: ["Edit"],
commands: ["selectAll", "deselectAll"]
},
{
path: ["View"],
commands: ["zoomIn", "zoomOut"]
}
]
// Bad: Unclear menu organization
menuCommands: [
{
path: ["File"],
commands: ["zoomIn", "selectAll", "saveWorkflow"]
}
]
Command Naming
// Good: Clear, descriptive command names
{
id: "save-workflow-as-json",
label: "Save Workflow as JSON",
function: () => { /* ... */ }
}
// Bad: Vague, unclear command names
{
id: "save1",
label: "Save",
function: () => { /* ... */ }
}
Error Handling
{
id: "safeCommand",
label: "Safe Command",
function: () => {
try {
// Perform potentially dangerous operation
app.graph.clear();
app.extensionManager.toast.add({
severity: "success",
summary: "Success",
detail: "Operation completed successfully",
life: 3000
});
} catch (error) {
console.error("Command failed:", error);
app.extensionManager.toast.add({
severity: "error",
summary: "Error",
detail: "Operation failed: " + error.message,
life: 5000
});
}
}
}
User Confirmation
{
id: "destructiveCommand",
label: "Destructive Command",
function: () => {
if (confirm("This action cannot be undone. Continue?")) {
// Perform destructive operation
console.log("Destructive operation executed");
}
}
}
Integration with Other APIs
Settings Integration
app.registerExtension({
name: "SettingsIntegration",
commands: [
{
id: "openSettings",
label: "Open Settings",
function: () => {
// Open settings panel
app.settings.show();
}
},
{
id: "resetSettings",
label: "Reset Settings",
function: () => {
if (confirm("Reset all settings to default?")) {
app.settings.reset();
}
}
}
],
menuCommands: [
{
path: ["Settings"],
commands: ["openSettings", "resetSettings"]
}
]
});
Dialog Integration
app.registerExtension({
name: "DialogIntegration",
commands: [
{
id: "showDialog",
label: "Show Dialog",
function: () => {
app.dialog.prompt({
title: "Custom Dialog",
message: "Enter your input:",
onConfirm: (value) => {
console.log("User input:", value);
}
});
}
}
],
menuCommands: [
{
path: ["Tools"],
commands: ["showDialog"]
}
]
});
Important Notes
Menu Hierarchy
// Menu paths are hierarchical
// ["File"] -> File menu
// ["File", "Export"] -> Export submenu under File
// ["Extensions", "My Tools"] -> My Tools submenu under Extensions
Command Reuse
// Commands can be used in multiple menu locations
menuCommands: [
{
path: ["File"],
commands: ["saveWorkflow"]
},
{
path: ["Edit"],
commands: ["saveWorkflow"]
}
]
Menu Ordering
// Menu items appear in the order they are defined
// Use consistent ordering for better user experience
}
ComfyUI Annotated Examples - UI Interactions
Purpose
ComfyUI's Annotated Examples provide practical code fragments for common UI interactions, event handling, and menu customization. This collection demonstrates how to extend ComfyUI's interface with custom functionality.
Right Click Menus
Background Menu
The main background menu (right-click on the canvas) is generated by a call to LGraph.getCanvasMenuOptions. One way to add your own menu options is to hijack this call:
/* in setup() */
const original_getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions;
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
// get the basic options
const options = original_getCanvasMenuOptions.apply(this, arguments);
options.push(null); // inserts a divider
options.push({
content: "The text for the menu",
callback: async () => {
// do whatever
}
})
return options;
}
Features:
- Menu hijacking: Override default canvas menu options
- Divider support: Add visual separators with
null - Custom callbacks: Execute custom functions on menu selection
- Canvas integration: Seamless integration with canvas context
Node Menu
When you right click on a node, the menu is similarly generated by node.getExtraMenuOptions. But instead of returning an options object, this one gets it passed in:
/* in beforeRegisterNodeDef() */
if (nodeType?.comfyClass=="MyNodeClass") {
const original_getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function(_, options) {
original_getExtraMenuOptions?.apply(this, arguments);
options.push({
content: "Do something fun",
callback: async () => {
// fun thing
}
})
}
}
Features:
- Node-specific menus: Custom menus for specific node types
- Menu extension: Add options to existing node menus
- Node context: Access to node instance in callbacks
- Type safety: Check node type before adding menu options
Submenus
If you want a submenu, provide a callback which uses LiteGraph.ContextMenu to create it:
function make_submenu(value, options, e, menu, node) {
const submenu = new LiteGraph.ContextMenu(
["option 1", "option 2", "option 3"],
{
event: e,
callback: function (v) {
// do something with v (=="option x")
},
parentMenu: menu,
node:node
}
)
}
/* ... */
options.push(
{
content: "Menu with options",
has_submenu: true,
callback: make_submenu,
}
)
Features:
- Nested menus: Create submenu structures
- Context preservation: Maintain node and event context
- Dynamic options: Generate menu options dynamically
- Parent menu integration: Seamless submenu integration
Capture UI Events
Button Click Detection
This works just like you'd expect - find the UI element in the DOM and add an eventListener. setup() is a good place to do this, since the page has fully loaded:
function queue_button_pressed() {
console.log("Queue button was pressed!")
}
document.getElementById("queue-button").addEventListener("click", queue_button_pressed);
Features:
- DOM event handling: Standard DOM event listeners
- Button detection: Capture button clicks
- Setup timing: Use
setup()for proper initialization - Event context: Access to event object and target
Workflow Events
This is one of many API events:
import { api } from "../../scripts/api.js";
/* in setup() */
function on_execution_start() {
/* do whatever */
}
api.addEventListener("execution_start", on_execution_start);
Available Events:
- execution_start: Workflow execution begins
- execution_cached: Workflow execution cached
- execution_success: Workflow execution successful
- execution_error: Workflow execution failed
- execution_interrupted: Workflow execution interrupted
API Hijacking
A simple example of hijacking the API:
import { api } from "../../scripts/api.js";
/* in setup() */
const original_api_interrupt = api.interrupt;
api.interrupt = function () {
/* Do something before the original method is called */
original_api_interrupt.apply(this, arguments);
/* Or after */
}
Features:
- Method hijacking: Override API methods
- Before/after execution: Execute code before or after original method
- Argument preservation: Maintain original method arguments
- Context preservation: Maintain
thiscontext
Node Click Detection
Node has a mouseDown method you can hijack. This time we're careful to pass on any return value:
async nodeCreated(node) {
if (node?.comfyClass === "My Node Name") {
const original_onMouseDown = node.onMouseDown;
node.onMouseDown = function( e, pos, canvas ) {
alert("ouch!");
return original_onMouseDown?.apply(this, arguments);
}
}
}
Features:
- Node-specific events: Custom behavior for specific node types
- Mouse interaction: Capture mouse events on nodes
- Return value preservation: Maintain original method return values
- Event context: Access to event, position, and canvas
Advanced Examples
Custom Canvas Menu
app.registerExtension({
name: "CustomCanvasMenu",
setup() {
const original_getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions;
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
const options = original_getCanvasMenuOptions.apply(this, arguments);
// Add divider
options.push(null);
// Add custom options
options.push({
content: "Save Canvas as Image",
callback: async () => {
const canvas = this.canvas;
const dataURL = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = 'canvas.png';
link.href = dataURL;
link.click();
}
});
options.push({
content: "Clear Canvas",
callback: async () => {
if (confirm("Clear the canvas?")) {
this.graph.clear();
}
}
});
return options;
};
}
});
Custom Node Menu
app.registerExtension({
name: "CustomNodeMenu",
beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeType?.comfyClass === "MyCustomNode") {
const original_getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function(_, options) {
original_getExtraMenuOptions?.apply(this, arguments);
options.push({
content: "Duplicate Node",
callback: async () => {
const newNode = this.clone();
newNode.pos = [this.pos[0] + 50, this.pos[1] + 50];
this.graph.add(newNode);
}
});
options.push({
content: "Node Info",
callback: async () => {
alert(`Node: ${this.title}\nType: ${this.type}\nPosition: ${this.pos}`);
}
});
};
}
}
});
Submenu Example
app.registerExtension({
name: "SubmenuExample",
setup() {
const original_getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions;
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
const options = original_getCanvasMenuOptions.apply(this, arguments);
function createExportSubmenu(value, options, e, menu, node) {
const submenu = new LiteGraph.ContextMenu(
["Export as JSON", "Export as PNG", "Export as SVG"],
{
event: e,
callback: function (v) {
switch(v) {
case "Export as JSON":
// Export workflow as JSON
app.graphToPrompt().then(workflow => {
const blob = new Blob([JSON.stringify(workflow)], {type: "application/json"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "workflow.json";
a.click();
URL.revokeObjectURL(url);
});
break;
case "Export as PNG":
// Export canvas as PNG
const canvas = this.canvas;
const dataURL = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = 'workflow.png';
link.href = dataURL;
link.click();
break;
case "Export as SVG":
// Export as SVG (placeholder)
alert("SVG export not implemented yet");
break;
}
},
parentMenu: menu,
node: node
}
);
}
options.push({
content: "Export",
has_submenu: true,
callback: createExportSubmenu,
});
return options;
};
}
});
Event Detection Examples
app.registerExtension({
name: "EventDetection",
setup() {
// Detect queue button clicks
const queueButton = document.getElementById("queue-button");
if (queueButton) {
queueButton.addEventListener("click", () => {
console.log("Queue button clicked!");
});
}
// Detect workflow events
import { api } from "../../scripts/api.js";
api.addEventListener("execution_start", () => {
console.log("Workflow execution started");
});
api.addEventListener("execution_success", () => {
console.log("Workflow execution completed successfully");
});
api.addEventListener("execution_error", (error) => {
console.log("Workflow execution failed:", error);
});
// Hijack API methods
const original_interrupt = api.interrupt;
api.interrupt = function () {
console.log("Workflow interrupted by user");
return original_interrupt.apply(this, arguments);
};
}
});
Node Event Handling
app.registerExtension({
name: "NodeEventHandling",
nodeCreated(node) {
if (node?.comfyClass === "MyCustomNode") {
// Hijack mouse down
const original_onMouseDown = node.onMouseDown;
node.onMouseDown = function(e, pos, canvas) {
console.log("Node clicked:", this.title);
return original_onMouseDown?.apply(this, arguments);
};
// Hijack mouse up
const original_onMouseUp = node.onMouseUp;
node.onMouseUp = function(e, pos, canvas) {
console.log("Node released:", this.title);
return original_onMouseUp?.apply(this, arguments);
};
// Hijack mouse move
const original_onMouseMove = node.onMouseMove;
node.onMouseMove = function(e, pos, canvas) {
// Only log occasionally to avoid spam
if (Math.random() < 0.01) {
console.log("Node moved:", this.title);
}
return original_onMouseMove?.apply(this, arguments);
};
}
}
});
Best Practices
Menu Organization
// Good: Clear menu structure with dividers
options.push(null); // Divider
options.push({
content: "Clear Canvas",
callback: async () => { /* ... */ }
});
// Bad: No organization
options.push({
content: "Clear Canvas",
callback: async () => { /* ... */ }
});
Event Handling
// Good: Proper event listener setup
function setupEventListeners() {
const button = document.getElementById("my-button");
if (button) {
button.addEventListener("click", handleClick);
}
}
// Bad: No null checking
document.getElementById("my-button").addEventListener("click", handleClick);
Method Hijacking
// Good: Preserve original method behavior
const original_method = obj.method;
obj.method = function() {
// Do something before
const result = original_method.apply(this, arguments);
// Do something after
return result;
};
// Bad: Don't preserve original behavior
obj.method = function() {
// Only custom behavior, original method lost
};
Error Handling
// Good: Safe event handling
function safeEventHandler() {
try {
// Potentially dangerous operation
app.graph.clear();
} catch (error) {
console.error("Error in event handler:", error);
}
}
// Bad: No error handling
function unsafeEventHandler() {
app.graph.clear(); // Could throw error
}
Important Notes
Timing Considerations
// setup() is called after page load
app.registerExtension({
name: "MyExtension",
setup() {
// Safe to access DOM elements here
const button = document.getElementById("my-button");
if (button) {
button.addEventListener("click", handleClick);
}
}
});
Method Preservation
// Always preserve original method behavior
const original_method = obj.method;
obj.method = function() {
// Custom behavior
return original_method.apply(this, arguments);
};
Event Context
// Access event context in callbacks
function handleClick(event) {
console.log("Clicked element:", event.target);
console.log("Event type:", event.type);
console.log("Mouse position:", event.clientX, event.clientY);
}
ComfyUI i18n Support - Multi-Language Custom Nodes
Purpose
ComfyUI supports internationalization (i18n) for custom nodes, allowing developers to provide multi-language support for their extensions. This enables users to interact with nodes in their preferred language.
Supported Languages
ComfyUI currently supports the following languages:
- English (en) - Default/base language
- Chinese (Simplified) (zh) - 简体中文
- Chinese (Traditional) (zh-TW) - 繁體中文
- French (fr) - Français
- Korean (ko) - 한국어
- Russian (ru) - Русский
- Spanish (es) - Español
- Japanese (ja) - 日本語
- Arabic (ar) - العربية
Directory Structure
Create a locales folder under your custom node with the following structure:
your_custom_node/
├── __init__.py
├── your_node.py
└── locales/ # i18n support folder
├── en/ # English translations (recommended as base)
│ ├── main.json # General English translation content
│ ├── nodeDefs.json # English node definition translations
│ ├── settings.json # Optional: settings interface translations
│ └── commands.json # Optional: command translations
├── zh/ # Chinese translation files
│ ├── nodeDefs.json # Chinese node definition translations
│ ├── main.json # General Chinese translation content
│ ├── settings.json # Chinese settings interface translations
│ └── commands.json # Chinese command translations
├── fr/ # French translation files
│ ├── nodeDefs.json
│ ├── main.json
│ └── settings.json
└── ... # Other language folders
Node Definition Translation (nodeDefs.json)
Example Node Class
class I18nTextProcessor:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {
"multiline": True,
"default": "Hello World!",
"tooltip": "The original text content to be processed"
}),
"operation": (["uppercase", "lowercase", "reverse", "add_prefix"], {
"default": "uppercase",
"tooltip": "The text processing operation to be executed"
}),
"count": ("INT", {
"default": 1,
"min": 1,
"max": 10,
"step": 1,
"tooltip": "The number of times to repeat the operation"
}),
},
"optional": {
"prefix": ("STRING", {
"default": "[I18N] ",
"multiline": False,
"tooltip": "The prefix to add to the text"
}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("processed_text",)
FUNCTION = "process_text"
CATEGORY = "I18n Demo"
DESCRIPTION = "A simple i18n demo node that demonstrates text processing with internationalization support"
def process_text(self, text, operation, count, prefix=""):
try:
result = text
for _ in range(count):
if operation == "uppercase":
result = result.upper()
elif operation == "lowercase":
result = result.lower()
elif operation == "reverse":
result = result[::-1]
elif operation == "add_prefix":
result = prefix + result
return (result,)
except Exception as e:
print(f"I18nTextProcessor error: {e}")
return (f"Error: {str(e)}",)
Corresponding nodeDefs.json
{
"I18nTextProcessor": {
"display_name": "I18n Text Processor",
"description": "A simple i18n demo node that demonstrates text processing with internationalization support",
"inputs": {
"text": {
"name": "Text Input",
"tooltip": "The original text content to be processed"
},
"operation": {
"name": "Operation Type",
"tooltip": "The text processing operation to be executed",
"options": {
"uppercase": "To Uppercase",
"lowercase": "To Lowercase",
"reverse": "Reverse Text",
"add_prefix": "Add Prefix"
}
},
"count": {
"name": "Repeat Count",
"tooltip": "The number of times to repeat the operation"
},
"prefix": {
"name": "Prefix Text",
"tooltip": "The prefix to add to the text"
}
},
"outputs": {
"0": {
"name": "Processed Text",
"tooltip": "The final processed text result"
}
}
}
}
Key Points:
- Output indexing: Use numeric indices (0, 1, 2...) for outputs instead of names
- Option translation: Translate dropdown options in the
optionsobject - Tooltip translation: Translate all tooltip text for better user experience
Menu Settings Translation
Extension Registration
app.registerExtension({
name: "I18nDemo",
settings: [
{
id: "I18nDemo.EnableDebugMode",
category: ["I18nDemo","DebugMode"], // Matches settingsCategories key in main.json
name: "Enable Debug Mode", // Will be overridden by translation
tooltip: "Show debug information in console for i18n demo nodes", // Will be overridden by translation
type: "boolean",
defaultValue: false,
experimental: true,
onChange: (value) => {
console.log("I18n Demo:", value ? "Debug mode enabled" : "Debug mode disabled");
}
},
{
id: "I18nDemo.DefaultTextOperation",
category: ["I18nDemo","DefaultTextOperation"], // Matches settingsCategories key in main.json
name: "Default Text Operation", // Will be overridden by translation
tooltip: "Default operation for text processor node", // Will be overridden by translation
type: "combo",
options: ["uppercase", "lowercase", "reverse", "add_prefix"],
defaultValue: "uppercase",
experimental: true
}
],
})
Settings Categories (main.json)
{
"settingsCategories": {
"I18nDemo": "I18n Demo",
"DebugMode": "Debug Mode",
"DefaultTextOperation": "Default Text Operation"
}
}
Settings Translation (settings.json)
{
"I18nDemo_EnableDebugMode": {
"name": "Enable Debug Mode",
"tooltip": "Show debug information in console for i18n demo nodes"
},
"I18nDemo_DefaultTextOperation": {
"name": "Default Text Operation",
"tooltip": "Default operation for text processor node",
"options": {
"uppercase": "Uppercase",
"lowercase": "Lowercase",
"reverse": "Reverse",
"add_prefix": "Add Prefix"
}
}
}
Translation Key Rules:
- Replace dots (.) with underscores (_) in setting IDs
- Example:
"I18nDemo.EnableDebugMode"becomes"I18nDemo_EnableDebugMode"
Translation File Examples
English (en/main.json)
{
"settingsCategories": {
"I18nDemo": "I18n Demo",
"DebugMode": "Debug Mode",
"DefaultTextOperation": "Default Text Operation"
},
"general": {
"welcome": "Welcome to I18n Demo",
"description": "This extension demonstrates internationalization support"
}
}
Chinese (zh/main.json)
{
"settingsCategories": {
"I18nDemo": "国际化演示",
"DebugMode": "调试模式",
"DefaultTextOperation": "默认文本操作"
},
"general": {
"welcome": "欢迎使用国际化演示",
"description": "此扩展演示了国际化支持功能"
}
}
French (fr/main.json)
{
"settingsCategories": {
"I18nDemo": "Démo I18n",
"DebugMode": "Mode Debug",
"DefaultTextOperation": "Opération de Texte par Défaut"
},
"general": {
"welcome": "Bienvenue dans la démo I18n",
"description": "Cette extension démontre le support de l'internationalisation"
}
}
Best Practices
Translation Key Naming
// Good: Clear, hierarchical naming
{
"I18nDemo_EnableDebugMode": {
"name": "Enable Debug Mode",
"tooltip": "Show debug information in console"
}
}
// Bad: Unclear naming
{
"setting1": {
"name": "Setting 1",
"tooltip": "Some setting"
}
}
Consistent Translation
// Good: Consistent terminology
{
"uppercase": "Uppercase",
"lowercase": "Lowercase",
"reverse": "Reverse"
}
// Bad: Inconsistent terminology
{
"uppercase": "UPPERCASE",
"lowercase": "lower case",
"reverse": "REVERSE"
}
Complete Translation Coverage
// Good: All text translated
{
"I18nTextProcessor": {
"display_name": "I18n Text Processor",
"description": "A simple i18n demo node...",
"inputs": {
"text": {
"name": "Text Input",
"tooltip": "The original text content..."
}
}
}
}
// Bad: Missing translations
{
"I18nTextProcessor": {
"display_name": "I18n Text Processor",
"description": "A simple i18n demo node...",
"inputs": {
"text": {
"name": "Text Input"
// Missing tooltip translation
}
}
}
}
Advanced Features
Dynamic Translation Loading
// Load translations dynamically
function loadTranslations(language) {
const translations = {};
try {
const response = fetch(`/extensions/I18nDemo/locales/${language}/main.json`);
const data = response.json();
Object.assign(translations, data);
} catch (error) {
console.warn(`Failed to load ${language} translations:`, error);
}
return translations;
}
Fallback Translation
// Implement fallback to English if translation missing
function getTranslation(key, language = 'en') {
const translations = loadTranslations(language);
if (translations[key]) {
return translations[key];
}
// Fallback to English
if (language !== 'en') {
const englishTranslations = loadTranslations('en');
return englishTranslations[key] || key;
}
return key;
}
Context-Aware Translation
// Translation with context
function translateWithContext(key, context = {}) {
const translation = getTranslation(key);
// Replace placeholders
return translation.replace(/\{(\w+)\}/g, (match, placeholder) => {
return context[placeholder] || match;
});
}
// Usage
const message = translateWithContext('welcome_user', {
username: 'John',
language: 'English'
});
Important Notes
File Encoding
// Always use UTF-8 encoding for translation files
// This ensures proper display of special characters
{
"chinese_text": "你好世界",
"french_text": "Bonjour le monde",
"arabic_text": "مرحبا بالعالم"
}
Translation Validation
// Validate translation completeness
function validateTranslations() {
const languages = ['en', 'zh', 'fr', 'es', 'ja'];
const baseKeys = Object.keys(loadTranslations('en'));
languages.forEach(lang => {
const translations = loadTranslations(lang);
const missingKeys = baseKeys.filter(key => !translations[key]);
if (missingKeys.length > 0) {
console.warn(`Missing translations in ${lang}:`, missingKeys);
}
});
}
Performance Considerations
// Cache translations for better performance
const translationCache = new Map();
function getCachedTranslation(key, language) {
const cacheKey = `${language}:${key}`;
if (translationCache.has(cacheKey)) {
return translationCache.get(cacheKey);
}
const translation = getTranslation(key, language);
translationCache.set(cacheKey, translation);
return translation;
}
ComfyUI V3 Migration - Modern Node Development
Purpose
ComfyUI V3 schema introduces a more organized way of defining nodes with better type safety, cleaner architecture, and future-proofing. This guide helps migrate existing V1 nodes to the new V3 schema.
Core Concepts
Versioned API
# Use latest ComfyUI API
from comfy_api.latest import ComfyExtension, io, ui
# Use a specific version of ComfyUI API
from comfy_api.v0_0_2 import ComfyExtension, io, ui
Features:
- Versioned API: Future revisions are backwards compatible
- Latest API:
comfy_api.latestpoints to the latest numbered API - Stable API: Previous version is considered stable
- Development API: Current version allows changes without warning
V1 vs V3 Architecture
Key Changes
- Inputs/Outputs: Defined by objects instead of dictionaries
- Execution method: Fixed to
executeand is a class method - Entry point:
comfy_entrypoint()function returnsComfyExtensionobject - Node state: No
__init__method needed, all methods are class methods - Node sanitization: Node class is sanitized before execution
V1 (Legacy)
class MyNode:
@classmethod
def INPUT_TYPES(s):
return {"required": {...}}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "execute"
CATEGORY = "my_category"
def execute(self, ...):
return (result,)
NODE_CLASS_MAPPINGS = {"MyNode": MyNode}
V3 (Modern)
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()
Migration Steps
Step 1: Change Base Class
# V1
class Example:
def __init__(self):
pass
# V3
from comfy_api.latest import io
class Example(io.ComfyNode):
# No __init__ needed
Step 2: Convert INPUT_TYPES to define_schema
# V1
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"int_field": ("INT", {
"default": 0,
"min": 0,
"max": 4096,
"step": 64,
"display": "number"
}),
"string_field": ("STRING", {
"multiline": False,
"default": "Hello"
}),
"custom_field": ("MY_CUSTOM_TYPE",),
},
"optional": {
"mask": ("MASK",)
}
}
# V3
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="Example",
display_name="Example Node",
category="examples",
description="Node description here",
inputs=[
io.Image.Input("image"),
io.Int.Input("int_field",
default=0,
min=0,
max=4096,
step=64,
display_mode=io.NumberDisplay.number
),
io.String.Input("string_field",
default="Hello",
multiline=False
),
io.Custom("my_custom_type").Input("custom_input"),
io.Mask.Input("mask", optional=True)
],
outputs=[
io.Image.Output()
]
)
Step 3: Update Execute Method
# V1
def test(self, image, string_field, int_field):
# Process
image = 1.0 - image
return (image,)
# V3
@classmethod
def execute(cls, image, string_field, int_field) -> io.NodeOutput:
# Process
image = 1.0 - image
# Return with optional UI preview
return io.NodeOutput(image, ui=ui.PreviewImage(image, cls=cls))
Step 4: Convert Node Properties
| V1 Property | V3 Schema Field | Notes |
|---|---|---|
| RETURN_TYPES | outputs in Schema | List of Output objects |
| RETURN_NAMES | display_name in Output | Per-output display names |
| FUNCTION | Always execute | Method name is standardized |
| CATEGORY | category in Schema | String value |
| OUTPUT_NODE | is_output_node in Schema | Boolean flag |
| DEPRECATED | is_deprecated in Schema | Boolean flag |
| EXPERIMENTAL | is_experimental in Schema | Boolean flag |
Step 5: Handle Special Methods
Validation
# V1
@classmethod
def VALIDATE_INPUTS(s, **kwargs):
# Validation logic
return True
# V3
@classmethod
def validate_inputs(cls, **kwargs) -> bool | str:
# Return True if valid, error string if not
if error_condition:
return "Error message"
return True
Lazy Evaluation
# V1
def check_lazy_status(self, image, string_field, ...):
if condition:
return ["string_field"]
return []
# V3
@classmethod
def check_lazy_status(cls, image, string_field, ...):
if condition:
return ["string_field"]
return []
Cache Control
# V1
@classmethod
def IS_CHANGED(s, **kwargs):
return "unique_value"
# V3
@classmethod
def fingerprint_inputs(cls, **kwargs):
return "unique_value"
Step 6: Create Extension and Entry Point
# V1
NODE_CLASS_MAPPINGS = {
"Example": Example
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Example": "Example Node"
}
# V3
from comfy_api.latest import ComfyExtension
class MyExtension(ComfyExtension):
# must be declared as async
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [
Example,
# Add more nodes here
]
# can be declared async or not, both will work
async def comfy_entrypoint() -> MyExtension:
return MyExtension()
Input Type Reference
Basic Types
| V1 Type | V3 Type | Example |
|---|---|---|
| "INT" | io.Int.Input() | io.Int.Input("count", default=1, min=0, max=100) |
| "FLOAT" | io.Float.Input() | io.Float.Input("strength", default=1.0, min=0.0, max=10.0) |
| "STRING" | io.String.Input() | io.String.Input("text", multiline=True) |
| "BOOLEAN" | io.Boolean.Input() | io.Boolean.Input("enabled", default=True) |
ComfyUI Types
| V1 Type | V3 Type | Example |
|---|---|---|
| "IMAGE" | io.Image.Input() | io.Image.Input("image", tooltip="Input image") |
| "MASK" | io.Mask.Input() | io.Mask.Input("mask", optional=True) |
| "LATENT" | io.Latent.Input() | io.Latent.Input("latent") |
| "CONDITIONING" | io.Conditioning.Input() | io.Conditioning.Input("positive") |
| "MODEL" | io.Model.Input() | io.Model.Input("model") |
| "VAE" | io.VAE.Input() | io.VAE.Input("vae") |
| "CLIP" | io.CLIP.Input() | io.CLIP.Input("clip") |
Combo (Dropdowns)
# V1
"mode": (["option1", "option2", "option3"],)
# V3
io.Combo.Input("mode", options=["option1", "option2", "option3"])
Advanced Features
UI Integration
from comfy_api.latest import ui
@classmethod
def execute(cls, images) -> io.NodeOutput:
# Show preview in node
return io.NodeOutput(images, ui=ui.PreviewImage(images, cls=cls))
Output Nodes
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="SaveNode",
inputs=[...],
outputs=[], # Does not need to be empty
is_output_node=True # Mark as output node
)
Custom Types
from comfy_api.latest import io
# Method 1: Using decorator with class
@io.comfytype(io_type="MY_CUSTOM_TYPE")
class MyCustomType:
Type = torch.Tensor # Python type hint
class Input(io.Input):
def __init__(self, id: str, **kwargs):
super().__init__(id, **kwargs)
class Output(io.Output):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Method 2: Using Custom helper
MyCustomType = io.Custom("MY_CUSTOM_TYPE")
Best Practices
Type Safety
# Good: Use proper type hints
@classmethod
def execute(cls, image: torch.Tensor, strength: float) -> io.NodeOutput:
return io.NodeOutput(processed_image)
# Bad: No type hints
@classmethod
def execute(cls, image, strength):
return io.NodeOutput(processed_image)
Error Handling
# Good: Proper error handling
@classmethod
def execute(cls, image, strength) -> io.NodeOutput:
try:
result = process_image(image, strength)
return io.NodeOutput(result)
except Exception as e:
return io.NodeOutput(f"Error: {str(e)}")
Documentation
# Good: Comprehensive documentation
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="ImageProcessor",
display_name="Image Processor",
category="image_processing",
description="Processes images with various filters and effects",
inputs=[
io.Image.Input("image", tooltip="Input image to process"),
io.Float.Input("strength", default=1.0, min=0.0, max=2.0,
tooltip="Processing strength multiplier")
],
outputs=[
io.Image.Output(tooltip="Processed image result")
]
)
ComfyUI Help Page - Rich Node Documentation
Purpose
ComfyUI's Help Page system allows custom nodes to include rich markdown documentation that will be displayed in the UI instead of generic node descriptions. This provides users with detailed information about node functionality, parameters, and usage examples.
Features
- Rich markdown documentation: Detailed node information in the UI
- Multi-language support: Localized documentation for different languages
- Automatic fallback: Falls back to default documentation if localized version unavailable
- Media support: Images and videos in documentation
- Tooltip integration: Basic parameter information from node definitions
Setup
Directory Structure
Create a docs folder inside your WEB_DIRECTORY with the following structure:
my-custom-node/
├── __init__.py
├── web/ # WEB_DIRECTORY
│ ├── js/
│ │ └── my-node.js
│ └── docs/
│ ├── MyNode.md # Fallback documentation
│ └── MyNode/
│ ├── en.md # English version
│ ├── zh.md # Chinese version
│ ├── fr.md # French version
│ └── de.md # German version
File Naming Convention
- Default documentation:
NodeName.md(fallback) - Localized documentation:
NodeName/locale.md(e.g.,en.md,zh.md,fr.md) - Node names: Use dictionary keys from
NODE_CLASS_MAPPINGS
Supported Markdown Features
Standard Markdown
# Heading 1
## Heading 2
### Heading 3
**Bold text**
*Italic text*
`Code snippets`
- Bullet points
- More items
1. Numbered lists
2. Second item
> Blockquotes for important information
[Links](https://example.com)
Images



Code Blocks
```python
# Python code example
def process_image(image, strength):
return image * strength
// JavaScript code example
function updateNode() {
console.log("Node updated");
}
#### HTML Media Elements
```markdown
<video controls loop muted>
<source src="demo.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
Supported video attributes:
controls: Show video controlsautoplay: Auto-play videoloop: Loop videomuted: Mute videopreload: Preload videoposter: Poster image
Documentation Examples
Basic Node Documentation
# Image Processor Node
This node processes images using advanced algorithms with customizable parameters.
## Overview
The Image Processor node applies various filters and effects to input images. It supports multiple processing modes and provides real-time preview of results.
## Parameters
### Required Parameters
- **image**: Input image to process
- Type: IMAGE
- Description: The source image that will be processed
- Tooltip: Select an image to apply processing effects
- **strength**: Processing strength
- Type: FLOAT
- Range: 0.0 - 1.0
- Default: 0.5
- Description: Controls the intensity of the processing effect
### Optional Parameters
- **mode**: Processing mode
- Type: COMBO
- Options: ["blur", "sharpen", "enhance", "filter"]
- Default: "enhance"
- Description: Select the type of processing to apply
## Usage Examples
### Basic Usage
1. Connect an image to the **image** input
2. Set the **strength** parameter (0.0 = no effect, 1.0 = full effect)
3. Choose a **mode** from the dropdown
4. The processed image will appear at the output
### Advanced Usage
For best results:
- Use high-resolution images for better quality
- Adjust strength gradually to find the optimal setting
- Combine with other nodes for complex workflows
## Tips and Tricks
- **Performance**: Lower strength values process faster
- **Quality**: Higher resolution images produce better results
- **Workflow**: Use this node early in your pipeline for best performance
## Troubleshooting
### Common Issues
**Node not processing**:
- Check that an image is connected to the input
- Verify the strength parameter is not zero
**Poor quality results**:
- Try adjusting the strength parameter
- Ensure input image is high resolution
- Check that the mode is appropriate for your image type
### Error Messages
- `"No image connected"`: Connect an image to the input
- `"Invalid strength value"`: Ensure strength is between 0.0 and 1.0
- `"Unsupported image format"`: Use supported image formats (PNG, JPG, etc.)
Advanced Documentation with Media
# Advanced Image Processor
A comprehensive image processing node with multiple algorithms and real-time preview.
## Features
- **Multiple algorithms**: Blur, sharpen, enhance, and custom filters
- **Real-time preview**: See results as you adjust parameters
- **Batch processing**: Process multiple images efficiently
- **Quality control**: Fine-tune processing parameters
## Algorithm Details
### Blur Algorithm
The blur algorithm uses Gaussian blur with configurable radius:
```python
def gaussian_blur(image, radius):
# Apply Gaussian blur with specified radius
return cv2.GaussianBlur(image, (radius, radius), 0)
Sharpen Algorithm
The sharpen algorithm enhances image details:
def sharpen_image(image, strength):
# Apply unsharp mask
kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]])
return cv2.filter2D(image, -1, kernel * strength)
Visual Examples
Performance Guidelines
| Image Size | Processing Time | Memory Usage |
|---|---|---|
| 512x512 | ~50ms | ~10MB |
| 1024x1024 | ~200ms | ~40MB |
| 2048x2048 | ~800ms | ~160MB |
API Reference
Input Parameters
| Parameter | Type | Range | Default | Description |
|---|---|---|---|---|
| image | IMAGE | - | - | Input image to process |
| strength | FLOAT | 0.0-1.0 | 0.5 | Processing intensity |
| mode | COMBO | - | "enhance" | Processing algorithm |
| quality | INT | 1-100 | 85 | Output quality |
Output Parameters
| Parameter | Type | Description |
|---|---|---|
| processed_image | IMAGE | Processed image result |
| metadata | STRING | Processing metadata |
Changelog
Version 1.2.0
- Added new sharpen algorithm
- Improved performance by 30%
- Fixed memory leak in batch processing
Version 1.1.0
- Added quality parameter
- Enhanced error handling
- Improved documentation
Version 1.0.0
- Initial release
- Basic blur and enhance algorithms
- Real-time preview
#### Multi-Language Documentation
##### English (en.md)
```markdown
# Image Processor Node
This node processes images using advanced algorithms.
## Parameters
- **image**: Input image to process
- **strength**: Processing strength (0.0 - 1.0)
## Usage

<video controls loop muted>
<source src="demo.mp4" type="video/mp4">
</video>
Chinese (zh.md)
# 图像处理器节点
此节点使用高级算法处理图像。
## 参数
- **image**: 要处理的输入图像
- **strength**: 处理强度 (0.0 - 1.0)
## 使用方法

<video controls loop muted>
<source src="demo.mp4" type="video/mp4">
</video>
French (fr.md)
# Nœud Processeur d'Image
Ce nœud traite les images en utilisant des algorithmes avancés.
## Paramètres
- **image**: Image d'entrée à traiter
- **strength**: Intensité de traitement (0.0 - 1.0)
## Utilisation

<video controls loop muted>
<source src="demo.mp4" type="video/mp4">
</video>
Best Practices
Documentation Structure
# Node Name
Brief description of what the node does.
## Overview
Detailed explanation of functionality and use cases.
## Parameters
### Required Parameters
- **param1**: Description with type and range
- **param2**: Description with type and range
### Optional Parameters
- **param3**: Description with type and range
- **param4**: Description with type and range
## Usage Examples
### Basic Usage
Step-by-step instructions for common use cases.
### Advanced Usage
Complex scenarios and advanced techniques.
## Tips and Tricks
- Performance optimization tips
- Quality improvement suggestions
- Workflow integration advice
## Troubleshooting
### Common Issues
- Problem descriptions and solutions
- Error message explanations
- Performance considerations
## API Reference
### Input Parameters
Table with all input parameters, types, ranges, and descriptions.
### Output Parameters
Table with all output parameters and descriptions.
## Changelog
### Version X.X.X
- Feature additions
- Bug fixes
- Performance improvements
Content Guidelines
# Good Documentation Practices
## Use Clear Headings
- Use descriptive headings that explain the content
- Maintain consistent heading hierarchy
- Use proper markdown syntax
## Provide Examples
- Include code examples for complex operations
- Show before/after comparisons
- Provide step-by-step tutorials
## Use Visual Aids
- Include screenshots for UI elements
- Add diagrams for complex workflows
- Use videos for dynamic demonstrations
## Write for Your Audience
- Assume basic ComfyUI knowledge
- Explain technical terms
- Provide context for advanced features
## Keep It Updated
- Update documentation with new features
- Remove outdated information
- Test all examples and links
Media Guidelines
# Media Best Practices
## Images
- Use high-quality screenshots
- Optimize file sizes for web
- Include alt text for accessibility
- Use consistent naming conventions
## Videos
- Keep videos short and focused
- Use clear, readable text overlays
- Include audio narration when helpful
- Provide multiple formats when possible
## File Organization
docs/ ├── MyNode.md ├── MyNode/ │ ├── en.md │ ├── zh.md │ └── fr.md ├── images/ │ ├── example.png │ ├── before_after.jpg │ └── workflow_diagram.svg └── videos/ ├── demo.mp4 └── tutorial.mp4
Advanced Features
Dynamic Content
# Node with Dynamic Parameters
This node supports dynamic parameter generation based on input.
## Dynamic Parameters
The following parameters are generated dynamically:
- **algorithm_count**: Number of available algorithms
- **quality_presets**: Available quality presets
- **supported_formats**: Supported input formats
## Conditional Parameters
Some parameters only appear when specific conditions are met:
- **blur_radius**: Only visible when mode is "blur"
- **sharpen_strength**: Only visible when mode is "sharpen"
- **filter_type**: Only visible when mode is "filter"
Interactive Examples
# Interactive Node Documentation
## Live Examples
Try these examples with your own images:
### Example 1: Basic Processing
1. Load an image
2. Set strength to 0.5
3. Choose "enhance" mode
4. Observe the results
### Example 2: Advanced Workflow
1. Start with a high-resolution image
2. Apply multiple processing steps
3. Adjust parameters iteratively
4. Export final result
## Parameter Combinations
| Strength | Mode | Result |
|----------|------|--------|
| 0.1 | blur | Light blur effect |
| 0.5 | enhance | Moderate enhancement |
| 1.0 | sharpen | Strong sharpening |
Integration with Node Definitions
Tooltip Integration
class MyNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE", {
"tooltip": "Input image to process"
}),
"strength": ("FLOAT", {
"default": 0.5,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"tooltip": "Processing strength (0.0 = no effect, 1.0 = full effect)"
}),
"mode": (["blur", "sharpen", "enhance", "filter"], {
"default": "enhance",
"tooltip": "Processing algorithm to use"
})
}
}
Documentation Access
# The system automatically loads documentation based on:
# 1. User's locale (e.g., zh.md for Chinese users)
# 2. Fallback to default (MyNode.md)
# 3. Tooltip information from node definition
Important Notes
File Naming
# File Naming Rules
## Node Names
- Use exact names from NODE_CLASS_MAPPINGS
- Case-sensitive
- No spaces or special characters
## Locale Codes
- Use standard locale codes (en, zh, fr, de, etc.)
- Lowercase only
- No country codes (use zh instead of zh-CN)
## Media Files
- Use descriptive names
- Include version numbers for updates
- Use web-friendly formats (PNG, JPG, MP4)
Performance Considerations
# Performance Guidelines
## File Sizes
- Keep images under 1MB when possible
- Compress videos for web delivery
- Use appropriate formats for content type
## Loading Times
- Optimize images for web display
- Consider lazy loading for large media
- Provide fallbacks for slow connections
## Accessibility
- Include alt text for all images
- Provide captions for videos
- Use high contrast for text overlays
ComfyUI Workflow Templates - Example Workflows
Purpose
ComfyUI's Workflow Templates system allows custom node developers to provide example workflow files that users can discover and use through the template browser. This helps users get started with your nodes and demonstrates their capabilities.
Features
- Template browser integration: Workflows appear in Workflow/Browse Templates menu
- Thumbnail support: JPG images for visual preview
- Automatic categorization: Templates grouped by custom node module
- Static file serving: ComfyUI serves templates and provides API endpoint
- Multiple folder names: Flexible naming conventions supported
Setup
Directory Structure
Create an example_workflows folder in your custom node directory:
ComfyUI-MyCustomNodeModule/
├── __init__.py
├── my_node.py
├── example_workflows/ # Workflow templates folder
│ ├── My_example_workflow_1.json
│ ├── My_example_workflow_1.jpg # Thumbnail (optional)
│ ├── My_example_workflow_2.json
│ ├── My_example_workflow_2.jpg # Thumbnail (optional)
│ ├── Advanced_workflow.json
│ └── Advanced_workflow.jpg # Thumbnail (optional)
└── web/ # Optional: WEB_DIRECTORY
└── js/
└── my_node.js
Supported Folder Names
ComfyUI accepts multiple folder names for workflow templates:
example_workflows(recommended)workflowworkflowsexampleexamples
Workflow Template Files
JSON Workflow Files
Workflow templates are standard ComfyUI workflow JSON files:
{
"last_node_id": 10,
"last_link_id": 5,
"nodes": [
{
"id": 1,
"type": "LoadImage",
"pos": [100, 100],
"size": {"0": 315, "1": 314},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{"name": "IMAGE", "type": "IMAGE", "links": [1], "slot_index": 0}
],
"properties": {"Node name for S&R": "LoadImage"},
"widgets_values": ["example_image.png"]
},
{
"id": 2,
"type": "MyCustomNode",
"pos": [500, 100],
"size": {"0": 315, "1": 314},
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{"name": "image", "type": "IMAGE", "link": 1}
],
"outputs": [
{"name": "processed_image", "type": "IMAGE", "links": [2], "slot_index": 0}
],
"properties": {"Node name for S&R": "MyCustomNode"},
"widgets_values": [0.5, "enhance"]
},
{
"id": 3,
"type": "SaveImage",
"pos": [900, 100],
"size": {"0": 315, "1": 314},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{"name": "images", "type": "IMAGE", "link": 2}
],
"properties": {"Node name for S&R": "SaveImage"},
"widgets_values": ["processed_output"]
}
],
"links": [
[1, 1, 0, 2, 0, "IMAGE"],
[2, 2, 0, 3, 0, "IMAGE"]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
Thumbnail Images
Optional JPG images with the same name as the workflow file:
example_workflows/
├── My_example_workflow_1.json
├── My_example_workflow_1.jpg # 300x200px recommended
├── My_example_workflow_2.json
├── My_example_workflow_2.jpg # 300x200px recommended
└── Advanced_workflow.json
# No thumbnail - will show default icon
Template Browser Integration
User Experience
- Access: Users go to Workflow/Browse Templates menu
- Categories: Templates grouped by custom node module name
- Preview: Thumbnail images show workflow preview
- Loading: Clicking a template loads it into the current workflow
API Endpoint
ComfyUI provides an API endpoint for workflow templates:
- Endpoint:
/api/workflow_templates - Method: GET
- Response: Collection of workflow templates with metadata
Best Practices
Workflow Design
{
"workflow_design_guidelines": {
"node_positioning": "Arrange nodes in logical flow (left to right)",
"node_spacing": "Use consistent spacing between nodes",
"grouping": "Group related nodes together",
"naming": "Use descriptive node names and titles"
}
}
Template Organization
example_workflows/
├── 01_Basic_Usage.json # Simple, beginner-friendly
├── 01_Basic_Usage.jpg
├── 02_Advanced_Features.json # Complex, feature-rich
├── 02_Advanced_Features.jpg
├── 03_Batch_Processing.json # Batch operations
├── 03_Batch_Processing.jpg
├── 04_Integration_Example.json # Integration with other nodes
└── 04_Integration_Example.jpg
Thumbnail Guidelines
# Thumbnail Best Practices
## Image Specifications
- **Format**: JPG (recommended) or PNG
- **Size**: 300x200 pixels (3:2 aspect ratio)
- **Quality**: High quality, clear and readable
- **File size**: Under 100KB for fast loading
## Content Guidelines
- **Show workflow overview**: Capture the entire workflow
- **Highlight key nodes**: Make important nodes visible
- **Use clear labels**: Ensure node names are readable
- **Consistent styling**: Use similar styling across thumbnails
## Design Tips
- **High contrast**: Ensure good visibility
- **Clean layout**: Avoid cluttered appearance
- **Focus on flow**: Show the logical progression
- **Brand consistency**: Use consistent colors and fonts
Advanced Examples
Basic Usage Template
{
"template_info": {
"name": "Basic Image Processing",
"description": "Simple example showing basic image processing with MyCustomNode",
"difficulty": "beginner",
"estimated_time": "2 minutes",
"required_nodes": ["LoadImage", "MyCustomNode", "SaveImage"]
},
"workflow": {
"nodes": [
{
"id": 1,
"type": "LoadImage",
"pos": [100, 100],
"widgets_values": ["example_input.jpg"]
},
{
"id": 2,
"type": "MyCustomNode",
"pos": [500, 100],
"widgets_values": [0.5, "enhance"]
},
{
"id": 3,
"type": "SaveImage",
"pos": [900, 100],
"widgets_values": ["output"]
}
]
}
}
Advanced Features Template
{
"template_info": {
"name": "Advanced Image Processing Pipeline",
"description": "Complex workflow demonstrating advanced features and integration",
"difficulty": "advanced",
"estimated_time": "10 minutes",
"required_nodes": ["LoadImage", "MyCustomNode", "ControlNet", "Upscale", "SaveImage"]
},
"workflow": {
"nodes": [
{
"id": 1,
"type": "LoadImage",
"pos": [100, 100],
"widgets_values": ["input_image.jpg"]
},
{
"id": 2,
"type": "MyCustomNode",
"pos": [500, 100],
"widgets_values": [0.8, "sharpen"]
},
{
"id": 3,
"type": "ControlNet",
"pos": [500, 300],
"widgets_values": ["canny", 0.5]
},
{
"id": 4,
"type": "Upscale",
"pos": [900, 100],
"widgets_values": [2.0, "lanczos"]
},
{
"id": 5,
"type": "SaveImage",
"pos": [1300, 100],
"widgets_values": ["final_output"]
}
]
}
}
Batch Processing Template
{
"template_info": {
"name": "Batch Image Processing",
"description": "Process multiple images with consistent settings",
"difficulty": "intermediate",
"estimated_time": "5 minutes",
"required_nodes": ["LoadImageBatch", "MyCustomNode", "SaveImageBatch"]
},
"workflow": {
"nodes": [
{
"id": 1,
"type": "LoadImageBatch",
"pos": [100, 100],
"widgets_values": ["input_folder/", "*.jpg"]
},
{
"id": 2,
"type": "MyCustomNode",
"pos": [500, 100],
"widgets_values": [0.6, "enhance"]
},
{
"id": 3,
"type": "SaveImageBatch",
"pos": [900, 100],
"widgets_values": ["output_folder/", "processed_"]
}
]
}
}
Template Metadata
Workflow Information
{
"template_metadata": {
"title": "My Custom Node Example",
"description": "This workflow demonstrates the basic usage of MyCustomNode",
"author": "Your Name",
"version": "1.0.0",
"created_date": "2024-01-15",
"updated_date": "2024-01-20",
"tags": ["image-processing", "beginner", "example"],
"difficulty": "beginner",
"estimated_time": "2 minutes",
"required_nodes": ["LoadImage", "MyCustomNode", "SaveImage"],
"prerequisites": ["Basic ComfyUI knowledge", "Image files"],
"output_description": "Processed image with enhanced details"
}
}
Template Categories
{
"template_categories": {
"beginner": {
"description": "Simple workflows for getting started",
"examples": ["Basic Usage", "Simple Processing"]
},
"intermediate": {
"description": "Workflows with moderate complexity",
"examples": ["Batch Processing", "Multi-step Pipeline"]
},
"advanced": {
"description": "Complex workflows with multiple integrations",
"examples": ["Advanced Pipeline", "Custom Integration"]
}
}
}
Integration with Documentation
Template Documentation
# Workflow Templates
## Available Templates
### 1. Basic Usage
- **File**: `01_Basic_Usage.json`
- **Description**: Simple image processing workflow
- **Difficulty**: Beginner
- **Time**: 2 minutes
- **Nodes**: LoadImage → MyCustomNode → SaveImage
### 2. Advanced Features
- **File**: `02_Advanced_Features.json`
- **Description**: Complex workflow with multiple features
- **Difficulty**: Advanced
- **Time**: 10 minutes
- **Nodes**: LoadImage → MyCustomNode → ControlNet → Upscale → SaveImage
### 3. Batch Processing
- **File**: `03_Batch_Processing.json`
- **Description**: Process multiple images efficiently
- **Difficulty**: Intermediate
- **Time**: 5 minutes
- **Nodes**: LoadImageBatch → MyCustomNode → SaveImageBatch
Template README
# Workflow Templates
This directory contains example workflows for MyCustomNode.
## Template Files
- `01_Basic_Usage.json` - Simple workflow for beginners
- `02_Advanced_Features.json` - Complex workflow with advanced features
- `03_Batch_Processing.json` - Batch processing workflow
- `04_Integration_Example.json` - Integration with other nodes
## Thumbnail Images
- `01_Basic_Usage.jpg` - Preview of basic workflow
- `02_Advanced_Features.jpg` - Preview of advanced workflow
- `03_Batch_Processing.jpg` - Preview of batch workflow
- `04_Integration_Example.jpg` - Preview of integration workflow
## Usage
1. Open ComfyUI
2. Go to Workflow → Browse Templates
3. Find "MyCustomNode" category
4. Click on a template to load it
5. Adjust parameters as needed
6. Run the workflow
## Customization
- Modify node parameters to suit your needs
- Add or remove nodes as required
- Save your customized workflow
- Share with the community
Important Notes
File Naming
# File Naming Guidelines
## Workflow Files
- Use descriptive names: `Basic_Usage.json`, `Advanced_Features.json`
- Avoid spaces: Use underscores or hyphens
- Include version numbers for updates: `Basic_Usage_v2.json`
- Keep names short but descriptive
## Thumbnail Files
- Match workflow file names exactly
- Use JPG format for better compression
- Include descriptive alt text in filenames
- Use consistent naming conventions
Performance Considerations
# Performance Guidelines
## File Sizes
- Keep JSON files under 1MB
- Compress thumbnail images
- Use appropriate image formats
- Optimize for web delivery
## Loading Times
- Minimize workflow complexity for templates
- Use efficient node arrangements
- Avoid unnecessary connections
- Test loading performance
## User Experience
- Provide clear workflow descriptions
- Include helpful comments in workflows
- Use intuitive node names
- Organize templates by difficulty
ComfyUI Registry - Publishing Custom Nodes
Purpose
The ComfyUI Registry is a public collection of custom nodes that allows developers to publish, version, deprecate, and track metrics related to their custom nodes. Users can discover, install, and rate custom nodes from the registry, creating a standardized ecosystem for ComfyUI extensions.
Features
- Node versioning: Semantic versioning for reliable workflow reproduction
- Node security: Security scanning and verification for safe installations
- Search functionality: Discover existing nodes for workflows
- Community ratings: User feedback and quality metrics
- Workflow compatibility: Version tracking in workflow JSON files
- Standardized development: Community standards for node development
Why Use the Registry?
Node Versioning
{
"versioning_benefits": {
"semantic_versioning": "Major.Minor.Patch versioning system",
"workflow_reproduction": "Reliable recreation of workflows with specific node versions",
"safe_upgrades": "Users can choose when to upgrade node versions",
"version_locking": "Lock specific versions to prevent breaking changes",
"deprecation_management": "Graceful handling of deprecated node versions"
}
}
Node Security
{
"security_features": {
"malicious_scanning": "Automatic scanning for malicious behavior",
"pip_wheel_checks": "Verification of custom pip wheels",
"system_call_monitoring": "Detection of arbitrary system calls",
"verification_flags": "Visual indicators for verified nodes",
"security_standards": "Comprehensive security standards compliance"
}
}
Search and Discovery
{
"discovery_features": {
"registry_search": "Search across all registered nodes",
"workflow_integration": "Find nodes suitable for specific workflows",
"community_ratings": "User feedback and quality metrics",
"categorization": "Organized node categories and tags",
"documentation": "Centralized documentation and examples"
}
}
Publishing Nodes
Getting Started
# Publishing Your First Node
## Prerequisites
- Complete custom node development
- GitHub repository with your node
- Semantic versioning understanding
- Security compliance verification
## Publishing Process
1. **Prepare your node**: Ensure it follows registry standards
2. **Security review**: Pass security scanning requirements
3. **Version management**: Implement semantic versioning
4. **Documentation**: Provide comprehensive documentation
5. **Registry submission**: Submit to the ComfyUI Registry
6. **Community review**: Community feedback and approval
7. **Publication**: Node becomes available to users
Registry Standards
{
"registry_standards": {
"node_structure": {
"required_files": ["__init__.py", "node_definition.py"],
"documentation": "README.md with usage examples",
"version_info": "Version information in node files",
"dependencies": "requirements.txt with all dependencies"
},
"security_requirements": {
"no_malicious_code": "No arbitrary system calls",
"safe_dependencies": "Verified pip packages only",
"no_network_calls": "No unauthorized network requests",
"code_review": "Community code review process"
},
"versioning_requirements": {
"semantic_versioning": "Follow semver.org standards",
"changelog": "Detailed changelog for each version",
"breaking_changes": "Clear documentation of breaking changes",
"deprecation_policy": "Graceful deprecation handling"
}
}
}
Node Versioning
Semantic Versioning
# Semantic Versioning for ComfyUI Nodes
## Version Format: MAJOR.MINOR.PATCH
- **MAJOR**: Breaking changes that require workflow updates
- **MINOR**: New features that are backward compatible
- **PATCH**: Bug fixes that are backward compatible
## Examples
- `1.0.0`: Initial release
- `1.0.1`: Bug fix (backward compatible)
- `1.1.0`: New feature (backward compatible)
- `2.0.0`: Breaking change (requires workflow updates)
## Workflow Integration
```json
{
"workflow_versioning": {
"node_versions": {
"MyCustomNode": "1.2.3",
"AnotherNode": "2.1.0"
},
"compatibility": "Workflows store exact node versions",
"reproduction": "Reliable workflow reproduction",
"upgrade_path": "Clear upgrade instructions"
}
}
Version Management
# Node version definition
class MyCustomNode:
VERSION = "1.2.3"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"strength": ("FLOAT", {
"default": 0.5,
"min": 0.0,
"max": 1.0
})
}
}
@classmethod
def get_version_info(cls):
return {
"version": cls.VERSION,
"compatibility": ">=1.0.0",
"breaking_changes": [],
"new_features": ["Enhanced processing", "Better performance"],
"bug_fixes": ["Fixed memory leak", "Improved error handling"]
}
Security Standards
Security Scanning
{
"security_scanning": {
"malicious_code_detection": {
"system_calls": "Scan for arbitrary system calls",
"network_requests": "Detect unauthorized network requests",
"file_operations": "Monitor file system access",
"process_execution": "Check for process spawning"
},
"dependency_verification": {
"pip_packages": "Verify all pip dependencies",
"custom_wheels": "Scan custom wheel files",
"version_pinning": "Ensure dependency version pinning",
"vulnerability_scanning": "Check for known vulnerabilities"
},
"code_analysis": {
"static_analysis": "Automated code analysis",
"dynamic_analysis": "Runtime behavior monitoring",
"pattern_detection": "Detect suspicious code patterns",
"community_review": "Human code review process"
}
}
}
Security Best Practices
# Security-compliant node example
import os
import json
from pathlib import Path
class SecureCustomNode:
"""
Example of a security-compliant custom node
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input_data": ("STRING", {"multiline": True})
}
}
@classmethod
def execute(cls, input_data):
# Safe operations only
try:
# No arbitrary system calls
# No network requests
# No file system access outside allowed directories
result = cls.process_data_safely(input_data)
return (result,)
except Exception as e:
# Proper error handling
return (f"Error: {str(e)}",)
@classmethod
def process_data_safely(cls, data):
# Only safe data processing
# No external dependencies
# No system calls
return data.upper()
Registry Integration
Node Registration
# Registry-compliant node structure
class RegistryCompliantNode:
"""
A node that follows registry standards
"""
# Version information
VERSION = "1.0.0"
AUTHOR = "Your Name"
DESCRIPTION = "A registry-compliant custom node"
CATEGORY = "Custom/Registry"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input": ("STRING", {"default": "Hello World"})
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("output",)
FUNCTION = "process"
@classmethod
def get_registry_info(cls):
return {
"version": cls.VERSION,
"author": cls.AUTHOR,
"description": cls.DESCRIPTION,
"category": cls.CATEGORY,
"security_verified": True,
"dependencies": [],
"compatibility": {
"comfyui_version": ">=0.0.1",
"python_version": ">=3.8"
}
}
def process(self, input):
return (input,)
Registry Metadata
{
"registry_metadata": {
"node_id": "comfyui-registry-compliant-node",
"name": "Registry Compliant Node",
"description": "A node that follows registry standards",
"version": "1.0.0",
"author": "Your Name",
"license": "MIT",
"repository": "https://github.com/yourusername/your-node",
"documentation": "https://github.com/yourusername/your-node#readme",
"tags": ["example", "registry", "compliant"],
"category": "Custom/Registry",
"security_verified": true,
"dependencies": [],
"compatibility": {
"comfyui_version": ">=0.0.1",
"python_version": ">=3.8"
},
"metrics": {
"downloads": 0,
"rating": 0,
"reviews": 0
}
}
}
Community Features
User Ratings and Reviews
{
"community_features": {
"user_ratings": {
"rating_system": "1-5 star rating system",
"review_comments": "Detailed user reviews",
"helpfulness_voting": "Vote on review helpfulness",
"rating_aggregation": "Average rating calculation"
},
"discovery": {
"search_functionality": "Search by name, description, tags",
"category_browsing": "Browse by node categories",
"trending_nodes": "Popular and trending nodes",
"recommendations": "Personalized node recommendations"
},
"workflow_integration": {
"workflow_templates": "Example workflows using the node",
"compatibility_info": "Compatibility with other nodes",
"version_tracking": "Track node versions in workflows",
"upgrade_notifications": "Notify users of available updates"
}
}
}
Quality Metrics
{
"quality_metrics": {
"download_statistics": {
"total_downloads": "Total number of downloads",
"monthly_downloads": "Downloads per month",
"version_distribution": "Download distribution by version",
"geographic_distribution": "Downloads by geographic region"
},
"user_engagement": {
"rating_distribution": "Distribution of user ratings",
"review_count": "Number of user reviews",
"workflow_usage": "Usage in community workflows",
"issue_reports": "Bug reports and feature requests"
},
"technical_metrics": {
"compatibility_score": "Compatibility with ComfyUI versions",
"performance_metrics": "Node execution performance",
"error_rates": "Error rates in user workflows",
"update_frequency": "Frequency of node updates"
}
}
}
Best Practices
Registry Compliance
# Registry Compliance Checklist
## Development Standards
- [ ] Follow semantic versioning
- [ ] Implement proper error handling
- [ ] Use secure coding practices
- [ ] Provide comprehensive documentation
- [ ] Include example workflows
- [ ] Test with multiple ComfyUI versions
## Security Requirements
- [ ] No arbitrary system calls
- [ ] No unauthorized network requests
- [ ] Safe dependency management
- [ ] Code review by community
- [ ] Security scanning compliance
- [ ] Regular security updates
## Documentation Standards
- [ ] Clear README with examples
- [ ] API documentation
- [ ] Changelog for each version
- [ ] Installation instructions
- [ ] Troubleshooting guide
- [ ] Community support channels
Version Management
# Version management best practices
class VersionedNode:
VERSION = "1.2.3"
@classmethod
def get_version_info(cls):
return {
"version": cls.VERSION,
"changelog": {
"1.2.3": {
"type": "patch",
"changes": ["Fixed memory leak", "Improved performance"],
"breaking_changes": False
},
"1.2.0": {
"type": "minor",
"changes": ["Added new feature", "Enhanced UI"],
"breaking_changes": False
},
"1.0.0": {
"type": "major",
"changes": ["Initial release"],
"breaking_changes": False
}
},
"migration_guide": {
"1.2.3": "No migration required",
"1.2.0": "No migration required"
}
}
Important Notes
Registry Benefits
# Registry Benefits for Developers
## Standardization
- Consistent development practices
- Community-approved standards
- Quality assurance processes
- Security verification
## Discovery
- Increased node visibility
- Community ratings and reviews
- Search functionality
- Workflow integration
## Maintenance
- Version management
- Dependency tracking
- Security updates
- Community support
User Benefits
# Registry Benefits for Users
## Safety
- Security-verified nodes
- Malicious code protection
- Safe dependency management
- Community oversight
## Reliability
- Version tracking in workflows
- Reproducible workflows
- Upgrade notifications
- Compatibility information
## Discovery
- Search functionality
- Community ratings
- Example workflows
- Documentation
ComfyUI Registry Publishing - Step-by-Step Guide
Purpose
This guide provides step-by-step instructions for publishing custom nodes to the ComfyUI Registry, including account setup, metadata configuration, and automated publishing workflows.
Features
- Account setup: Create publisher accounts and API keys
- Metadata management: Configure node metadata in pyproject.toml
- Manual publishing: Use Comfy CLI for manual publishing
- Automated publishing: GitHub Actions for automated publishing
- Version management: Semantic versioning and automated updates
Step 1: Set Up a Registry Account
Create a Publisher
# Creating a Publisher Account
## Steps
1. **Go to Comfy Registry**: Visit the registry website
2. **Create publisher account**: Set up your publisher identity
3. **Choose publisher ID**: Select a globally unique identifier
4. **Profile setup**: Complete your publisher profile
## Important Notes
- **Publisher ID**: Globally unique, cannot be changed later
- **URL format**: Used in custom node URLs
- **Profile page**: Publisher ID found after @ symbol
Create an API Key
# Creating an API Key
## Steps
1. **Navigate to API keys**: Go to publisher settings
2. **Select publisher**: Choose the publisher for the API key
3. **Name the key**: Give your API key a descriptive name
4. **Save securely**: Store the key safely (cannot be recovered)
## Security Notes
- **Safe storage**: Save API key in secure location
- **No recovery**: Lost keys cannot be recovered
- **Access control**: Limit key access to necessary personnel
Step 2: Add Metadata
Install Comfy CLI
# Install Comfy CLI
pip install comfy-cli
# Verify installation
comfy --version
Initialize Node Metadata
# Initialize node metadata
comfy node init
Generated pyproject.toml
# pyproject.toml
[project]
name = "" # Unique identifier for your node. Immutable after creation.
description = ""
version = "1.0.0" # Custom Node version. Must be semantically versioned.
license = { file = "LICENSE.txt" }
dependencies = [] # Filled in from requirements.txt
[project.urls]
Repository = "https://github.com/..."
[tool.comfy]
PublisherId = "" # TODO (fill in Publisher ID from Comfy Registry Website).
DisplayName = "" # Display name for the Custom Node. Can be changed later.
Icon = "https://example.com/icon.png" # SVG, PNG, JPG or GIF (MAX. 800x400px)
Complete Metadata Configuration
# Complete pyproject.toml example
[project]
name = "my-awesome-node"
description = "An awesome custom node for ComfyUI"
version = "1.0.0"
license = { file = "LICENSE.txt" }
dependencies = [
"torch>=1.9.0",
"pillow>=8.3.0",
"numpy>=1.21.0"
]
[project.urls]
Repository = "https://github.com/yourusername/ComfyUI-MyAwesomeNode"
Documentation = "https://github.com/yourusername/ComfyUI-MyAwesomeNode#readme"
Issues = "https://github.com/yourusername/ComfyUI-MyAwesomeNode/issues"
[tool.comfy]
PublisherId = "your-publisher-id"
DisplayName = "My Awesome Node"
Icon = "https://raw.githubusercontent.com/yourusername/ComfyUI-MyAwesomeNode/main/icon.png"
Category = "Custom/Image Processing"
Tags = ["image", "processing", "awesome"]
Step 3: Publish to the Registry
Option 1: Manual Publishing with Comfy CLI
# Publish node manually
comfy node publish
# You'll be prompted for the API key
# API Key for publisher '<publisher id>': ****************************************************
# ...Version 1.0.0 Published.
# See it here: https://registry.comfy.org/publisherId/your-node
API Key Handling
# API Key Best Practices
## Copy-Paste Issues
- **Windows CTRL+V**: May add \x16 at the end
- **Right-click paste**: Recommended for clean pasting
- **Manual typing**: Alternative for short keys
## Security
- **Hidden input**: API key is hidden by default
- **Secure storage**: Store in password manager
- **Access control**: Limit who has access to keys
Option 2: Automated Publishing with GitHub Actions
Set Up GitHub Secret
# GitHub Secret Setup
## Steps
1. **Go to repository settings**: Settings -> Secrets and Variables -> Actions
2. **Create new secret**: Under Repository secrets -> New Repository Secret
3. **Name the secret**: `REGISTRY_ACCESS_TOKEN`
4. **Add API key**: Store your API key as the value
5. **Save securely**: Confirm the secret is saved
Create GitHub Action
# .github/workflows/publish_action.yml
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "pyproject.toml"
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
with:
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
Customize for Different Branches
# For different branch names (e.g., master)
on:
workflow_dispatch:
push:
branches:
- master # Change from 'main' to 'master'
paths:
- "pyproject.toml"
Test the GitHub Action
# Testing Automated Publishing
## Steps
1. **Update version**: Change version in pyproject.toml
2. **Commit changes**: Commit and push to repository
3. **Check action**: Monitor GitHub Actions tab
4. **Verify registry**: Check registry for updated node
5. **Test workflow**: Ensure action runs successfully
## Triggers
- **Manual trigger**: workflow_dispatch
- **Automatic trigger**: Push to main branch with pyproject.toml changes
- **Version updates**: Any change to pyproject.toml triggers publish
Advanced Publishing Features
Version Management
# Semantic versioning in pyproject.toml
[project]
version = "1.2.3" # MAJOR.MINOR.PATCH
# Version bumping examples
# 1.0.0 -> 1.0.1 # Patch (bug fixes)
# 1.0.1 -> 1.1.0 # Minor (new features)
# 1.1.0 -> 2.0.0 # Major (breaking changes)
Dependency Management
# Dependencies from requirements.txt
[project]
dependencies = [
"torch>=1.9.0",
"pillow>=8.3.0",
"numpy>=1.21.0",
"opencv-python>=4.5.0"
]
# Optional dependencies
[project.optional-dependencies]
dev = [
"pytest>=6.0.0",
"black>=21.0.0",
"flake8>=3.8.0"
]
Metadata Validation
# Metadata Requirements
## Required Fields
- **name**: Unique identifier (immutable)
- **description**: Clear description of functionality
- **version**: Semantic version (e.g., 1.0.0)
- **PublisherId**: Your publisher ID from registry
- **DisplayName**: User-friendly display name
## Optional Fields
- **Icon**: URL to icon image (max 800x400px)
- **Category**: Node category for organization
- **Tags**: Searchable tags for discovery
- **Repository**: GitHub repository URL
- **Documentation**: Documentation URL
- **Issues**: Issues tracker URL
Best Practices
Publishing Workflow
# Publishing Best Practices
## Pre-Publishing Checklist
- [ ] Test node functionality thoroughly
- [ ] Update version in pyproject.toml
- [ ] Verify all metadata is correct
- [ ] Test with multiple ComfyUI versions
- [ ] Update documentation if needed
- [ ] Create/update example workflows
- [ ] Review security compliance
## Publishing Process
1. **Development**: Complete node development
2. **Testing**: Test thoroughly in development
3. **Version bump**: Update version number
4. **Commit**: Commit changes to repository
5. **Push**: Push to main branch
6. **Verify**: Check registry for publication
7. **Test**: Test published node installation
Security Considerations
# Security Best Practices
## API Key Security
- **Secure storage**: Use password manager
- **Access control**: Limit key access
- **Rotation**: Rotate keys periodically
- **Monitoring**: Monitor key usage
## Code Security
- **No secrets**: Don't commit API keys
- **Dependency scanning**: Scan for vulnerabilities
- **Code review**: Review all code changes
- **Security testing**: Test for security issues
Version Management
# Version Management Best Practices
## Semantic Versioning
- **MAJOR**: Breaking changes (2.0.0)
- **MINOR**: New features (1.1.0)
- **PATCH**: Bug fixes (1.0.1)
## Version Bumping
- **Bug fixes**: Increment patch version
- **New features**: Increment minor version
- **Breaking changes**: Increment major version
- **Pre-release**: Use pre-release versions (1.0.0-alpha.1)
Troubleshooting
Common Issues
# Common Publishing Issues
## API Key Issues
- **Invalid key**: Verify key is correct
- **Expired key**: Create new key if expired
- **Permission issues**: Check key permissions
- **Copy-paste issues**: Use right-click paste
## Metadata Issues
- **Missing fields**: Ensure all required fields are present
- **Invalid format**: Check TOML syntax
- **Version format**: Use semantic versioning
- **Publisher ID**: Verify publisher ID is correct
## GitHub Actions Issues
- **Secret not found**: Check secret name and value
- **Permission denied**: Verify repository permissions
- **Workflow not triggered**: Check branch and path filters
- **Action failed**: Check action logs for errors
Debugging Steps
# Debug publishing issues
comfy node publish --verbose
# Check node metadata
comfy node info
# Validate pyproject.toml
comfy node validate
# Test without publishing
comfy node publish --dry-run
Important Notes
Registry Benefits
# Registry Benefits
## For Developers
- **Automated publishing**: GitHub Actions integration
- **Version management**: Semantic versioning
- **Security scanning**: Automatic security checks
- **Community feedback**: User ratings and reviews
## For Users
- **Safe installation**: Security-verified nodes
- **Version tracking**: Reliable workflow reproduction
- **Easy discovery**: Search and categorization
- **Quality assurance**: Community oversight
Publishing Timeline
# Publishing Timeline
## Initial Publication
1. **Setup**: 5-10 minutes
2. **Metadata**: 10-15 minutes
3. **First publish**: 2-3 minutes
4. **Verification**: 5-10 minutes
## Updates
1. **Version bump**: 1-2 minutes
2. **Commit/push**: 1-2 minutes
3. **Automated publish**: 2-3 minutes
4. **Registry update**: 1-2 minutes
ComfyUI Registry - Claim My Node Feature
Purpose
The "Claim My Node" feature allows developers to claim ownership of custom nodes in the ComfyUI Registry that were migrated from the legacy ComfyUI Manager system. This ensures proper ownership, security, and accountability within the community.
Features
- Node ownership: Claim migrated nodes from legacy system
- GitHub authentication: Verify repository admin access
- Publisher management: Create and manage publishers
- Security verification: Ensure rightful ownership
- Smooth migration: Transition from legacy to new registry standards
What are Unclaimed Nodes?
Legacy System Migration
# Unclaimed Nodes Overview
## What are Unclaimed Nodes?
- **Legacy migration**: Nodes from ComfyUI Manager legacy system
- **New standards**: Migrated to Comfy Registry with latest standards
- **Waiting for claim**: Original authors need to claim ownership
- **Smooth transition**: Maintain proper ownership and control
## Migration Process
1. **Legacy system**: Originally published in ComfyUI Manager
2. **Registry migration**: Moved to Comfy Registry with new standards
3. **Unclaimed status**: Waiting for original authors to claim
4. **Ownership verification**: Ensure proper ownership and control
Node Status
{
"node_status": {
"unclaimed": {
"description": "Nodes migrated from legacy system",
"status": "Waiting for original author to claim",
"action_required": "Claim ownership through registry",
"benefits": "Full control and management capabilities"
},
"claimed": {
"description": "Nodes with verified ownership",
"status": "Fully managed by original author",
"capabilities": "Update, version, and manage node",
"security": "Verified ownership and permissions"
}
}
}
Getting Started
Step 1: Navigate to Unclaimed Node Page
# Accessing Unclaimed Nodes
## Steps
1. **Visit registry**: Go to Comfy Registry website
2. **Find unclaimed nodes**: Look for "unclaimed" status
3. **Click claim button**: "Claim my node!" button
4. **Review node details**: Check node information and requirements
## Node Information
- **Node name**: Original node name
- **Repository link**: GitHub repository URL
- **Publisher status**: Current ownership status
- **Migration details**: Legacy system information
Step 2: Create a Publisher (if needed)
# Publisher Creation
## When Required
- **No existing publisher**: First time using registry
- **New publisher needed**: Want to use different publisher
- **Account setup**: Complete publisher profile
## Publisher Creation Process
1. **Account setup**: Create publisher account
2. **Choose publisher ID**: Globally unique identifier
3. **Profile completion**: Complete publisher profile
4. **Verification**: Verify publisher information
Step 3: Select Publisher
# Publisher Selection
## Selection Process
1. **Choose publisher**: Select from existing publishers
2. **Verify permissions**: Ensure admin access
3. **Redirect to claim**: Navigate to claim page
4. **Review details**: Check node and publisher information
## Publisher Requirements
- **Admin access**: Must have admin rights to repository
- **GitHub account**: Logged in to correct GitHub account
- **Repository access**: Admin privileges for node repository
Claim Process
Step 1: Review Node Information
# Node Information Review
## Information to Check
- **Node name**: Verify correct node
- **Repository link**: Check GitHub repository
- **Publisher status**: Current ownership status
- **Migration details**: Legacy system information
## Verification Steps
1. **Node details**: Confirm node information
2. **Repository access**: Verify GitHub repository
3. **Admin privileges**: Check admin access
4. **Publisher selection**: Confirm publisher choice
Step 2: GitHub Authentication
# GitHub Authentication Process
## Authentication Steps
1. **Click "Continue with GitHub"**: Start authentication
2. **Login verification**: Ensure correct GitHub account
3. **Admin rights check**: Verify admin privileges
4. **Repository access**: Confirm repository permissions
## Requirements
- **GitHub account**: Must be logged in to correct account
- **Admin rights**: Repository admin privileges required
- **Repository access**: Full access to node repository
- **Authentication**: Complete GitHub OAuth process
Step 3: Verify Admin Access
# Admin Access Verification
## Verification Process
1. **Repository check**: Verify GitHub repository access
2. **Admin privileges**: Confirm admin rights
3. **Permission validation**: Check repository permissions
4. **Access confirmation**: Ensure full repository access
## Requirements
- **Admin privileges**: Repository admin rights
- **Repository access**: Full access to node repository
- **Permission verification**: Confirm admin capabilities
- **Access validation**: Verify repository permissions
Step 4: Claim the Node
# Node Claiming Process
## Claiming Steps
1. **Click "Claim"**: Initiate claiming process
2. **Verification**: Confirm admin access
3. **Ownership transfer**: Change publisher status
4. **Completion**: Node ownership confirmed
## Result
- **Publisher status**: Changes to show ownership
- **Node management**: Full control over node
- **Update capabilities**: Can update and manage node
- **Security**: Verified ownership and permissions
Step 5: Complete!
# Claim Completion
## What Happens Next
1. **Ownership confirmed**: Node ownership verified
2. **Management access**: Full node management capabilities
3. **Update permissions**: Can update and version node
4. **Registry integration**: Node fully integrated with registry
## Next Steps
- **Node management**: Update and maintain node
- **Version control**: Manage node versions
- **Registry publishing**: Publish updates to registry
- **Community engagement**: Manage user feedback
Advanced Features
Publisher Management
# Publisher Management
## Publisher Capabilities
- **Node ownership**: Manage multiple nodes
- **Version control**: Update and version nodes
- **Registry publishing**: Publish to registry
- **Community management**: Handle user feedback
## Publisher Requirements
- **Admin access**: Repository admin privileges
- **GitHub account**: Verified GitHub account
- **Repository ownership**: Full repository access
- **Registry integration**: Registry account setup
Node Management
# Node Management After Claiming
## Management Capabilities
- **Node updates**: Update node functionality
- **Version control**: Manage node versions
- **Registry publishing**: Publish to registry
- **Community engagement**: Handle user feedback
## Security Features
- **Ownership verification**: Verified node ownership
- **Admin access**: Repository admin privileges
- **Registry integration**: Full registry capabilities
- **Community oversight**: Community feedback and ratings
Best Practices
Claiming Process
# Claiming Best Practices
## Pre-Claiming Checklist
- [ ] Verify GitHub account access
- [ ] Confirm repository admin rights
- [ ] Check node information accuracy
- [ ] Review publisher requirements
- [ ] Prepare for claim process
## Claiming Process
1. **Account verification**: Ensure correct GitHub account
2. **Repository access**: Verify admin privileges
3. **Node review**: Check node information
4. **Publisher setup**: Create or select publisher
5. **Claim initiation**: Start claiming process
6. **Verification**: Complete admin access verification
7. **Completion**: Confirm node ownership
Security Considerations
# Security Best Practices
## Account Security
- **GitHub account**: Use secure GitHub account
- **Repository access**: Limit admin access to necessary personnel
- **Authentication**: Use strong authentication methods
- **Access control**: Monitor repository access
## Registry Security
- **Publisher verification**: Verify publisher identity
- **Node ownership**: Confirm rightful ownership
- **Registry integration**: Secure registry access
- **Community oversight**: Community feedback and ratings
Troubleshooting
Common Issues
# Common Claiming Issues
## Authentication Issues
- **GitHub login**: Ensure correct GitHub account
- **Admin access**: Verify repository admin rights
- **Repository access**: Check repository permissions
- **Authentication failure**: Retry authentication process
## Publisher Issues
- **Publisher creation**: Create publisher if needed
- **Publisher selection**: Choose correct publisher
- **Publisher verification**: Verify publisher information
- **Publisher access**: Check publisher permissions
## Node Issues
- **Node information**: Verify node details
- **Repository link**: Check GitHub repository
- **Migration status**: Confirm migration from legacy system
- **Claim eligibility**: Verify claim eligibility
Debugging Steps
# Debugging Claim Issues
## Authentication Debugging
1. **Check GitHub account**: Verify correct account
2. **Repository access**: Confirm admin privileges
3. **Authentication status**: Check login status
4. **Permission verification**: Verify repository permissions
## Publisher Debugging
1. **Publisher creation**: Create publisher if needed
2. **Publisher selection**: Choose correct publisher
3. **Publisher verification**: Verify publisher information
4. **Publisher access**: Check publisher permissions
## Node Debugging
1. **Node information**: Verify node details
2. **Repository link**: Check GitHub repository
3. **Migration status**: Confirm migration status
4. **Claim eligibility**: Verify claim eligibility
Important Notes
Claim Benefits
# Claim Benefits
## For Developers
- **Node ownership**: Full control over node
- **Version management**: Manage node versions
- **Registry publishing**: Publish to registry
- **Community engagement**: Handle user feedback
## For Users
- **Verified ownership**: Confirmed node ownership
- **Quality assurance**: Original author management
- **Security**: Verified node security
- **Community oversight**: Community feedback and ratings
Migration Timeline
# Migration Timeline
## Legacy to Registry
1. **Legacy system**: Original ComfyUI Manager
2. **Registry migration**: Move to Comfy Registry
3. **Unclaimed status**: Waiting for author claim
4. **Claim process**: Author claims ownership
5. **Registry integration**: Full registry capabilities
## Claim Process
1. **Account setup**: 5-10 minutes
2. **Publisher creation**: 5-10 minutes
3. **Authentication**: 2-3 minutes
4. **Verification**: 1-2 minutes
5. **Claim completion**: 1-2 minutes
ComfyUI Registry - Security and Quality Standards
Purpose
The ComfyUI Registry maintains strict security and quality standards to ensure safe, functional, and valuable custom nodes for the community. These standards protect users from malicious code, ensure compatibility, and maintain high-quality development practices.
Features
- Security standards: Protection against malicious code and vulnerabilities
- Quality requirements: Functional, documented, and maintained nodes
- Community value: Valuable functionality for ComfyUI users
- Legal compliance: Adherence to applicable laws and regulations
- Fork guidelines: Clear differentiation and significant improvements
Base Standards
1. Community Value
# Community Value Requirements
## Requirements
- **Valuable functionality**: Must provide useful features to ComfyUI community
- **Clear purpose**: Well-defined functionality and use cases
- **User benefit**: Genuine value for ComfyUI users
- **Community contribution**: Positive impact on ComfyUI ecosystem
## Prohibited Practices
- **Excessive self-promotion**: Avoid excessive marketing content
- **Impersonation**: No misleading behavior or false claims
- **Malicious behavior**: No harmful or destructive functionality
- **Spam content**: No irrelevant or low-quality content
## Permitted Self-Promotion
- **Settings menu**: Self-promotion allowed in designated settings section
- **Useful functionality**: Top and side menus should contain useful features only
- **Community benefit**: Focus on community value over self-promotion
2. Node Compatibility
# Node Compatibility Requirements
## Compatibility Standards
- **No interference**: Don't interfere with other custom nodes
- **Installation safety**: Don't break other nodes' installation
- **Update compatibility**: Don't interfere with other nodes' updates
- **Removal safety**: Don't break other nodes' removal
## Dependency Management
- **Clear warnings**: Display warnings when dependent functionality is used
- **Example workflows**: Provide workflows demonstrating required nodes
- **Dependency documentation**: Document all dependencies clearly
- **Fallback handling**: Handle missing dependencies gracefully
3. Legal Compliance
# Legal Compliance Requirements
## Compliance Standards
- **Applicable laws**: Must comply with all applicable laws and regulations
- **Jurisdiction compliance**: Follow laws in relevant jurisdictions
- **Data protection**: Comply with data protection regulations
- **Intellectual property**: Respect intellectual property rights
## Legal Considerations
- **Copyright compliance**: Respect copyright and licensing
- **Privacy regulations**: Follow privacy and data protection laws
- **Export controls**: Comply with export control regulations
- **Terms of service**: Follow platform terms of service
4. Quality Requirements
# Quality Requirements
## Quality Standards
- **Fully functional**: Nodes must work as intended
- **Well documented**: Comprehensive documentation and examples
- **Actively maintained**: Regular updates and bug fixes
- **User support**: Provide user support and feedback
## Documentation Requirements
- **README**: Clear installation and usage instructions
- **API documentation**: Document all functions and parameters
- **Example workflows**: Provide working example workflows
- **Troubleshooting**: Include common issues and solutions
5. Fork Guidelines
# Fork Guidelines
## Fork Requirements
- **Distinct names**: Must have clearly different names from original
- **Significant differences**: Provide substantial functionality or code changes
- **Improvement justification**: Clear reasons for forking
- **Original attribution**: Proper attribution to original work
## Fork Standards
- **Unique functionality**: Add new features or improvements
- **Code differentiation**: Substantial code changes
- **User benefit**: Clear benefits over original
- **Community value**: Provide additional value to community
Security Standards
eval/exec Calls Policy
# eval/exec Calls Policy
## Prohibition
- **eval() function**: Prohibited in custom nodes
- **exec() function**: Prohibited in custom nodes
- **Dynamic code execution**: No arbitrary code execution
- **User input processing**: No eval/exec with user inputs
## Security Risks
- **Remote Code Execution (RCE)**: Potential for arbitrary code execution
- **Workflow exploitation**: Workflows can be exploited for attacks
- **Malicious code execution**: Risk of harmful code execution
- **Security vulnerabilities**: Multiple attack vectors
## Attack Vectors
- **Keylogging**: Potential for keylogging attacks
- **Ransomware**: Risk of ransomware attacks
- **Data theft**: Potential for data theft
- **System compromise**: Risk of system compromise
subprocess for pip install Policy
# subprocess for pip install Policy
## Prohibition
- **Runtime installation**: No subprocess calls for pip install
- **Package installation**: No dynamic package installation
- **Dependency management**: Use ComfyUI Manager instead
- **Manual installation**: Avoid manual package installation
## Reasoning
- **ComfyUI Manager**: Centralized dependency management
- **Security improvement**: Prevents supply chain attacks
- **User experience**: Eliminates multiple ComfyUI reloads
- **Dependency tracking**: Better dependency management
## Best Practices
- **requirements.txt**: Use requirements.txt for dependencies
- **ComfyUI Manager**: Let users install dependencies through manager
- **Documentation**: Document all dependencies clearly
- **Fallback handling**: Handle missing dependencies gracefully
Code Obfuscation Policy
# Code Obfuscation Policy
## Prohibition
- **Obfuscated code**: No obfuscated or minified code
- **Code hiding**: No attempts to hide code functionality
- **Encrypted code**: No encrypted or encoded code
- **Unreadable code**: No intentionally unreadable code
## Reasoning
- **Code review**: Impossible to review obfuscated code
- **Security risk**: Obfuscated code likely to be malicious
- **Transparency**: Code must be transparent and reviewable
- **Community trust**: Obfuscated code erodes community trust
## Requirements
- **Readable code**: Code must be human-readable
- **Clear functionality**: Code purpose must be clear
- **Reviewable**: Code must be reviewable by community
- **Transparent**: No hidden or obfuscated functionality
Implementation Guidelines
Security Best Practices
# Security-compliant node example
import os
import json
from pathlib import Path
class SecureCustomNode:
"""
Example of a security-compliant custom node
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input_data": ("STRING", {"multiline": True})
}
}
@classmethod
def execute(cls, input_data):
# Safe operations only
try:
# No eval/exec calls
# No subprocess calls
# No obfuscated code
result = cls.process_data_safely(input_data)
return (result,)
except Exception as e:
# Proper error handling
return (f"Error: {str(e)}",)
@classmethod
def process_data_safely(cls, data):
# Only safe data processing
# No external dependencies
# No system calls
return data.upper()
Quality Implementation
# Quality-compliant node example
class QualityCustomNode:
"""
Example of a quality-compliant custom node
"""
# Version information
VERSION = "1.0.0"
AUTHOR = "Your Name"
DESCRIPTION = "A quality-compliant custom node"
CATEGORY = "Custom/Quality"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input": ("STRING", {"default": "Hello World"})
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("output",)
FUNCTION = "process"
@classmethod
def get_quality_info(cls):
return {
"version": cls.VERSION,
"author": cls.AUTHOR,
"description": cls.DESCRIPTION,
"documentation": "https://github.com/yourusername/your-node#readme",
"examples": "https://github.com/yourusername/your-node/tree/main/examples",
"support": "https://github.com/yourusername/your-node/issues"
}
def process(self, input):
# Well-documented functionality
# Clear error handling
# Proper return values
return (input,)
Compliance Checklist
Security Compliance
# Security Compliance Checklist
## Code Security
- [ ] No eval/exec calls
- [ ] No subprocess calls for pip install
- [ ] No obfuscated code
- [ ] No malicious functionality
- [ ] Safe data processing
- [ ] Proper error handling
## Dependency Security
- [ ] Use requirements.txt
- [ ] Document all dependencies
- [ ] No dynamic package installation
- [ ] Safe dependency management
- [ ] Fallback handling for missing dependencies
Quality Compliance
# Quality Compliance Checklist
## Functionality
- [ ] Fully functional node
- [ ] Well-documented code
- [ ] Clear error handling
- [ ] Proper return values
- [ ] User-friendly interface
## Documentation
- [ ] Comprehensive README
- [ ] API documentation
- [ ] Example workflows
- [ ] Troubleshooting guide
- [ ] User support information
Community Value Compliance
# Community Value Compliance Checklist
## Value Proposition
- [ ] Clear functionality
- [ ] User benefit
- [ ] Community contribution
- [ ] Positive impact
- [ ] Useful features
## Content Quality
- [ ] No excessive self-promotion
- [ ] No misleading behavior
- [ ] No malicious content
- [ ] High-quality content
- [ ] Community-focused
Best Practices
Development Standards
# Development Best Practices
## Code Quality
- **Readable code**: Write clear, readable code
- **Documentation**: Document all functions and classes
- **Error handling**: Implement proper error handling
- **Testing**: Test functionality thoroughly
## Security Practices
- **Safe operations**: Use only safe operations
- **Input validation**: Validate all user inputs
- **Error handling**: Handle errors gracefully
- **Dependency management**: Manage dependencies safely
Community Engagement
# Community Engagement Best Practices
## User Support
- **Documentation**: Provide comprehensive documentation
- **Examples**: Include working examples
- **Support**: Provide user support
- **Feedback**: Respond to user feedback
## Community Contribution
- **Value**: Provide genuine value to community
- **Quality**: Maintain high quality standards
- **Updates**: Regular updates and improvements
- **Collaboration**: Collaborate with community
Important Notes
Registry Benefits
# Registry Benefits
## For Developers
- **Quality assurance**: Maintained quality standards
- **Security**: Security-verified nodes
- **Community trust**: Trusted by community
- **Support**: Community support and feedback
## For Users
- **Safe installation**: Security-verified nodes
- **Quality assurance**: High-quality nodes
- **Community oversight**: Community-reviewed nodes
- **Reliable functionality**: Well-tested and maintained nodes
Compliance Timeline
# Compliance Timeline
## Initial Review
1. **Security scan**: Automatic security scanning
2. **Quality review**: Manual quality review
3. **Compliance check**: Standards compliance verification
4. **Approval**: Registry approval process
## Ongoing Compliance
1. **Regular updates**: Maintain compliance with updates
2. **Security monitoring**: Ongoing security monitoring
3. **Quality maintenance**: Maintain quality standards
4. **Community feedback**: Respond to community feedback
ComfyUI Registry - Custom Node CI/CD
Purpose
The ComfyUI Registry provides a comprehensive CI/CD (Continuous Integration/Continuous Deployment) system for custom nodes to ensure compatibility, functionality, and quality across different operating systems and configurations. This system helps developers test their nodes before publishing and prevents breaking changes from reaching users.
Features
- Automated testing: Test custom nodes across different environments
- Workflow validation: Run ComfyUI workflows to verify functionality
- Cross-platform support: Test on Linux, Mac, and Windows
- Model management: Download and test with different models
- Custom node testing: Test with other custom nodes for compatibility
- Results dashboard: View test results and output files
Introduction
Why CI/CD is Important
# CI/CD Importance for Custom Nodes
## Common Issues
- **Breaking changes**: Changes can break ComfyUI or other custom nodes
- **OS compatibility**: Different behavior across operating systems
- **PyTorch configurations**: Various PyTorch versions and configurations
- **Dependency conflicts**: Conflicts with other custom nodes
- **Model compatibility**: Issues with different model types
## CI/CD Benefits
- **Automated testing**: Test changes before publishing
- **Cross-platform validation**: Ensure compatibility across OS
- **Workflow verification**: Validate actual ComfyUI workflows
- **Quality assurance**: Catch issues before they reach users
- **Regression testing**: Prevent breaking existing functionality
Comfy-Action Overview
# Comfy-Action Overview
## Capabilities
- **Workflow execution**: Run ComfyUI workflow.json files
- **Model downloading**: Automatically download required models
- **Custom node support**: Install and test custom nodes
- **Multi-platform**: Support for Linux, Mac, and Windows
- **GitHub Actions**: Integrate with GitHub Actions workflow
## Features
- **Automated setup**: Automatic ComfyUI setup and configuration
- **Model management**: Download and cache models for testing
- **Custom node installation**: Install custom nodes for testing
- **Workflow execution**: Run complete ComfyUI workflows
- **Output collection**: Collect and upload test results
Getting Started
Setup Comfy-Action
# .github/workflows/comfy-ci.yml
name: ComfyUI CI/CD Testing
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test-comfy-workflow:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup ComfyUI with Comfy-Action
uses: Comfy-Org/comfy-action@main
with:
comfyui-version: "latest"
custom-nodes: |
ComfyUI-Model_preset_Pilot
ComfyUI-Manager
models: |
https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.ckpt
- name: Run test workflow
uses: Comfy-Org/comfy-action@main
with:
workflow-file: "test_workflow.json"
output-dir: "test_outputs"
- name: Upload test results
uses: actions/upload-artifact@v3
with:
name: test-results-${{ matrix.os }}
path: test_outputs/
Workflow Configuration
{
"workflow_configuration": {
"comfyui_version": "latest",
"custom_nodes": [
"ComfyUI-Model_preset_Pilot",
"ComfyUI-Manager",
"ComfyUI-Extra-Models"
],
"models": [
"https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.ckpt",
"https://huggingface.co/stabilityai/stable-diffusion-2-1/resolve/main/v2-1_768-ema-pruned.ckpt"
],
"test_workflows": [
"test_basic_workflow.json",
"test_advanced_workflow.json",
"test_error_handling.json"
]
}
}
Advanced Configuration
Custom Node Testing
# Advanced CI/CD configuration
name: Advanced ComfyUI Testing
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test-custom-node:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup ComfyUI
uses: Comfy-Org/comfy-action@main
with:
comfyui-version: "latest"
custom-nodes: |
ComfyUI-Model_preset_Pilot
ComfyUI-Manager
ComfyUI-Extra-Models
ComfyUI-AnimateDiff-Evolved
models: |
https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.ckpt
https://huggingface.co/stabilityai/stable-diffusion-2-1/resolve/main/v2-1_768-ema-pruned.ckpt
- name: Test basic functionality
uses: Comfy-Org/comfy-action@main
with:
workflow-file: "tests/basic_test.json"
output-dir: "test_outputs/basic"
- name: Test advanced features
uses: Comfy-Org/comfy-action@main
with:
workflow-file: "tests/advanced_test.json"
output-dir: "test_outputs/advanced"
- name: Test error handling
uses: Comfy-Org/comfy-action@main
with:
workflow-file: "tests/error_test.json"
output-dir: "test_outputs/error"
- name: Upload all test results
uses: actions/upload-artifact@v3
with:
name: comprehensive-test-results
path: test_outputs/
Multi-Platform Testing
# Multi-platform testing configuration
name: Multi-Platform ComfyUI Testing
on:
push:
branches: [ main ]
jobs:
test-multi-platform:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [3.8, 3.9, 3.10, 3.11]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Setup ComfyUI
uses: Comfy-Org/comfy-action@main
with:
comfyui-version: "latest"
custom-nodes: |
ComfyUI-Model_preset_Pilot
ComfyUI-Manager
models: |
https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.ckpt
- name: Run platform-specific tests
uses: Comfy-Org/comfy-action@main
with:
workflow-file: "tests/platform_test.json"
output-dir: "test_outputs/${{ matrix.os }}-${{ matrix.python-version }}"
- name: Upload platform results
uses: actions/upload-artifact@v3
with:
name: test-results-${{ matrix.os }}-${{ matrix.python-version }}
path: test_outputs/${{ matrix.os }}-${{ matrix.python-version }}/
Test Workflow Examples
Basic Test Workflow
{
"basic_test_workflow": {
"1": {
"class_type": "CheckpointLoaderSimple",
"inputs": {
"ckpt_name": "v1-5-pruned-emaonly.ckpt"
}
},
"2": {
"class_type": "CLIPTextEncode",
"inputs": {
"text": "a beautiful landscape",
"clip": ["1", 1]
}
},
"3": {
"class_type": "CLIPTextEncode",
"inputs": {
"text": "blurry, low quality",
"clip": ["1", 1]
}
},
"4": {
"class_type": "KSampler",
"inputs": {
"seed": 12345,
"steps": 20,
"cfg": 7.0,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1.0,
"model": ["1", 0],
"positive": ["2", 0],
"negative": ["3", 0],
"latent_image": ["5", 0]
}
},
"5": {
"class_type": "EmptyLatentImage",
"inputs": {
"width": 512,
"height": 512,
"batch_size": 1
}
},
"6": {
"class_type": "VAEDecode",
"inputs": {
"samples": ["4", 0],
"vae": ["1", 2]
}
},
"7": {
"class_type": "SaveImage",
"inputs": {
"filename_prefix": "test_output",
"images": ["6", 0]
}
}
}
}
Custom Node Test Workflow
{
"custom_node_test_workflow": {
"1": {
"class_type": "CheckpointLoaderSimple",
"inputs": {
"ckpt_name": "v1-5-pruned-emaonly.ckpt"
}
},
"2": {
"class_type": "ModelPresetLoader",
"inputs": {
"model": ["1", 0],
"preset_name": "realistic"
}
},
"3": {
"class_type": "CLIPTextEncode",
"inputs": {
"text": "a beautiful landscape",
"clip": ["1", 1]
}
},
"4": {
"class_type": "CLIPTextEncode",
"inputs": {
"text": "blurry, low quality",
"clip": ["1", 1]
}
},
"5": {
"class_type": "KSampler",
"inputs": {
"seed": 12345,
"steps": 20,
"cfg": 7.0,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1.0,
"model": ["2", 0],
"positive": ["3", 0],
"negative": ["4", 0],
"latent_image": ["6", 0]
}
},
"6": {
"class_type": "EmptyLatentImage",
"inputs": {
"width": 512,
"height": 512,
"batch_size": 1
}
},
"7": {
"class_type": "VAEDecode",
"inputs": {
"samples": ["5", 0],
"vae": ["1", 2]
}
},
"8": {
"class_type": "SaveImage",
"inputs": {
"filename_prefix": "custom_node_test",
"images": ["7", 0]
}
}
}
}
Results and Dashboard
CI/CD Dashboard
# CI/CD Dashboard Features
## Results Display
- **Test results**: View test execution results
- **Output files**: Download generated output files
- **Error logs**: View detailed error information
- **Performance metrics**: Monitor test performance
- **Platform comparison**: Compare results across platforms
## Dashboard Benefits
- **Visual feedback**: See test results before committing
- **Quality assurance**: Ensure changes don't break functionality
- **Regression testing**: Prevent breaking existing features
- **Cross-platform validation**: Verify compatibility across OS
- **Automated validation**: Reduce manual testing effort
Output Management
# Output management configuration
name: Output Management
on:
push:
branches: [ main ]
jobs:
test-with-outputs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup ComfyUI
uses: Comfy-Org/comfy-action@main
with:
comfyui-version: "latest"
custom-nodes: |
ComfyUI-Model_preset_Pilot
models: |
https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.ckpt
- name: Run test with outputs
uses: Comfy-Org/comfy-action@main
with:
workflow-file: "test_workflow.json"
output-dir: "test_outputs"
save-outputs: true
- name: Upload test outputs
uses: actions/upload-artifact@v3
with:
name: test-outputs
path: test_outputs/
retention-days: 30
- name: Upload to dashboard
uses: Comfy-Org/upload-to-dashboard@main
with:
dashboard-url: "https://ci.comfy.org"
api-key: ${{ secrets.DASHBOARD_API_KEY }}
test-results: "test_outputs/"
Best Practices
CI/CD Best Practices
# CI/CD Best Practices
## Test Coverage
- **Basic functionality**: Test core node functionality
- **Error handling**: Test error conditions and edge cases
- **Integration testing**: Test with other custom nodes
- **Performance testing**: Monitor performance and memory usage
- **Regression testing**: Ensure changes don't break existing functionality
## Workflow Design
- **Modular tests**: Create separate test workflows for different features
- **Realistic scenarios**: Use realistic test scenarios and data
- **Error simulation**: Test error conditions and recovery
- **Performance monitoring**: Monitor execution time and resource usage
- **Output validation**: Verify output quality and correctness
Testing Strategy
# Testing Strategy
## Test Types
- **Unit tests**: Test individual node functionality
- **Integration tests**: Test node interactions with ComfyUI
- **Workflow tests**: Test complete ComfyUI workflows
- **Performance tests**: Monitor performance and resource usage
- **Compatibility tests**: Test across different platforms and configurations
## Test Organization
- **Test structure**: Organize tests by functionality and complexity
- **Test data**: Use realistic test data and scenarios
- **Test isolation**: Ensure tests don't interfere with each other
- **Test cleanup**: Clean up test artifacts and temporary files
- **Test documentation**: Document test purpose and expected results
Important Notes
CI/CD Benefits
# CI/CD Benefits
## For Developers
- **Automated testing**: Reduce manual testing effort
- **Quality assurance**: Catch issues before publishing
- **Cross-platform validation**: Ensure compatibility across OS
- **Regression prevention**: Prevent breaking existing functionality
- **Continuous integration**: Integrate changes safely
## For Users
- **Reliable nodes**: Higher quality and reliability
- **Compatibility assurance**: Works across different platforms
- **Quality validation**: Tested and validated functionality
- **Error prevention**: Fewer bugs and issues
- **Performance optimization**: Optimized for different environments
Implementation Timeline
# Implementation Timeline
## Setup Phase
1. **Repository setup**: 5-10 minutes
2. **Workflow configuration**: 10-15 minutes
3. **Test workflow creation**: 15-30 minutes
4. **Initial testing**: 5-10 minutes
## Ongoing Maintenance
1. **Test updates**: 5-10 minutes per update
2. **Workflow maintenance**: 10-15 minutes
3. **Test execution**: 5-10 minutes
4. **Results review**: 5-10 minutes
ComfyUI Registry - pyproject.toml Specifications
Purpose
The pyproject.toml file is the configuration file for ComfyUI custom nodes in the registry. It contains metadata about the node, dependencies, compatibility requirements, and registry-specific information. This file is essential for publishing custom nodes to the ComfyUI Registry.
Features
- Node metadata: Name, version, description, and licensing information
- Dependency management: Python and ComfyUI version requirements
- Registry configuration: Publisher information and display settings
- Compatibility specification: OS and GPU accelerator support
- URL management: Repository, documentation, and bug tracker links
[project] Section
name (required)
# Node ID specification
[project]
name = "super-resolution-node" # ✅ Good: Simple and clear
name = "image-processor" # ✅ Good: Describes functionality
name = "ComfyUI-enhancer" # ❌ Bad: Includes ComfyUI
name = "123-tool" # ❌ Bad: Starts with number
Requirements:
- Must be less than 100 characters
- Can only contain alphanumeric characters, hyphens, underscores, and periods
- Cannot have consecutive special characters
- Cannot start with a number or special character
- Case-insensitive comparison
Best Practices:
- Use a short, descriptive name
- Don't include "ComfyUI" in the name
- Make it memorable and easy to type
version (required)
# Semantic versioning
[project]
version = "1.0.0" # Initial release
version = "1.1.0" # Added new features
version = "1.1.1" # Bug fix
version = "2.0.0" # Breaking changes
Version Format:
- X (MAJOR): Breaking changes
- Y (MINOR): New features (backwards compatible)
- Z (PATCH): Bug fixes
license (optional)
# License specification
[project]
# File reference
license = { file = "LICENSE" } # ✅ Points to LICENSE file
license = { file = "LICENSE.txt" } # ✅ Points to LICENSE.txt
# License name
license = { text = "MIT License" } # ✅ Correct format
license = { text = "Apache-2.0" } # ✅ Correct format
Common licenses: MIT, GPL, Apache
description (recommended)
# Node description
[project]
description = "A super resolution node for enhancing image quality"
repository (required)
# Repository URL
[project.urls]
Repository = "https://github.com/username/repository"
urls (recommended)
# Additional URLs
[project.urls]
Documentation = "https://github.com/username/repository/wiki"
"Bug Tracker" = "https://github.com/username/repository/issues"
requires-python (recommended)
# Python version requirements
[project]
requires-python = ">=3.8" # Python 3.8 or higher
requires-python = ">=3.8,<3.11" # Python 3.8 up to (but not including) 3.11
Frontend Version Compatibility (optional)
# ComfyUI frontend version compatibility
[project]
dependencies = [
"comfyui-frontend-package>=1.20.0" # Requires frontend 1.20.0 or newer
"comfyui-frontend-package<=1.21.6" # Restricts to frontend versions up to 1.21.6
"comfyui-frontend-package>=1.19,<1.22" # Works with frontend 1.19 to 1.21.x
"comfyui-frontend-package~=1.20.0" # Compatible with 1.20.x but not 1.21.0
"comfyui-frontend-package!=1.21.3" # Works with any version except 1.21.3
]
Use cases:
- Your custom node uses frontend APIs introduced in specific versions
- You've identified incompatibilities with certain frontend versions
- Your node requires specific UI features only available in newer versions
classifiers (recommended)
# Operating system and GPU compatibility
[project]
classifiers = [
# OS compatibility
"Operating System :: OS Independent", # Works on all operating systems
"Operating System :: Microsoft :: Windows", # Windows specific
"Operating System :: POSIX :: Linux", # Linux specific
"Operating System :: MacOS", # macOS specific
# GPU Accelerator support
"Environment :: GPU :: NVIDIA CUDA", # NVIDIA CUDA support
"Environment :: GPU :: AMD ROCm", # AMD ROCm support
"Environment :: GPU :: Intel Arc", # Intel Arc support
"Environment :: NPU :: Huawei Ascend", # Huawei Ascend support
"Environment :: GPU :: Apple Metal", # Apple Metal support
]
[tool.comfy] Section
PublisherId (required)
# Publisher identifier
[tool.comfy]
PublisherId = "john-doe" # ✅ Matches GitHub username
PublisherId = "image-wizard" # ✅ Unique identifier
DisplayName (optional)
# User-friendly display name
[tool.comfy]
DisplayName = "Super Resolution Node"
Icon (optional)
# Node icon URL
[tool.comfy]
Icon = "https://raw.githubusercontent.com/username/repo/main/icon.png"
Requirements:
- File types: SVG, PNG, JPG, or GIF
- Maximum resolution: 400px × 400px
- Aspect ratio should be square
Banner (optional)
# Banner image URL
[tool.comfy]
Banner = "https://raw.githubusercontent.com/username/repo/main/banner.png"
Requirements:
- File types: SVG, PNG, JPG, or GIF
- Aspect ratio: 21:9
requires-comfyui (optional)
# ComfyUI version compatibility
[tool.comfy]
requires-comfyui = ">=1.0.0" # ComfyUI 1.0.0 or higher
requires-comfyui = ">=1.0.0,<2.0.0" # ComfyUI 1.0.0 up to (but not including) 2.0.0
requires-comfyui = "~=1.0.0" # Compatible release: version 1.0.0 or newer, but not version 2.0.0
requires-comfyui = "!=1.2.3" # Any version except 1.2.3
requires-comfyui = ">0.1.3,<1.0.0" # Greater than 0.1.3 and less than 1.0.0
Supported operators: <, >, <=, >=, ~=, <>, != and ranges
includes (optional)
# Force include specific folders
[tool.comfy]
includes = ['dist']
Use cases:
- Custom nodes in frontend projects
- Final packaged output folder in .gitignore
- Force include for registry use
Complete Example
Basic Example
# Basic pyproject.toml example
[project]
name = "super-resolution-node"
version = "1.0.0"
description = "Enhance image quality using advanced super resolution techniques"
license = { file = "LICENSE" }
requires-python = ">=3.8"
dependencies = [
"comfyui-frontend-package<=1.21.6" # Frontend version compatibility
]
classifiers = [
"Operating System :: OS Independent" # Works on all operating systems
]
dynamic = ["dependencies"]
[tool.setuptools.dynamic]
dependencies = {file = ["requirements.txt"]}
[project.urls]
Repository = "https://github.com/username/super-resolution-node"
Documentation = "https://github.com/username/super-resolution-node/wiki"
"Bug Tracker" = "https://github.com/username/super-resolution-node/issues"
[tool.comfy]
PublisherId = "image-wizard"
DisplayName = "Super Resolution Node"
Icon = "https://raw.githubusercontent.com/username/super-resolution-node/main/icon.png"
Banner = "https://raw.githubusercontent.com/username/super-resolution-node/main/banner.png"
requires-comfyui = ">=1.0.0" # ComfyUI version compatibility
Advanced Example
# Advanced pyproject.toml example
[project]
name = "advanced-image-processor"
version = "2.1.3"
description = "Advanced image processing with AI-powered enhancement and real-time preview"
license = { file = "LICENSE" }
requires-python = ">=3.8,<3.12"
dependencies = [
"torch>=1.9.0",
"pillow>=8.3.0",
"numpy>=1.21.0",
"opencv-python>=4.5.0",
"comfyui-frontend-package>=1.20.0,<1.22.0"
]
classifiers = [
"Operating System :: OS Independent",
"Environment :: GPU :: NVIDIA CUDA",
"Environment :: GPU :: AMD ROCm",
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Topic :: Multimedia :: Graphics :: Graphics Conversion",
"Topic :: Scientific/Engineering :: Artificial Intelligence"
]
dynamic = ["dependencies"]
[tool.setuptools.dynamic]
dependencies = {file = ["requirements.txt"]}
[project.urls]
Repository = "https://github.com/username/advanced-image-processor"
Documentation = "https://github.com/username/advanced-image-processor/wiki"
"Bug Tracker" = "https://github.com/username/advanced-image-processor/issues"
"Homepage" = "https://github.com/username/advanced-image-processor"
"Changelog" = "https://github.com/username/advanced-image-processor/blob/main/CHANGELOG.md"
[tool.comfy]
PublisherId = "advanced-wizard"
DisplayName = "Advanced Image Processor"
Icon = "https://raw.githubusercontent.com/username/advanced-image-processor/main/assets/icon.png"
Banner = "https://raw.githubusercontent.com/username/advanced-image-processor/main/assets/banner.png"
requires-comfyui = ">=1.0.0,<2.0.0"
includes = ['dist', 'assets']
Best Practices
Naming Conventions
# Naming Best Practices
## Node Names
- **Short and descriptive**: Use clear, concise names
- **No ComfyUI prefix**: Don't include "ComfyUI" in the name
- **Memorable**: Easy to remember and type
- **Unique**: Ensure uniqueness in the registry
## Examples
- ✅ "image-processor" - Clear and descriptive
- ✅ "super-resolution" - Describes functionality
- ✅ "ai-upscaler" - Concise and informative
- ❌ "ComfyUI-enhancer" - Includes ComfyUI
- ❌ "123-tool" - Starts with number
Version Management
# Version Management Best Practices
## Semantic Versioning
- **MAJOR (X)**: Breaking changes that require workflow updates
- **MINOR (Y)**: New features that are backward compatible
- **PATCH (Z)**: Bug fixes that are backward compatible
## Version Examples
- `1.0.0`: Initial release
- `1.0.1`: Bug fix (backward compatible)
- `1.1.0`: New feature (backward compatible)
- `2.0.0`: Breaking change (requires workflow updates)
Dependency Management
# Dependency Management Best Practices
## Python Dependencies
- **Version pinning**: Pin specific versions for stability
- **Compatibility**: Ensure compatibility across Python versions
- **Security**: Use trusted packages and update regularly
## ComfyUI Dependencies
- **Frontend compatibility**: Specify frontend version requirements
- **Core compatibility**: Specify ComfyUI core version requirements
- **Testing**: Test with different ComfyUI versions
Metadata Quality
# Metadata Quality Best Practices
## Description
- **Clear purpose**: Describe what the node does
- **Use cases**: Explain when to use the node
- **Features**: Highlight key features and capabilities
## URLs
- **Repository**: Link to source code repository
- **Documentation**: Link to comprehensive documentation
- **Bug tracker**: Link to issue tracker for bug reports
- **Homepage**: Link to project homepage if available
Troubleshooting
Common Issues
# Common pyproject.toml Issues
## Name Issues
- **Invalid characters**: Use only alphanumeric, hyphens, underscores, periods
- **Length limit**: Keep under 100 characters
- **Special characters**: No consecutive special characters
- **Starting character**: Cannot start with number or special character
## Version Issues
- **Semantic versioning**: Use proper X.Y.Z format
- **Version comparison**: Ensure version ordering is correct
- **Breaking changes**: Increment major version for breaking changes
## Dependency Issues
- **Version conflicts**: Resolve conflicting dependency versions
- **Missing dependencies**: Include all required dependencies
- **Compatibility**: Ensure compatibility with ComfyUI versions
Validation
# Validate pyproject.toml
# Check syntax
python -m tomllib pyproject.toml
# Validate with setuptools
python -m setuptools.config.validate pyproject.toml
# Check dependencies
pip check
Important Notes
Registry Benefits
# Registry Benefits
## For Developers
- **Standardized metadata**: Consistent node information
- **Dependency management**: Clear dependency specifications
- **Version tracking**: Semantic versioning support
- **Compatibility**: OS and GPU compatibility information
## For Users
- **Clear information**: Comprehensive node metadata
- **Dependency awareness**: Know what dependencies are required
- **Compatibility**: Understand system requirements
- **Quality assurance**: Standardized node information
Implementation Timeline
# Implementation Timeline
## Initial Setup
1. **Basic metadata**: 5-10 minutes
2. **Dependency specification**: 10-15 minutes
3. **Registry configuration**: 5-10 minutes
4. **Testing**: 5-10 minutes
## Ongoing Maintenance
1. **Version updates**: 2-3 minutes per update
2. **Dependency updates**: 5-10 minutes
3. **Metadata updates**: 3-5 minutes
4. **Validation**: 2-3 minutes
ComfyUI Node Definition JSON Schema (v2.0)
Purpose
The ComfyUI Node Definition JSON schema (v2.0) provides a standardized way to define custom nodes using JSON Schema. This schema defines the structure, inputs, outputs, and metadata for ComfyUI custom nodes, ensuring consistency and compatibility across the ecosystem.
Features
- Standardized structure: Consistent node definition format
- Input specifications: Detailed input type definitions with validation
- Output definitions: Clear output type and structure specifications
- Metadata support: Node information, categories, and descriptions
- Advanced features: Lazy evaluation, remote data, and dynamic options
- Type safety: JSON Schema validation for node definitions
Schema Overview
Core Structure
{
"inputs": {
"input_name": {
"type": "INPUT_TYPE",
"name": "Display Name",
"default": "default_value",
"tooltip": "Help text",
"hidden": false,
"advanced": false,
"lazy": false
}
},
"outputs": [
{
"index": 0,
"name": "Output Name",
"type": "OUTPUT_TYPE",
"is_list": false,
"tooltip": "Output description"
}
],
"name": "NodeClassName",
"display_name": "User-Friendly Name",
"description": "Node description",
"category": "Node Category",
"output_node": false,
"python_module": "module_name",
"deprecated": false,
"experimental": false
}
Input Types
INT (Integer)
{
"type": "INT",
"name": "Steps",
"default": 20,
"min": 1,
"max": 100,
"step": 1,
"display": "slider",
"tooltip": "Number of sampling steps",
"control_after_generate": true
}
Properties:
default: Default value (number or array)min: Minimum valuemax: Maximum valuestep: Step incrementdisplay: Display type ("slider", "number", "knob")control_after_generate: Allow control after generation
FLOAT (Float)
{
"type": "FLOAT",
"name": "CFG Scale",
"default": 7.0,
"min": 0.1,
"max": 20.0,
"step": 0.1,
"round": 2,
"display": "slider",
"tooltip": "Classifier-free guidance scale"
}
Properties:
default: Default value (number or array)min: Minimum valuemax: Maximum valuestep: Step incrementround: Rounding precision (number or false)display: Display type ("slider", "number", "knob")
BOOLEAN (Boolean)
{
"type": "BOOLEAN",
"name": "Enable Feature",
"default": true,
"label_on": "On",
"label_off": "Off",
"tooltip": "Enable or disable feature"
}
Properties:
default: Default boolean valuelabel_on: Label for true statelabel_off: Label for false state
STRING (String)
{
"type": "STRING",
"name": "Prompt",
"default": "A beautiful landscape",
"multiline": true,
"dynamicPrompts": true,
"placeholder": "Enter your prompt here",
"tooltip": "Text prompt for generation"
}
Properties:
default: Default string valuemultiline: Allow multiline inputdynamicPrompts: Enable dynamic promptsdefaultVal: Alternative default valueplaceholder: Placeholder text
COMBO (Combo/Select)
{
"type": "COMBO",
"name": "Sampler",
"default": "euler",
"options": ["euler", "dpm_2", "dpm_2_ancestral", "lms"],
"tooltip": "Sampling method"
}
Properties:
default: Default selected valueoptions: Array of available optionscontrol_after_generate: Allow control after generationimage_upload: Enable image uploadimage_folder: Image folder type ("input", "output", "temp")allow_batch: Allow batch processingvideo_upload: Enable video uploadremote: Remote data configuration
Custom Types
{
"type": "CUSTOM_TYPE",
"name": "Custom Input",
"default": null,
"tooltip": "Custom input type"
}
Properties:
type: Custom type identifiername: Display namedefault: Default valuetooltip: Help text
Advanced Input Features
Lazy Evaluation
{
"type": "STRING",
"name": "Dynamic Input",
"lazy": true,
"tooltip": "This input is evaluated lazily"
}
Hidden Inputs
{
"type": "STRING",
"name": "Internal Data",
"hidden": true,
"default": "internal_value"
}
Advanced Inputs
{
"type": "FLOAT",
"name": "Advanced Setting",
"advanced": true,
"tooltip": "Advanced configuration option"
}
Remote Data
{
"type": "COMBO",
"name": "Remote Options",
"remote": {
"route": "/api/options",
"refresh": 30,
"response_key": "data",
"query_params": {
"category": "samplers"
},
"refresh_button": true,
"control_after_refresh": "first",
"timeout": 5000,
"max_retries": 3
}
}
Remote Properties:
route: API endpoint URLrefresh: Refresh interval in secondsresponse_key: Key to extract data from responsequery_params: Query parametersrefresh_button: Show refresh buttoncontrol_after_refresh: Control behavior after refreshtimeout: Request timeoutmax_retries: Maximum retry attempts
Output Definitions
Basic Output
{
"index": 0,
"name": "IMAGE",
"type": "IMAGE",
"is_list": false,
"tooltip": "Generated image"
}
List Output
{
"index": 1,
"name": "IMAGES",
"type": "IMAGE",
"is_list": true,
"tooltip": "List of generated images"
}
Output with Options
{
"index": 2,
"name": "MASK",
"type": "MASK",
"is_list": false,
"options": ["invert", "normalize"],
"tooltip": "Generated mask with options"
}
Node Metadata
Basic Metadata
{
"name": "MyCustomNode",
"display_name": "My Custom Node",
"description": "A custom node for image processing",
"category": "image/processing",
"output_node": false,
"python_module": "my_custom_node"
}
Advanced Metadata
{
"name": "AdvancedNode",
"display_name": "Advanced Processing Node",
"description": "Advanced image processing with AI features",
"category": "image/ai",
"output_node": true,
"python_module": "advanced_node",
"deprecated": false,
"experimental": true
}
Complete Example
Basic Node Definition
{
"inputs": {
"image": {
"type": "IMAGE",
"name": "Image",
"tooltip": "Input image to process"
},
"strength": {
"type": "FLOAT",
"name": "Strength",
"default": 0.5,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"display": "slider",
"tooltip": "Processing strength"
},
"enable_feature": {
"type": "BOOLEAN",
"name": "Enable Feature",
"default": true,
"tooltip": "Enable advanced feature"
},
"sampler": {
"type": "COMBO",
"name": "Sampler",
"default": "euler",
"options": ["euler", "dpm_2", "lms"],
"tooltip": "Sampling method"
}
},
"outputs": [
{
"index": 0,
"name": "IMAGE",
"type": "IMAGE",
"is_list": false,
"tooltip": "Processed image"
},
{
"index": 1,
"name": "INFO",
"type": "STRING",
"is_list": false,
"tooltip": "Processing information"
}
],
"name": "ImageProcessor",
"display_name": "Image Processor",
"description": "Process images with various effects",
"category": "image/processing",
"output_node": false,
"python_module": "image_processor"
}
Advanced Node Definition
{
"inputs": {
"model": {
"type": "MODEL",
"name": "Model",
"tooltip": "AI model for processing"
},
"prompt": {
"type": "STRING",
"name": "Prompt",
"default": "A beautiful landscape",
"multiline": true,
"dynamicPrompts": true,
"placeholder": "Enter your prompt here",
"tooltip": "Text prompt for generation"
},
"steps": {
"type": "INT",
"name": "Steps",
"default": 20,
"min": 1,
"max": 100,
"step": 1,
"display": "slider",
"control_after_generate": true,
"tooltip": "Number of sampling steps"
},
"cfg_scale": {
"type": "FLOAT",
"name": "CFG Scale",
"default": 7.0,
"min": 0.1,
"max": 20.0,
"step": 0.1,
"round": 2,
"display": "slider",
"tooltip": "Classifier-free guidance scale"
},
"sampler": {
"type": "COMBO",
"name": "Sampler",
"default": "euler",
"options": ["euler", "dpm_2", "dpm_2_ancestral", "lms"],
"tooltip": "Sampling method"
},
"seed": {
"type": "INT",
"name": "Seed",
"default": 0,
"min": 0,
"max": 2147483647,
"step": 1,
"display": "number",
"tooltip": "Random seed for generation"
},
"batch_size": {
"type": "INT",
"name": "Batch Size",
"default": 1,
"min": 1,
"max": 8,
"step": 1,
"display": "slider",
"tooltip": "Number of images to generate"
},
"enable_advanced": {
"type": "BOOLEAN",
"name": "Advanced Options",
"default": false,
"tooltip": "Enable advanced options"
},
"advanced_strength": {
"type": "FLOAT",
"name": "Advanced Strength",
"default": 0.8,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"display": "slider",
"advanced": true,
"tooltip": "Advanced processing strength"
},
"remote_options": {
"type": "COMBO",
"name": "Remote Options",
"default": "default",
"remote": {
"route": "/api/options",
"refresh": 30,
"response_key": "data",
"query_params": {
"category": "advanced"
},
"refresh_button": true,
"control_after_refresh": "first",
"timeout": 5000,
"max_retries": 3
},
"tooltip": "Options loaded from remote API"
}
},
"outputs": [
{
"index": 0,
"name": "IMAGE",
"type": "IMAGE",
"is_list": false,
"tooltip": "Generated image"
},
{
"index": 1,
"name": "IMAGES",
"type": "IMAGE",
"is_list": true,
"tooltip": "List of generated images"
},
{
"index": 2,
"name": "INFO",
"type": "STRING",
"is_list": false,
"tooltip": "Generation information"
}
],
"name": "AdvancedGenerator",
"display_name": "Advanced Image Generator",
"description": "Advanced AI image generation with multiple options",
"category": "image/generation",
"output_node": true,
"python_module": "advanced_generator",
"deprecated": false,
"experimental": false
}
Best Practices
Input Design
# Input Design Best Practices
## Input Organization
- **Logical grouping**: Group related inputs together
- **Default values**: Provide sensible defaults
- **Tooltips**: Include helpful descriptions
- **Validation**: Use min/max for numeric inputs
- **Advanced options**: Mark advanced features as advanced
## Input Types
- **Appropriate types**: Use the right input type for the data
- **User experience**: Consider how users will interact with inputs
- **Validation**: Provide clear validation rules
- **Feedback**: Give users feedback on input values
Output Design
# Output Design Best Practices
## Output Clarity
- **Clear names**: Use descriptive output names
- **Type consistency**: Ensure output types match expectations
- **List handling**: Use is_list appropriately
- **Tooltips**: Provide output descriptions
## Output Organization
- **Logical order**: Order outputs logically
- **Index management**: Use consistent indexing
- **Type safety**: Ensure output types are correct
- **Documentation**: Document output structure
Metadata Quality
# Metadata Quality Best Practices
## Node Information
- **Clear names**: Use descriptive node names
- **Helpful descriptions**: Explain what the node does
- **Appropriate categories**: Use correct category classification
- **Version information**: Include version and status information
## Documentation
- **Comprehensive descriptions**: Document all features
- **Usage examples**: Provide usage examples
- **Troubleshooting**: Include troubleshooting information
- **Updates**: Keep documentation current
Validation
Schema Validation
# Validate node definition JSON
# Using JSON Schema validator
python -m jsonschema node_definition.json schema.json
# Using online validator
# Upload to JSON Schema validator website
Common Validation Issues
# Common Validation Issues
## Input Issues
- **Missing required fields**: Ensure all required fields are present
- **Invalid types**: Use correct input types
- **Invalid values**: Check min/max values and constraints
- **Missing tooltips**: Include helpful tooltips
## Output Issues
- **Missing outputs**: Ensure outputs are defined
- **Invalid output types**: Use correct output types
- **Index conflicts**: Avoid duplicate output indices
- **Missing descriptions**: Include output descriptions
## Metadata Issues
- **Missing required fields**: Include all required metadata
- **Invalid categories**: Use valid category names
- **Inconsistent naming**: Ensure consistent naming
- **Missing descriptions**: Include node descriptions
Important Notes
Schema Benefits
# Schema Benefits
## For Developers
- **Standardized structure**: Consistent node definition format
- **Type safety**: JSON Schema validation
- **Documentation**: Self-documenting node definitions
- **Compatibility**: Ensures compatibility across ComfyUI versions
## For Users
- **Clear interface**: Well-defined input/output structure
- **Validation**: Input validation and error handling
- **Documentation**: Built-in help and tooltips
- **Consistency**: Consistent user experience across nodes
Implementation Timeline
# Implementation Timeline
## Node Definition
1. **Basic structure**: 10-15 minutes
2. **Input definition**: 15-30 minutes
3. **Output definition**: 10-15 minutes
4. **Metadata**: 5-10 minutes
## Advanced Features
1. **Lazy evaluation**: 5-10 minutes
2. **Remote data**: 15-30 minutes
3. **Advanced inputs**: 10-20 minutes
4. **Validation**: 10-15 minutes
Basic Node Structure
Python Server Side
class MyCustomNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input_name": ("TYPE", {"default": "default_value"}),
},
"optional": {
"optional_input": ("TYPE", {"default": "default_value"}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"extra_pnginfo": "EXTRA_PNGINFO",
}
}
RETURN_TYPES = ("OUTPUT_TYPE",)
RETURN_NAMES = ("output_name",)
FUNCTION = "process"
CATEGORY = "Custom/My Category"
def process(self, input_name, optional_input=None, unique_id=None, extra_pnginfo=None):
# Process the inputs
result = do_something(input_name)
return (result,)
Key Components
INPUT_TYPES
- required: Mandatory inputs
- optional: Optional inputs
- hidden: System inputs (unique_id, extra_pnginfo)
- widgets: Custom UI widgets (buttons, sliders, etc.)
Widget Types
- STRING: Text input
- INT: Integer input
- FLOAT: Float input
- BOOLEAN: Checkbox (acts as button when clicked)
- IMAGE: Image tensor
- MODEL: Model object
- CONDITIONING: Conditioning data
- LATENT: Latent space data
RETURN_TYPES
- Define output types in order
- Must match the number of return values
RETURN_NAMES
- Human-readable names for outputs
- Must match RETURN_TYPES count
FUNCTION
- Name of the method that processes inputs
- Must be a method of the class
CATEGORY
- Where the node appears in the UI
- Format: "Category/Subcategory"
Widget Implementation
Boolean Widget (Button-like)
"widgets": {
"my_button": ("BOOLEAN", {"default": False, "label": "Click Me"}),
}
String Widget
"widgets": {
"my_text": ("STRING", {"default": "", "multiline": True}),
}
Choice Widget
"widgets": {
"my_choice": ("STRING", {"default": "option1", "choices": ["option1", "option2"]}),
}
JavaScript Client Side
Basic Extension
app.registerExtension({
name: "MyExtension",
async nodeCreated(node) {
if (node.comfyClass === "MyCustomNode") {
// Custom UI setup
}
}
});
Custom Widgets
function setupCustomUI(node) {
// Create custom elements
const button = document.createElement("button");
button.textContent = "Custom Button";
// Add to node
const container = node.widgets[0].options.el.parentElement;
container.appendChild(button);
// Handle events
button.addEventListener("click", () => {
// Handle button click
});
}
ComfyUI Datatypes - Complete Reference
Purpose
Datatypes provide strong typing on the client side, preventing workflows from passing wrong data types between nodes. The JavaScript client generally prevents connections between incompatible types.
Primitive Datatypes
INT
"my_int": ("INT", {"default": 10, "min": 0, "max": 100})
- Python type:
int - Required params:
default - Optional params:
min,max
FLOAT
"my_float": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01})
- Python type:
float - Required params:
default - Optional params:
min,max,step
STRING
"my_string": ("STRING", {"default": "hello", "multiline": True})
- Python type:
str - Required params:
default - Optional params:
multiline,placeholder
BOOLEAN
"my_bool": ("BOOLEAN", {"default": False, "label_on": "On", "label_off": "Off"})
- Python type:
bool - Required params:
default - Optional params:
label_on,label_off
Tensor Datatypes
IMAGE
"image": ("IMAGE", {})
- Python type:
torch.Tensor - Shape:
[B, H, W, C](batch, height, width, channels) - Channels: Generally 3 for RGB
- Range: 0-1 (float32)
LATENT
"latent": ("LATENT", {})
- Python type:
dict - Key:
samples(torch.Tensor) - Shape:
[B, C, H, W](batch, channels, height, width) - Channels: Generally 4 for SD models
- Size: Height/width are 1/8 of image size
MASK
"mask": ("MASK", {})
- Python type:
torch.Tensor - Shape:
[H, W]or[B, C, H, W]
AUDIO
"audio": ("AUDIO", {})
- Python type:
dict - Keys:
waveform(torch.Tensor),sample_rate(int) - Shape:
[B, C, T](batch, channels, time) - Channels: 1 for mono, 2 for stereo
Special Datatypes
COMBO (Dropdown)
"my_choice": (["option1", "option2", "option3"], {})
- Python type:
str - Format: List of options, first is default
- Dynamic: Can be generated at runtime
- Example:
"ckpt_name": (folder_paths.get_filename_list("checkpoints"), )
NOISE
"noise": ("NOISE", {})
- Python type: Custom object
- Method:
generate_noise(self, input_latent: Tensor) -> Tensor - Property:
seed: Optional[int] - Purpose: Noise source for sampling
SAMPLER
"sampler": ("SAMPLER", {})
- Python type: Custom object
- Method:
samplemethod - Purpose: Sampling algorithm
SIGMAS
"sigmas": ("SIGMAS", {})
- Python type:
torch.Tensor - Shape:
[steps+1] - Purpose: Noise levels for each sampling step
- Example:
[14.6146, 10.7468, 8.0815, ..., 0.0000]
GUIDER
"guider": ("GUIDER", {})
- Python type: Callable object
- Method:
__call__(*args, **kwargs) - Input: Noisy latents
[B, C, H, W] - Output: Noise prediction
[B, C, H, W]
Model Datatypes
MODEL
"model": ("MODEL", {})
- Purpose: Stable diffusion model
- Python type: Custom model object
CLIP
"clip": ("CLIP", {})
- Purpose: CLIP text encoder
- Python type: Custom CLIP object
VAE
"vae": ("VAE", {})
- Purpose: Variational Autoencoder
- Python type: Custom VAE object
CONDITIONING
"conditioning": ("CONDITIONING", {})
- Purpose: Text conditioning data
- Python type: List of conditioning objects
Additional Parameters
Standard Parameters
| Key | Description | Applicable Types |
|---|---|---|
default |
Default value | All types |
min |
Minimum value | INT, FLOAT |
max |
Maximum value | INT, FLOAT |
step |
Increment amount | FLOAT |
label_on |
Label when True | BOOLEAN |
label_off |
Label when False | BOOLEAN |
multiline |
Multiline text box | STRING |
placeholder |
Placeholder text | STRING |
Advanced Parameters
| Key | Description | Purpose |
|---|---|---|
defaultInput |
Default to input socket | Widget override |
forceInput |
Force input socket | Widget override |
dynamicPrompts |
Evaluate dynamic prompts | STRING |
lazy |
Lazy evaluation | All types |
rawLink |
Receive link instead of value | Node expansion |
Examples
Basic Input Types
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE", {}),
"strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0}),
"steps": ("INT", {"default": 20, "min": 1, "max": 100}),
"enabled": ("BOOLEAN", {"default": True}),
"mode": (["fast", "quality", "balanced"], {}),
},
"optional": {
"text": ("STRING", {"default": "", "multiline": True}),
"mask": ("MASK", {}),
}
}
Advanced Input Types
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL", {}),
"clip": ("CLIP", {}),
"conditioning": ("CONDITIONING", {}),
"latent": ("LATENT", {}),
"noise": ("NOISE", {}),
"sampler": ("SAMPLER", {}),
"sigmas": ("SIGMAS", {}),
}
}
Custom Datatypes
Defining Custom Types
# Custom datatype example
class CustomDataType:
def __init__(self, value):
self.value = value
def process(self):
return self.value * 2
# Usage in INPUT_TYPES
"custom_input": ("CUSTOM", {})
Type Validation
@classmethod
def VALIDATE_INPUTS(cls, custom_input):
if not isinstance(custom_input, CustomDataType):
return "Input must be CustomDataType"
return True
Best Practices
Type Safety
- Use appropriate types: Choose the most specific type
- Validate inputs: Use VALIDATE_INPUTS for custom types
- Handle edge cases: Consider None values and type mismatches
Performance
- Minimal tensors: Avoid unnecessary tensor operations
- Efficient processing: Use appropriate tensor shapes
- Memory management: Handle large tensors carefully
User Experience
- Clear labels: Use descriptive parameter names
- Sensible defaults: Choose reasonable default values
- Input validation: Provide helpful error messages
Data Types
Common Input Types
- MODEL: Model object
- IMAGE: Image tensor [batch, height, width, channels]
- LATENT: Latent space tensor
- CONDITIONING: Text conditioning
- CLIP: CLIP model
- VAE: VAE model
- STRING: Text string
- INT: Integer
- FLOAT: Float
- BOOLEAN: Boolean
Tensor Shapes
- IMAGE:
[batch, height, width, channels](float32, 0-1 range) - LATENT:
[batch, channels, height, width](float32) - CONDITIONING: List of conditioning objects
Working with Images, Latents, and Masks
Key Concepts
- torch.Tensor: Core data structure for all tensor types
- Single output: Return
(tensor,)not(tensor)- trailing comma required - Shape awareness: Understanding tensor dimensions is crucial
- PIL integration: Converting between torch.Tensor and PIL.Image
Images (IMAGE)
Basic Properties
# IMAGE tensor properties
image_tensor.shape # [B, H, W, C] where C=3 for RGB
image_tensor.dtype # torch.float32
image_tensor.range # 0.0 to 1.0
Working with PIL.Image
from PIL import Image, ImageOps
import torch
# Convert PIL.Image to torch.Tensor
def pil_to_tensor(pil_image):
# Convert to RGB if needed
if pil_image.mode != 'RGB':
pil_image = pil_image.convert('RGB')
# Convert to tensor
tensor = torch.from_numpy(np.array(pil_image)).float() / 255.0
return tensor.unsqueeze(0) # Add batch dimension
# Convert torch.Tensor to PIL.Image
def tensor_to_pil(tensor):
# Remove batch dimension if present
if tensor.dim() == 4:
tensor = tensor.squeeze(0)
# Convert to numpy and scale to 0-255
array = (tensor * 255.0).clamp(0, 255).byte().numpy()
return Image.fromarray(array)
# Example usage
def process_image(self, image_tensor):
# Convert to PIL for processing
pil_image = tensor_to_pil(image_tensor)
# Process with PIL
processed = ImageOps.invert(pil_image)
# Convert back to tensor
result = pil_to_tensor(processed)
return (result,)
Shape Considerations
# IMAGE is channel-last: [B, H, W, C]
image_tensor = torch.randn(1, 512, 512, 3) # Batch, Height, Width, Channels
# Some operations expect channel-first: [B, C, H, W]
# Use permute to convert
channel_first = image_tensor.permute(0, 3, 1, 2) # [B, C, H, W]
channel_last = channel_first.permute(0, 2, 3, 1) # Back to [B, H, W, C]
Masks (MASK)
Basic Properties
# MASK tensor properties
mask_tensor.shape # [B, H, W] or [H, W] (no channel dimension)
mask_tensor.dtype # torch.float32
mask_tensor.range # 0.0 to 1.0 (binary or continuous)
Understanding Mask Shapes
def process_mask(self, mask):
# Check mask shape
if len(mask.shape) == 2:
# [H, W] - single mask, add batch dimension
mask = mask.unsqueeze(0) # [1, H, W]
elif len(mask.shape) == 3:
# [B, H, W] - batch of masks
pass
else:
raise ValueError(f"Unexpected mask shape: {mask.shape}")
# Add channel dimension for operations
mask_with_channels = mask.unsqueeze(-1) # [B, H, W, 1]
return mask_with_channels
Mask Operations
def apply_mask(self, image, mask):
# Ensure compatible shapes
if len(mask.shape) == 2:
mask = mask.unsqueeze(0).unsqueeze(-1) # [1, H, W, 1]
# Apply mask to image
masked_image = image * mask
return (masked_image,)
LoadImage Node Masks
# LoadImage creates masks from alpha channel
# - Alpha values normalized to [0,1]
# - Values inverted (1 - alpha)
# - Default mask [1, 64, 64] for images without alpha
Latents (LATENT)
Basic Properties
# LATENT is a dictionary
latent_dict = {
'samples': torch.Tensor, # [B, C, H, W] where C=4
# Other keys may include masks, etc.
}
# Access the latent tensor
latent_tensor = latent_dict['samples']
latent_tensor.shape # [B, C, H, W] where C=4
Working with Latents
def process_latent(self, latent):
# Extract the samples tensor
samples = latent['samples'] # [B, C, H, W]
# Process the latent
processed_samples = samples * 0.5 # Example operation
# Return new latent dict
return ({
'samples': processed_samples
},)
Latent vs Image Shapes
# LATENT: channel-first [B, C, H, W]
latent_tensor = torch.randn(1, 4, 64, 64) # Batch, Channels, Height, Width
# IMAGE: channel-last [B, H, W, C]
image_tensor = torch.randn(1, 512, 512, 3) # Batch, Height, Width, Channels
# Convert between formats
def latent_to_image_shape(latent):
# [B, C, H, W] -> [B, H, W, C]
return latent.permute(0, 2, 3, 1)
def image_to_latent_shape(image):
# [B, H, W, C] -> [B, C, H, W]
return image.permute(0, 3, 1, 2)
Common Operations
Tensor Manipulation
def manipulate_tensor(self, tensor):
# Add dimensions
tensor = tensor.unsqueeze(0) # Add batch dimension
tensor = tensor.unsqueeze(-1) # Add channel dimension
# Remove dimensions
tensor = tensor.squeeze(0) # Remove batch dimension
tensor = tensor.squeeze(-1) # Remove channel dimension
# Permute dimensions
tensor = tensor.permute(0, 3, 1, 2) # [B,H,W,C] -> [B,C,H,W]
return (tensor,)
Shape Matching
def match_shapes(self, tensor1, tensor2):
# Ensure same batch size
if tensor1.shape[0] != tensor2.shape[0]:
# Broadcast or repeat as needed
pass
# Ensure compatible dimensions
if len(tensor1.shape) != len(tensor2.shape):
# Add or remove dimensions
pass
return (tensor1, tensor2)
Memory Management
def efficient_processing(self, tensor):
# Use torch.no_grad() for inference
with torch.no_grad():
result = self.model(tensor)
# Clear cache if needed
torch.cuda.empty_cache()
return (result,)
Best Practices
Shape Validation
def validate_tensor_shape(self, tensor, expected_shape):
if tensor.shape != expected_shape:
raise ValueError(f"Expected shape {expected_shape}, got {tensor.shape}")
return True
Type Safety
def safe_tensor_operation(self, tensor):
# Check tensor properties
if not isinstance(tensor, torch.Tensor):
raise TypeError("Input must be torch.Tensor")
if tensor.dtype != torch.float32:
tensor = tensor.float()
return tensor
Performance Tips
def optimize_tensor_operations(self, tensor):
# Use in-place operations when possible
tensor.mul_(0.5) # In-place multiplication
# Use appropriate data types
tensor = tensor.half() # Use float16 if precision allows
# Use efficient operations
result = torch.nn.functional.relu(tensor) # More efficient than manual ReLU
return (result,)
Common Pitfalls
Shape Mismatches
# ❌ Wrong - shape mismatch
image = torch.randn(1, 512, 512, 3) # [B, H, W, C]
mask = torch.randn(1, 512, 512) # [B, H, W]
result = image * mask # Error: shapes don't match
# ✅ Correct - match shapes
image = torch.randn(1, 512, 512, 3) # [B, H, W, C]
mask = torch.randn(1, 512, 512, 1) # [B, H, W, 1]
result = image * mask # Works: [B, H, W, C] * [B, H, W, 1]
Dimension Confusion
# ❌ Wrong - confusing dimensions
tensor = torch.randn(1, 4, 64, 64) # Is this [B, C, H, W] or [B, H, W, C]?
# ✅ Correct - be explicit
latent = torch.randn(1, 4, 64, 64) # [B, C, H, W] for latent
image = torch.randn(1, 64, 64, 3) # [B, H, W, C] for image
Return Format
# ❌ Wrong - missing trailing comma
def process(self, input):
return (output) # This is a tuple with one element, not a tuple
# ✅ Correct - proper tuple
def process(self, input):
return (output,) # This is a tuple with one element
Node Lifecycle - How ComfyUI Loads Custom Nodes
Loading Process
- ComfyUI starts → Scans
custom_nodesdirectory - Finds Python modules → Directories with
__init__.py - Attempts import → Executes
__init__.py - Checks exports → Looks for
NODE_CLASS_MAPPINGS - Loads nodes → Makes them available in ComfyUI
- Error handling → Reports failures but continues
Module Structure
custom_nodes/
└── my_custom_node/ # Python module (directory)
├── __init__.py # Required - module entry point
├── my_node.py # Node implementations
├── requirements.txt # Dependencies
└── web/ # Client-side code (optional)
└── js/
└── my_extension.js
__init__.py - Module Entry Point
Basic Structure
from .my_node import MyCustomNode
NODE_CLASS_MAPPINGS = {
"My Custom Node": MyCustomNode,
}
__all__ = ["NODE_CLASS_MAPPINGS"]
Complete Example
from .my_node import MyCustomNode, AnotherNode
NODE_CLASS_MAPPINGS = {
"My Custom Node": MyCustomNode,
"Another Node": AnotherNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"My Custom Node": "🎯 My Custom Node",
"Another Node": "📊 Another Node",
}
WEB_DIRECTORY = "./web/js"
__all__ = [
'NODE_CLASS_MAPPINGS',
'NODE_DISPLAY_NAME_MAPPINGS',
'WEB_DIRECTORY'
]
Required Exports
NODE_CLASS_MAPPINGS
- Purpose: Maps node names to classes
- Format:
{"Node Name": NodeClass} - Uniqueness: Names must be unique across entire ComfyUI install
- Required: Must be present for module to be recognized
NODE_DISPLAY_NAME_MAPPINGS (Optional)
- Purpose: Human-readable display names
- Format:
{"Node Name": "Display Name"} - Default: Uses NODE_CLASS_MAPPINGS keys if omitted
- Emoji support:
"🎯 My Node"for visual distinction
WEB_DIRECTORY (Optional)
- Purpose: Path to client-side JavaScript files
- Format:
"./web/js"(relative to module) - File types: Only
.jsfiles served - Convention: Use
jssubdirectory
Error Handling
Import Failures
- ComfyUI continues: Other modules still load
- Console output: Check Python console for errors
- Module status: Failed modules reported in logs
Common Issues
# ❌ Wrong - missing __init__.py
custom_nodes/my_node.py
# ✅ Correct - proper module structure
custom_nodes/my_node/
├── __init__.py
└── my_node.py
Development Workflow
1. Create Module Structure
mkdir custom_nodes/my_custom_node
touch custom_nodes/my_custom_node/__init__.py
2. Implement Nodes
# my_custom_node/my_node.py
class MyCustomNode:
# ... node implementation
3. Register in __init__.py
# my_custom_node/__init__.py
from .my_node import MyCustomNode
NODE_CLASS_MAPPINGS = {
"My Custom Node": MyCustomNode,
}
4. Test Loading
- Restart ComfyUI after changes
- Check console for import errors
- Verify nodes appear in Add Node menu
Advanced Module Structure
Multiple Node Files
# __init__.py
from .image_nodes import ImageProcessor, ImageFilter
from .text_nodes import TextProcessor, TextAnalyzer
NODE_CLASS_MAPPINGS = {
"Image Processor": ImageProcessor,
"Image Filter": ImageFilter,
"Text Processor": TextProcessor,
"Text Analyzer": TextAnalyzer,
}
Client-Side Integration
# __init__.py
from .my_node import MyCustomNode
NODE_CLASS_MAPPINGS = {
"My Custom Node": MyCustomNode,
}
WEB_DIRECTORY = "./web/js"
__all__ = ['NODE_CLASS_MAPPINGS', 'WEB_DIRECTORY']
Best Practices
Module Organization
- Single responsibility: One module per feature set
- Clear naming: Descriptive module and node names
- Proper structure: Follow ComfyUI conventions
Error Prevention
- Test imports: Verify modules load correctly
- Handle exceptions: Graceful error handling
- Console monitoring: Check for import errors
Performance
- Lazy loading: Import only when needed
- Minimal dependencies: Reduce startup time
- Efficient code: Optimize node execution
Node Registration
__init__.py
from .my_node import MyCustomNode
NODE_CLASS_MAPPINGS = {
"MyCustomNode": MyCustomNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"MyCustomNode": "🎯 My Custom Node",
}
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
Advanced Features
IS_CHANGED Method
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN") # Always update
# or return hash of relevant inputs
VALIDATE_INPUTS Method
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
# Validate inputs before processing
return True
Custom JavaScript
@classmethod
def get_js(cls):
js = """
// Custom JavaScript code
function myFunction() {
// Implementation
}
"""
return js
File Structure
my_custom_node/
├── __init__.py # Node registration
├── my_node.py # Node implementation
├── requirements.txt # Dependencies
├── README.md # Documentation
└── web/ # Client-side assets (optional)
└── my_extension.js
Best Practices
1. Error Handling
try:
result = process_input(input_data)
except Exception as e:
print(f"Error in MyCustomNode: {e}")
return (None,)
2. Input Validation
def process(self, image, strength):
if image is None:
raise ValueError("Image input is required")
if not 0 <= strength <= 1:
raise ValueError("Strength must be between 0 and 1")
3. Memory Management
# Use torch.no_grad() for inference
with torch.no_grad():
result = model(input_tensor)
4. Documentation
class MyCustomNode:
"""
My Custom Node - Does something useful
Inputs:
image: Input image tensor
strength: Processing strength (0-1)
Outputs:
result: Processed image tensor
"""
Common Patterns
Image Processing Node
class ImageProcessor:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "process"
CATEGORY = "Image Processing"
def process(self, image, strength):
# Process image
processed = image * strength
return (processed,)
Model Wrapper Node
class ModelWrapper:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL",),
"input_data": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output",)
FUNCTION = "run"
CATEGORY = "Model"
def run(self, model, input_data):
with torch.no_grad():
output = model(input_data)
return (output,)
Debugging
Console Output
print(f"Debug: Processing input shape {input_tensor.shape}")
Error Messages
try:
result = process(input)
except Exception as e:
print(f"Error in {self.__class__.__name__}: {e}")
import traceback
traceback.print_exc()
Testing
Manual Testing
- Add node to workflow
- Connect inputs
- Execute workflow
- Check outputs
Unit Testing
def test_my_node():
node = MyCustomNode()
result = node.process(test_input)
assert result[0].shape == expected_shape
Distribution
Git Repository
- Clear README with installation instructions
- Proper versioning
- Example workflows
Package Structure
- Follow ComfyUI conventions
- Include requirements.txt
- Document dependencies
Step-by-Step Tutorial: Image Selector Node
Prerequisites
- Working ComfyUI installation (manual installation recommended for development)
- Working comfy-cli installation
Setting Up
cd ComfyUI/custom_nodes
comfy node scaffold
Answer the scaffold questions:
- full_name: Your name
- email: your@email.com
- github_username: your_github_username
- project_name: Your Project Name
- project_slug: your-project-slug
- project_short_description: Description of your nodes
- version: 0.0.1
- license: Choose appropriate license
- include_web_directory_for_custom_javascript: y/n
Basic Node Structure
class ImageSelector:
CATEGORY = "example"
@classmethod
def INPUT_TYPES(s):
return { "required": { "images": ("IMAGE",) } }
RETURN_TYPES = ("IMAGE",)
FUNCTION = "choose_image"
def choose_image(self, images):
brightness = list(torch.mean(image.flatten()).item() for image in images)
brightest = brightness.index(max(brightness))
result = images[brightest].unsqueeze(0)
return (result,)
Key Points
- IMAGE means image batch (single image = batch of size 1)
- Shape:
[B,H,W,C]where B=batch, H=height, W=width, C=channels unsqueeze(0)adds batch dimension- Return tuple:
(result,)- trailing comma essential
Register the Node
NODE_CLASS_MAPPINGS = {
"Example": Example,
"Image Selector": ImageSelector,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Example": "Example Node",
"Image Selector": "Image Selector",
}
Adding Options
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
"mode": (["brightest", "reddest", "greenest", "bluest"],)
}
}
def choose_image(self, images, mode):
batch_size = images.shape[0]
brightness = list(torch.mean(image.flatten()).item() for image in images)
if (mode=="brightest"):
scores = brightness
else:
channel = 0 if mode=="reddest" else (1 if mode=="greenest" else 2)
absolute = list(torch.mean(image[:,:,channel].flatten()).item() for image in images)
scores = list(absolute[i]/(brightness[i]+1e-8) for i in range(batch_size))
best = scores.index(max(scores))
result = images[best].unsqueeze(0)
return (result,)
Server-Client Communication
Send Message from Server
from server import PromptServer
def choose_image(self, images, mode):
# ... processing code ...
PromptServer.instance.send_sync("example.imageselector.textmessage", {
"message": f"Picked image {best+1}"
})
return (result,)
Client Extension
Create web/js/imageSelector.js:
app.registerExtension({
name: "example.imageselector",
async setup() {
function messageHandler(event) {
alert(event.detail.message);
}
app.api.addEventListener("example.imageselector.textmessage", messageHandler);
},
})
Update __init__.py
WEB_DIRECTORY = "./web/js"
__all__ = ['NODE_CLASS_MAPPINGS', 'WEB_DIRECTORY']
Development Workflow
- Make changes to your node code
- Restart ComfyUI server
- Reload webpage in browser
- Test workflow with your node
Complete Example Structure
my_custom_node/
├── __init__.py # Node registration + WEB_DIRECTORY
├── src/
│ └── nodes.py # Node implementations
├── web/
│ └── js/
│ └── imageSelector.js # Client-side JavaScript
├── requirements.txt # Dependencies
└── README.md # Documentation
Resources
Official Documentation
- ComfyUI GitHub: https://github.com/comfyanonymous/ComfyUI
- Custom Nodes Guide: https://github.com/comfyanonymous/ComfyUI/wiki
Example Repositories
- cookiecutter-comfy-extension
- ComfyUI-React-Extension-Template
- ComfyUI_frontend_vue_basic
Community
- ComfyUI Discord
- GitHub Issues
- Community Forums
This guide is based on ComfyUI's official documentation and community best practices.
