ComfyUI V1 → V3 Migration Guide
Migrate existing V1 nodes to the modern V3 API. V3 uses classmethods, typed inputs/outputs, and ComfyExtension registration.
Migration Checklist
- Change base class to
io.ComfyNode
- Replace
INPUT_TYPES() with define_schema() returning io.Schema
- Rename execution function to
execute and make it a @classmethod
- Replace return tuples with
io.NodeOutput(...)
- Replace
IS_CHANGED with fingerprint_inputs
- Replace
VALIDATE_INPUTS with validate_inputs
- Convert
check_lazy_status to @classmethod
- Replace
NODE_CLASS_MAPPINGS with ComfyExtension + comfy_entrypoint()
- Access hidden inputs via
cls.hidden instead of kwargs
- Remove
__init__ methods (no instance state in V3)
Side-by-Side Comparison
V1 (Before)
import torch
class ImageInvertV1:
CATEGORY = "image"
FUNCTION = "invert"
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
OUTPUT_TOOLTIPS = ("The inverted image",)
DESCRIPTION = "Inverts image colors"
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"strength": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.01,
}),
},
"optional": {
"mask": ("MASK",),
},
"hidden": {
"unique_id": "UNIQUE_ID",
},
}
@classmethod
def IS_CHANGED(s, image, strength, mask=None, unique_id=None):
return strength
@classmethod
def VALIDATE_INPUTS(s, image, strength, mask=None, unique_id=None):
if strength < 0:
return "Strength must be non-negative"
return True
def invert(self, image, strength, mask=None, unique_id=None):
inverted = 1.0 - image
result = image * (1 - strength) + inverted * strength
if mask is not None:
result = image * (1 - mask.unsqueeze(-1)) + result * mask.unsqueeze(-1)
return (result,)
NODE_CLASS_MAPPINGS = {"ImageInvertV1": ImageInvertV1}
NODE_DISPLAY_NAME_MAPPINGS = {"ImageInvertV1": "Invert Image"}
V3 (After)
import torch
from typing_extensions import override
from comfy_api.latest import ComfyExtension, io
class ImageInvertV3(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ImageInvertV3",
display_name="Invert Image",
description="Inverts image colors",
category="image",
inputs=[
io.Image.Input("image"),
io.Float.Input("strength", default=1.0, min=0.0, max=1.0, step=0.01),
io.Mask.Input("mask", optional=True),
],
outputs=[
io.Image.Output("IMAGE", tooltip="The inverted image"),
],
hidden=[io.Hidden.unique_id],
)
@classmethod
def fingerprint_inputs(cls, image, strength, mask=None):
return strength
@classmethod
def validate_inputs(cls, image, strength, mask=None):
if strength < 0:
return "Strength must be non-negative"
return True
@classmethod
def execute(cls, image, strength, mask=None):
node_id = cls.hidden.unique_id # access hidden via cls.hidden
inverted = 1.0 - image
result = image * (1 - strength) + inverted * strength
if mask is not None:
result = image * (1 - mask.unsqueeze(-1)) + result * mask.unsqueeze(-1)
return io.NodeOutput(result)
class MyExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [ImageInvertV3]
async def comfy_entrypoint() -> MyExtension:
return MyExtension()
Property Mapping
| V1 Property |
V3 Equivalent |
CATEGORY = "image" |
io.Schema(category="image") |
FUNCTION = "my_func" |
Always execute (fixed name) |
RETURN_TYPES = ("IMAGE",) |
outputs=[io.Image.Output()] |
RETURN_NAMES = ("image",) |
outputs=[io.Image.Output(display_name="image")] |
OUTPUT_TOOLTIPS = ("tip",) |
outputs=[io.Image.Output(tooltip="tip")] |
OUTPUT_NODE = True |
io.Schema(is_output_node=True) |
DEPRECATED = True |
io.Schema(is_deprecated=True) |
EXPERIMENTAL = True |
io.Schema(is_experimental=True) |
API_NODE = True |
io.Schema(is_api_node=True) |
NOT_IDEMPOTENT = True |
io.Schema(not_idempotent=True) |
DESCRIPTION = "..." |
io.Schema(description="...") |
SEARCH_ALIASES = [...] |
io.Schema(search_aliases=[...]) |
INPUT_IS_LIST = True |
io.Schema(is_input_list=True) |
OUTPUT_IS_LIST = (True,) |
io.Image.Output(is_output_list=True) |
DEV_ONLY = True |
io.Schema(is_dev_only=True) |
ESSENTIALS_CATEGORY = "Basic" |
io.Schema(essentials_category="Basic") |
Input Type Mapping
| V1 Input |
V3 Input |
("IMAGE",) |
io.Image.Input("id") |
("MASK",) |
io.Mask.Input("id") |
("LATENT",) |
io.Latent.Input("id") |
("MODEL",) |
io.Model.Input("id") |
("CLIP",) |
io.Clip.Input("id") |
("VAE",) |
io.Vae.Input("id") |
("CONDITIONING",) |
io.Conditioning.Input("id") |
("INT", {"default": 0, ...}) |
io.Int.Input("id", default=0, ...) |
("FLOAT", {"default": 1.0, ...}) |
io.Float.Input("id", default=1.0, ...) |
("STRING", {"multiline": True}) |
io.String.Input("id", multiline=True) |
("BOOLEAN", {"default": True}) |
io.Boolean.Input("id", default=True) |
(["opt1", "opt2"],) |
io.Combo.Input("id", options=["opt1", "opt2"]) |
("CONTROL_NET",) |
io.ControlNet.Input("id") |
("CLIP_VISION",) |
io.ClipVision.Input("id") |
("CLIP_VISION_OUTPUT",) |
io.ClipVisionOutput.Input("id") |
("STYLE_MODEL",) |
io.StyleModel.Input("id") |
("GLIGEN",) |
io.Gligen.Input("id") |
("UPSCALE_MODEL",) |
io.UpscaleModel.Input("id") |
("AUDIO",) |
io.Audio.Input("id") |
("VIDEO",) |
io.Video.Input("id") |
("SAMPLER",) |
io.Sampler.Input("id") |
("SIGMAS",) |
io.Sigmas.Input("id") |
("NOISE",) |
io.Noise.Input("id") |
("GUIDER",) |
io.Guider.Input("id") |
("HOOKS",) |
io.Hooks.Input("id") |
("LORA_MODEL",) |
io.LoraModel.Input("id") |
("MESH",) |
io.Mesh.Input("id") |
("VOXEL",) |
io.Voxel.Input("id") |
("FILE_3D",) |
io.File3DAny.Input("id") |
("FILE_3D_GLB",) |
io.File3DGLB.Input("id") |
("SVG",) |
io.SVG.Input("id") |
("COLOR",) |
io.Color.Input("id") |
("BOUNDING_BOX",) |
io.BoundingBox.Input("id") |
("CURVE",) |
io.Curve.Input("id") |
("LATENT_UPSCALE_MODEL",) |
io.LatentUpscaleModel.Input("id") |
("MODEL_PATCH",) |
io.ModelPatch.Input("id") |
("HOOK_KEYFRAMES",) |
io.HookKeyframes.Input("id") |
("AUDIO_ENCODER",) |
io.AudioEncoder.Input("id") |
("AUDIO_ENCODER_OUTPUT",) |
io.AudioEncoderOutput.Input("id") |
("TRACKS",) |
io.Tracks.Input("id") |
("LOSS_MAP",) |
io.LossMap.Input("id") |
("TIMESTEPS_RANGE",) |
io.TimestepsRange.Input("id") |
("LATENT_OPERATION",) |
io.LatentOperation.Input("id") |
("WEBCAM",) |
io.Webcam.Input("id") |
("PHOTOMAKER",) |
io.Photomaker.Input("id") |
("WAN_CAMERA_EMBEDDING",) |
io.WanCameraEmbedding.Input("id") |
("LOAD_3D",) |
io.Load3D.Input("id") |
("LOAD_3D_ANIMATION",) |
io.Load3DAnimation.Input("id") |
("LOAD3D_CAMERA",) |
io.Load3DCamera.Input("id") |
("FILE_3D_GLTF",) |
io.File3DGLTF.Input("id") |
("FILE_3D_FBX",) |
io.File3DFBX.Input("id") |
("FILE_3D_OBJ",) |
io.File3DOBJ.Input("id") |
("FILE_3D_STL",) |
io.File3DSTL.Input("id") |
("FILE_3D_USDZ",) |
io.File3DUSDZ.Input("id") |
("FILE_3D_PLY",) |
io.File3DPLY.Input("id") |
("FILE_3D_SPLAT",) |
io.File3DSPLAT.Input("id") |
("FILE_3D_SPZ",) |
io.File3DSPZ.Input("id") |
("FILE_3D_KSPLAT",) |
io.File3DKSPLAT.Input("id") |
("FILE_3D_SPLAT_ANY",) |
io.File3DSplatAny.Input("id") |
("FILE_3D_POINT_CLOUD_ANY",) |
io.File3DPointCloudAny.Input("id") |
("SPLAT",) |
io.Splat.Input("id") |
("LOAD3D_MODEL_INFO",) |
io.Load3DModelInfo.Input("id") |
("BACKGROUND_REMOVAL",) |
io.BackgroundRemoval.Input("id") |
("DICT",) |
io.Dict.Input("id") |
("ARRAY",) |
io.Array.Input("id") |
("COLORS",) |
io.Colors.Input("id") |
("BOUNDING_BOXES",) |
io.BoundingBoxes.Input("id") |
("RANGE",) |
io.Range.Input("id") |
("HISTOGRAM",) |
io.Histogram.Input("id") |
("POINT",) |
io.Point.Input("id") |
("FACE_ANALYSIS",) |
io.FaceAnalysis.Input("id") |
("BBOX",) |
io.BBOX.Input("id") |
("SEGS",) |
io.SEGS.Input("id") |
("IMAGECOMPARE",) |
io.ImageCompare.Input("id") |
("*",) |
io.AnyType.Input("id") or io.MultiType.Input("id", types=[...]) |
Method Migration
Execute Method
# V1: instance method with custom name
class V1Node:
FUNCTION = "process"
def process(self, image, value):
return (result,)
# V3: classmethod named "execute", returns NodeOutput
class V3Node(io.ComfyNode):
@classmethod
def execute(cls, image, value):
return io.NodeOutput(result)
IS_CHANGED → fingerprint_inputs
# V1
@classmethod
def IS_CHANGED(s, **kwargs):
return float("NaN") # always re-execute
# V3
@classmethod
def fingerprint_inputs(cls, **kwargs):
import time
return time.time() # always re-execute
VALIDATE_INPUTS → validate_inputs
# V1
@classmethod
def VALIDATE_INPUTS(s, input_types=None, **kwargs):
return True
# V3
@classmethod
def validate_inputs(cls, input_types=None, **kwargs):
return True
check_lazy_status
# V1: instance method
def check_lazy_status(self, **kwargs):
return ["input_name"]
# V3: classmethod
@classmethod
def check_lazy_status(cls, **kwargs):
return ["input_name"]
Hidden Inputs
# V1: received as kwargs
def execute(self, image, unique_id=None, prompt=None):
node_id = unique_id
# V3: accessed via cls.hidden
@classmethod
def execute(cls, image):
node_id = cls.hidden.unique_id
prompt = cls.hidden.prompt
Registration Migration
# V1
NODE_CLASS_MAPPINGS = {
"Node1": Node1Class,
"Node2": Node2Class,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Node1": "Node One",
"Node2": "Node Two",
}
WEB_DIRECTORY = "./js"
# V3
from typing_extensions import override
from comfy_api.latest import ComfyExtension, io
class MyExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [Node1Class, Node2Class]
@override
async def on_load(self):
# Optional: initialization logic
pass
async def comfy_entrypoint() -> MyExtension:
return MyExtension()
# WEB_DIRECTORY still works the same way for JS extensions
WEB_DIRECTORY = "./js"
Output Node Migration
# V1
class V1SaveNode:
RETURN_TYPES = ()
OUTPUT_NODE = True
FUNCTION = "save"
def save(self, images, prefix):
# ... save logic ...
return {"ui": {"images": results}}
# V3
from comfy_api.latest import io, ui
class V3SaveNode(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="V3SaveNode",
display_name="Save",
category="image",
is_output_node=True,
inputs=[
io.Image.Input("images"),
io.String.Input("prefix", default="output"),
],
outputs=[],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
)
@classmethod
def execute(cls, images, prefix):
saved = ui.ImageSaveHelper.get_save_images_ui(images, prefix, cls=cls)
return io.NodeOutput(ui=saved)
Key Gotchas
- No instance state: V3 execute is a classmethod. Don't store state on
self. Use external storage if needed.
- Fixed method name: Always
execute, never custom names.
- Hidden access changed: Use
cls.hidden.prompt not function parameters.
- Return type changed:
io.NodeOutput(val) not (val,).
- Optional inputs: Use
=None default in execute params, not separate "optional" dict.
- Async support: V3 execute can be
async def execute(cls, ...).
See Also
comfyui-node-basics - V3 node fundamentals
comfyui-node-packaging - Project structure
comfyui-node-lifecycle - Execution lifecycle differences
1---2name: comfyui-node-migration3description: ComfyUI V1 to V3 node migration - converting legacy nodes to the V3 API. Use when migrating existing custom nodes from V1 to V3, understanding differences between API versions, or modernizing node code.4---56# ComfyUI V1 → V3 Migration Guide78Migrate existing V1 nodes to the modern V3 API. V3 uses classmethods, typed inputs/outputs, and `ComfyExtension` registration.910## Migration Checklist11121. Change base class to `io.ComfyNode`132. Replace `INPUT_TYPES()` with `define_schema()` returning `io.Schema`143. Rename execution function to `execute` and make it a `@classmethod`154. Replace return tuples with `io.NodeOutput(...)`165. Replace `IS_CHANGED` with `fingerprint_inputs`176. Replace `VALIDATE_INPUTS` with `validate_inputs`187. Convert `check_lazy_status` to `@classmethod`198. Replace `NODE_CLASS_MAPPINGS` with `ComfyExtension` + `comfy_entrypoint()`209. Access hidden inputs via `cls.hidden` instead of kwargs2110. Remove `__init__` methods (no instance state in V3)2223## Side-by-Side Comparison2425### V1 (Before)2627```python28import torch2930class ImageInvertV1:31 CATEGORY = "image"32 FUNCTION = "invert"33 RETURN_TYPES = ("IMAGE",)34 RETURN_NAMES = ("image",)35 OUTPUT_TOOLTIPS = ("The inverted image",)36 DESCRIPTION = "Inverts image colors"3738 @classmethod39 def INPUT_TYPES(s):40 return {41 "required": {42 "image": ("IMAGE",),43 "strength": ("FLOAT", {44 "default": 1.0,45 "min": 0.0,46 "max": 1.0,47 "step": 0.01,48 }),49 },50 "optional": {51 "mask": ("MASK",),52 },53 "hidden": {54 "unique_id": "UNIQUE_ID",55 },56 }5758 @classmethod59 def IS_CHANGED(s, image, strength, mask=None, unique_id=None):60 return strength6162 @classmethod63 def VALIDATE_INPUTS(s, image, strength, mask=None, unique_id=None):64 if strength < 0:65 return "Strength must be non-negative"66 return True6768 def invert(self, image, strength, mask=None, unique_id=None):69 inverted = 1.0 - image70 result = image * (1 - strength) + inverted * strength71 if mask is not None:72 result = image * (1 - mask.unsqueeze(-1)) + result * mask.unsqueeze(-1)73 return (result,)7475NODE_CLASS_MAPPINGS = {"ImageInvertV1": ImageInvertV1}76NODE_DISPLAY_NAME_MAPPINGS = {"ImageInvertV1": "Invert Image"}77```7879### V3 (After)8081```python82import torch83from typing_extensions import override84from comfy_api.latest import ComfyExtension, io8586class ImageInvertV3(io.ComfyNode):87 @classmethod88 def define_schema(cls):89 return io.Schema(90 node_id="ImageInvertV3",91 display_name="Invert Image",92 description="Inverts image colors",93 category="image",94 inputs=[95 io.Image.Input("image"),96 io.Float.Input("strength", default=1.0, min=0.0, max=1.0, step=0.01),97 io.Mask.Input("mask", optional=True),98 ],99 outputs=[100 io.Image.Output("IMAGE", tooltip="The inverted image"),101 ],102 hidden=[io.Hidden.unique_id],103 )104105 @classmethod106 def fingerprint_inputs(cls, image, strength, mask=None):107 return strength108109 @classmethod110 def validate_inputs(cls, image, strength, mask=None):111 if strength < 0:112 return "Strength must be non-negative"113 return True114115 @classmethod116 def execute(cls, image, strength, mask=None):117 node_id = cls.hidden.unique_id # access hidden via cls.hidden118119 inverted = 1.0 - image120 result = image * (1 - strength) + inverted * strength121 if mask is not None:122 result = image * (1 - mask.unsqueeze(-1)) + result * mask.unsqueeze(-1)123 return io.NodeOutput(result)124125126class MyExtension(ComfyExtension):127 @override128 async def get_node_list(self) -> list[type[io.ComfyNode]]:129 return [ImageInvertV3]130131async def comfy_entrypoint() -> MyExtension:132 return MyExtension()133```134135## Property Mapping136137| V1 Property | V3 Equivalent |138|---|---|139| `CATEGORY = "image"` | `io.Schema(category="image")` |140| `FUNCTION = "my_func"` | Always `execute` (fixed name) |141| `RETURN_TYPES = ("IMAGE",)` | `outputs=[io.Image.Output()]` |142| `RETURN_NAMES = ("image",)` | `outputs=[io.Image.Output(display_name="image")]` |143| `OUTPUT_TOOLTIPS = ("tip",)` | `outputs=[io.Image.Output(tooltip="tip")]` |144| `OUTPUT_NODE = True` | `io.Schema(is_output_node=True)` |145| `DEPRECATED = True` | `io.Schema(is_deprecated=True)` |146| `EXPERIMENTAL = True` | `io.Schema(is_experimental=True)` |147| `API_NODE = True` | `io.Schema(is_api_node=True)` |148| `NOT_IDEMPOTENT = True` | `io.Schema(not_idempotent=True)` |149| `DESCRIPTION = "..."` | `io.Schema(description="...")` |150| `SEARCH_ALIASES = [...]` | `io.Schema(search_aliases=[...])` |151| `INPUT_IS_LIST = True` | `io.Schema(is_input_list=True)` |152| `OUTPUT_IS_LIST = (True,)` | `io.Image.Output(is_output_list=True)` |153| `DEV_ONLY = True` | `io.Schema(is_dev_only=True)` |154| `ESSENTIALS_CATEGORY = "Basic"` | `io.Schema(essentials_category="Basic")` |155156## Input Type Mapping157158| V1 Input | V3 Input |159|---|---|160| `("IMAGE",)` | `io.Image.Input("id")` |161| `("MASK",)` | `io.Mask.Input("id")` |162| `("LATENT",)` | `io.Latent.Input("id")` |163| `("MODEL",)` | `io.Model.Input("id")` |164| `("CLIP",)` | `io.Clip.Input("id")` |165| `("VAE",)` | `io.Vae.Input("id")` |166| `("CONDITIONING",)` | `io.Conditioning.Input("id")` |167| `("INT", {"default": 0, ...})` | `io.Int.Input("id", default=0, ...)` |168| `("FLOAT", {"default": 1.0, ...})` | `io.Float.Input("id", default=1.0, ...)` |169| `("STRING", {"multiline": True})` | `io.String.Input("id", multiline=True)` |170| `("BOOLEAN", {"default": True})` | `io.Boolean.Input("id", default=True)` |171| `(["opt1", "opt2"],)` | `io.Combo.Input("id", options=["opt1", "opt2"])` |172| `("CONTROL_NET",)` | `io.ControlNet.Input("id")` |173| `("CLIP_VISION",)` | `io.ClipVision.Input("id")` |174| `("CLIP_VISION_OUTPUT",)` | `io.ClipVisionOutput.Input("id")` |175| `("STYLE_MODEL",)` | `io.StyleModel.Input("id")` |176| `("GLIGEN",)` | `io.Gligen.Input("id")` |177| `("UPSCALE_MODEL",)` | `io.UpscaleModel.Input("id")` |178| `("AUDIO",)` | `io.Audio.Input("id")` |179| `("VIDEO",)` | `io.Video.Input("id")` |180| `("SAMPLER",)` | `io.Sampler.Input("id")` |181| `("SIGMAS",)` | `io.Sigmas.Input("id")` |182| `("NOISE",)` | `io.Noise.Input("id")` |183| `("GUIDER",)` | `io.Guider.Input("id")` |184| `("HOOKS",)` | `io.Hooks.Input("id")` |185| `("LORA_MODEL",)` | `io.LoraModel.Input("id")` |186| `("MESH",)` | `io.Mesh.Input("id")` |187| `("VOXEL",)` | `io.Voxel.Input("id")` |188| `("FILE_3D",)` | `io.File3DAny.Input("id")` |189| `("FILE_3D_GLB",)` | `io.File3DGLB.Input("id")` |190| `("SVG",)` | `io.SVG.Input("id")` |191| `("COLOR",)` | `io.Color.Input("id")` |192| `("BOUNDING_BOX",)` | `io.BoundingBox.Input("id")` |193| `("CURVE",)` | `io.Curve.Input("id")` |194| `("LATENT_UPSCALE_MODEL",)` | `io.LatentUpscaleModel.Input("id")` |195| `("MODEL_PATCH",)` | `io.ModelPatch.Input("id")` |196| `("HOOK_KEYFRAMES",)` | `io.HookKeyframes.Input("id")` |197| `("AUDIO_ENCODER",)` | `io.AudioEncoder.Input("id")` |198| `("AUDIO_ENCODER_OUTPUT",)` | `io.AudioEncoderOutput.Input("id")` |199| `("TRACKS",)` | `io.Tracks.Input("id")` |200| `("LOSS_MAP",)` | `io.LossMap.Input("id")` |201| `("TIMESTEPS_RANGE",)` | `io.TimestepsRange.Input("id")` |202| `("LATENT_OPERATION",)` | `io.LatentOperation.Input("id")` |203| `("WEBCAM",)` | `io.Webcam.Input("id")` |204| `("PHOTOMAKER",)` | `io.Photomaker.Input("id")` |205| `("WAN_CAMERA_EMBEDDING",)` | `io.WanCameraEmbedding.Input("id")` |206| `("LOAD_3D",)` | `io.Load3D.Input("id")` |207| `("LOAD_3D_ANIMATION",)` | `io.Load3DAnimation.Input("id")` |208| `("LOAD3D_CAMERA",)` | `io.Load3DCamera.Input("id")` |209| `("FILE_3D_GLTF",)` | `io.File3DGLTF.Input("id")` |210| `("FILE_3D_FBX",)` | `io.File3DFBX.Input("id")` |211| `("FILE_3D_OBJ",)` | `io.File3DOBJ.Input("id")` |212| `("FILE_3D_STL",)` | `io.File3DSTL.Input("id")` |213| `("FILE_3D_USDZ",)` | `io.File3DUSDZ.Input("id")` |214| `("FILE_3D_PLY",)` | `io.File3DPLY.Input("id")` |215| `("FILE_3D_SPLAT",)` | `io.File3DSPLAT.Input("id")` |216| `("FILE_3D_SPZ",)` | `io.File3DSPZ.Input("id")` |217| `("FILE_3D_KSPLAT",)` | `io.File3DKSPLAT.Input("id")` |218| `("FILE_3D_SPLAT_ANY",)` | `io.File3DSplatAny.Input("id")` |219| `("FILE_3D_POINT_CLOUD_ANY",)` | `io.File3DPointCloudAny.Input("id")` |220| `("SPLAT",)` | `io.Splat.Input("id")` |221| `("LOAD3D_MODEL_INFO",)` | `io.Load3DModelInfo.Input("id")` |222| `("BACKGROUND_REMOVAL",)` | `io.BackgroundRemoval.Input("id")` |223| `("DICT",)` | `io.Dict.Input("id")` |224| `("ARRAY",)` | `io.Array.Input("id")` |225| `("COLORS",)` | `io.Colors.Input("id")` |226| `("BOUNDING_BOXES",)` | `io.BoundingBoxes.Input("id")` |227| `("RANGE",)` | `io.Range.Input("id")` |228| `("HISTOGRAM",)` | `io.Histogram.Input("id")` |229| `("POINT",)` | `io.Point.Input("id")` |230| `("FACE_ANALYSIS",)` | `io.FaceAnalysis.Input("id")` |231| `("BBOX",)` | `io.BBOX.Input("id")` |232| `("SEGS",)` | `io.SEGS.Input("id")` |233| `("IMAGECOMPARE",)` | `io.ImageCompare.Input("id")` |234| `("*",)` | `io.AnyType.Input("id")` or `io.MultiType.Input("id", types=[...])` |235236## Method Migration237238### Execute Method239240```python241# V1: instance method with custom name242class V1Node:243 FUNCTION = "process"244 def process(self, image, value):245 return (result,)246247# V3: classmethod named "execute", returns NodeOutput248class V3Node(io.ComfyNode):249 @classmethod250 def execute(cls, image, value):251 return io.NodeOutput(result)252```253254### IS_CHANGED → fingerprint_inputs255256```python257# V1258@classmethod259def IS_CHANGED(s, **kwargs):260 return float("NaN") # always re-execute261262# V3263@classmethod264def fingerprint_inputs(cls, **kwargs):265 import time266 return time.time() # always re-execute267```268269### VALIDATE_INPUTS → validate_inputs270271```python272# V1273@classmethod274def VALIDATE_INPUTS(s, input_types=None, **kwargs):275 return True276277# V3278@classmethod279def validate_inputs(cls, input_types=None, **kwargs):280 return True281```282283### check_lazy_status284285```python286# V1: instance method287def check_lazy_status(self, **kwargs):288 return ["input_name"]289290# V3: classmethod291@classmethod292def check_lazy_status(cls, **kwargs):293 return ["input_name"]294```295296### Hidden Inputs297298```python299# V1: received as kwargs300def execute(self, image, unique_id=None, prompt=None):301 node_id = unique_id302303# V3: accessed via cls.hidden304@classmethod305def execute(cls, image):306 node_id = cls.hidden.unique_id307 prompt = cls.hidden.prompt308```309310## Registration Migration311312```python313# V1314NODE_CLASS_MAPPINGS = {315 "Node1": Node1Class,316 "Node2": Node2Class,317}318NODE_DISPLAY_NAME_MAPPINGS = {319 "Node1": "Node One",320 "Node2": "Node Two",321}322WEB_DIRECTORY = "./js"323324# V3325from typing_extensions import override326from comfy_api.latest import ComfyExtension, io327328class MyExtension(ComfyExtension):329 @override330 async def get_node_list(self) -> list[type[io.ComfyNode]]:331 return [Node1Class, Node2Class]332333 @override334 async def on_load(self):335 # Optional: initialization logic336 pass337338async def comfy_entrypoint() -> MyExtension:339 return MyExtension()340341# WEB_DIRECTORY still works the same way for JS extensions342WEB_DIRECTORY = "./js"343```344345## Output Node Migration346347```python348# V1349class V1SaveNode:350 RETURN_TYPES = ()351 OUTPUT_NODE = True352 FUNCTION = "save"353354 def save(self, images, prefix):355 # ... save logic ...356 return {"ui": {"images": results}}357358# V3359from comfy_api.latest import io, ui360361class V3SaveNode(io.ComfyNode):362 @classmethod363 def define_schema(cls):364 return io.Schema(365 node_id="V3SaveNode",366 display_name="Save",367 category="image",368 is_output_node=True,369 inputs=[370 io.Image.Input("images"),371 io.String.Input("prefix", default="output"),372 ],373 outputs=[],374 hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],375 )376377 @classmethod378 def execute(cls, images, prefix):379 saved = ui.ImageSaveHelper.get_save_images_ui(images, prefix, cls=cls)380 return io.NodeOutput(ui=saved)381```382383## Key Gotchas3843851. **No instance state**: V3 execute is a classmethod. Don't store state on `self`. Use external storage if needed.3862. **Fixed method name**: Always `execute`, never custom names.3873. **Hidden access changed**: Use `cls.hidden.prompt` not function parameters.3884. **Return type changed**: `io.NodeOutput(val)` not `(val,)`.3895. **Optional inputs**: Use `=None` default in execute params, not separate `"optional"` dict.3906. **Async support**: V3 execute can be `async def execute(cls, ...)`.391392## See Also393394- `comfyui-node-basics` - V3 node fundamentals395- `comfyui-node-packaging` - Project structure396- `comfyui-node-lifecycle` - Execution lifecycle differences