Skip to main content
The Node Replacement API allows custom node developers to define migration paths from deprecated nodes to their newer equivalents. When you update or rename nodes, users can automatically upgrade their workflows.

When to use

  • Changing node class names: You changed a node’s class name (use DISPLAY_NAME for display name changes instead)
  • Merging nodes: Multiple nodes consolidated into one (e.g., Load3DAnimation merged into Load3D)
  • Refactoring inputs: Input names or types changed between versions
  • Fixing typos: Correcting node names without breaking existing workflows

Where to register replacements

Register replacements during your extension’s on_load lifecycle hook. Create a dedicated file (e.g., node_replacements.py) in your custom node package:
my_custom_nodes/
├── __init__.py
├── nodes.py
└── node_replacements.py   # Register replacements here

Complete example

Here’s a full example showing how to structure node replacements in a custom node package:
# node_replacements.py
from comfy_api.latest import ComfyExtension, io, ComfyAPI

api = ComfyAPI()


async def register_my_replacements():
    """Register all node replacements for this package."""
    
    # Simple rename - no input changes needed
    await api.node_replacement.register(io.NodeReplace(
        new_node_id="MyNewNode",
        old_node_id="MyOldNode",
    ))
    
    # Complex replacement with input mapping
    await api.node_replacement.register(io.NodeReplace(
        new_node_id="MyImprovedSampler",
        old_node_id="MyOldSampler",
        old_widget_ids=["steps", "cfg"],
        input_mapping=[
            {"new_id": "model", "old_id": "model"},
            {"new_id": "num_steps", "old_id": "steps"},
            {"new_id": "guidance", "old_id": "cfg"},
            {"new_id": "scheduler", "set_value": "normal"},  # New input with default
        ],
        output_mapping=[
            {"new_idx": 0, "old_idx": 0},
        ],
    ))


class MyExtension(ComfyExtension):
    async def on_load(self) -> None:
        await register_my_replacements()

    async def get_node_list(self) -> list[type[io.ComfyNode]]:
        return []  # No nodes defined here, just replacements


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

Core examples

ComfyUI core uses node replacements for built-in node migrations. Here are real examples from comfy_extras/nodes_replacements.py:

Simple node merge

When Load3DAnimation was merged into Load3D:
await api.node_replacement.register(io.NodeReplace(
    new_node_id="Load3D",
    old_node_id="Load3DAnimation",
))

Typo fix

Correcting a typo in SDV_img2vid_ConditioningSVD_img2vid_Conditioning:
await api.node_replacement.register(io.NodeReplace(
    new_node_id="SVD_img2vid_Conditioning",
    old_node_id="SDV_img2vid_Conditioning",
))

Input renaming with defaults

Replacing ImageScaleBy with ResizeImageMaskNode:
await api.node_replacement.register(io.NodeReplace(
    new_node_id="ResizeImageMaskNode",
    old_node_id="ImageScaleBy",
    old_widget_ids=["upscale_method", "scale_by"],
    input_mapping=[
        {"new_id": "input", "old_id": "image"},
        {"new_id": "resize_type", "set_value": "scale by multiplier"},
        {"new_id": "resize_type.multiplier", "old_id": "scale_by"},
        {"new_id": "scale_method", "old_id": "upscale_method"},
    ],
))

Autogrow input mapping

For nodes using Autogrow (dynamic inputs), use dot notation:
await api.node_replacement.register(io.NodeReplace(
    new_node_id="BatchImagesNode",
    old_node_id="ImageBatch",
    input_mapping=[
        {"new_id": "images.image0", "old_id": "image1"},
        {"new_id": "images.image1", "old_id": "image2"},
    ],
))

NodeReplace parameters

ParameterTypeDescription
new_node_idstrClass name of the replacement node
old_node_idstrClass name of the deprecated node
old_widget_idslist[str] | NoneOrdered list binding widget IDs to their relative indexes
input_mappinglist | NoneHow to map inputs from old to new node
output_mappinglist | NoneHow to map outputs from old to new node

Input mapping

Each input mapping entry defines how an input transfers from the old node to the new one. Map from old input:
{"new_id": "model", "old_id": "model"}
Set a fixed value:
{"new_id": "scheduler", "set_value": "normal"}
Map dynamic/autogrow inputs (use dot notation):
{"new_id": "images.image0", "old_id": "image1"}

Output mapping

Output mappings use index-based references:
{"new_idx": 0, "old_idx": 0}  # Map first output
{"new_idx": 1, "old_idx": 0}  # Old output 0 -> new output 1

Widget ID binding

The old_widget_ids field maps widget IDs to their positional indexes. This is required because workflow JSON stores widget values by position, not ID.
old_widget_ids=["steps", "cfg", "sampler"]
# Widget at index 0 = "steps"
# Widget at index 1 = "cfg"
# Widget at index 2 = "sampler"

REST API

Retrieve all registered replacements:
GET /api/node_replacements
Response:
{
  "OldSamplerNode": [
    {
      "new_node_id": "NewSamplerNode",
      "old_node_id": "OldSamplerNode",
      "old_widget_ids": ["num_steps", "cfg_scale", "sampler_name"],
      "input_mapping": [
        {"new_id": "model", "old_id": "model"},
        {"new_id": "steps", "old_id": "num_steps"},
        {"new_id": "scheduler", "set_value": "normal"}
      ],
      "output_mapping": [
        {"new_idx": 0, "old_idx": 0}
      ]
    }
  ]
}

Frontend behavior

When a workflow contains a deprecated node, the frontend:
  1. Fetches replacements from GET /api/node_replacements
  2. Detects nodes matching old_node_id
  3. Prompts the user to upgrade
  4. Applies input/output mappings automatically
  5. Preserves connections and widget values
See the frontend implementation: