Skip to content

Node Replacement API#12014

Merged
comfyanonymous merged 33 commits intomasterfrom
jk/node-replace-api
Feb 15, 2026
Merged

Node Replacement API#12014
comfyanonymous merged 33 commits intomasterfrom
jk/node-replace-api

Conversation

@Kosinkadink
Copy link
Member

@Kosinkadink Kosinkadink commented Jan 22, 2026

Code snippet:

from comfy_api.latest import io, ComfyAPI

api = ComfyAPI()

await api.node_replacement.register(io.NodeReplace(
            new_node_id="Example2",
            old_node_id="Example",
            input_mapping=[
                {"new_id": "image2", "old_id": "image"},
                {"new_id": "string_field2", "set_value": "Hello world!2"},
            ],
            output_mapping=[{"new_idx": 1, "old_idx": 0}, {"new_idx": 2, "old_idx": 1}],
        ))

GET /api/node_replacements

Returns all registered node replacements. Node replacements define how to migrate from deprecated/old nodes to their newer equivalents, including how to map inputs and outputs between them.

Request

GET /api/node_replacements

No parameters required.

Response

Returns a JSON object where keys are old node IDs and values are arrays of possible replacements for that node.

Response Schema

{
  "<old_node_id>": [
    {
      "new_node_id": "string",
      "old_node_id": "string",
      "old_widget_ids": ["string", ...] | null,
      "input_mapping": [...] | null,
      "output_mapping": [...] | null
    }
  ]
}

Fields

Field Type Description
new_node_id string The ID of the replacement node
old_node_id string The ID of the deprecated/old node being replaced
old_widget_ids array | null Maps widget IDs to their relative positions (see below)
input_mapping array | null How to map inputs from the old node to the new node
output_mapping array | null How to map outputs from the old node to the new node

Widget ID Binding

The old_widget_ids field is used to bind input IDs to their relative widget indexes. This is necessary because the graph JSON file stores widget values by their relative position index, not by their ID. By providing this ordered list, the system can resolve which widget value corresponds to which input ID when performing the replacement.

For example, if old_widget_ids is ["steps", "cfg", "sampler"], then:

  • Widget at index 0 corresponds to input ID "steps"
  • Widget at index 1 corresponds to input ID "cfg"
  • Widget at index 2 corresponds to input ID "sampler"

Input Mapping

Each input mapping entry is a dictionary. The type is inferred by the keys present:

Map from old input (old_id):

{
  "new_id": "string",
  "old_id": "string"
}

Set a fixed value (set_value):

{
  "new_id": "string",
  "set_value": <any>
}
Field Type Description
new_id string The input ID on the new node
old_id string The input ID on the old node to connect (mutually exclusive with set_value)
set_value any The fixed value to use for this input (mutually exclusive with old_id)

Output Mapping

Each output mapping entry has the following structure:

{
  "new_idx": 0,
  "old_idx": 0
}
Field Type Description
new_idx integer The output index on the new node
old_idx integer The output index on the old node

Example 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}
      ]
    }
  ]
}

Registering Node Replacements

Custom node developers can register replacements using the comfy_api.latest module:

from comfy_api.latest import io, ComfyAPI

# Register a simple replacement with input and output mappings
await api.node_replacement.register(
    io.NodeReplace(
        new_node_id="NewNodeClass",
        old_node_id="OldNodeClass",
        old_widget_ids=["old_input_name", "old_param", "sampler_type"],
        input_mapping=[
            # Map old input to new input by ID
            {"new_id": "new_input_name", "old_id": "old_input_name"},
            # Set a fixed value for a new input widget
            {"new_id": "new_param", "set_value": 512},
        ],
        output_mapping=[
            # Map output at index 0 of old node to index 0 of new node
            {"new_idx": 0, "old_idx": 0},
        ],
    )
)

Classes

NodeReplace

Defines a node replacement mapping.

Parameter Type Description
new_node_id str The class name of the new replacement node
old_node_id str The class name of the deprecated node
old_widget_ids list[str] | None Ordered list binding widget IDs to their relative indexes (optional)
input_mapping list[InputMap] | None Input mappings (optional)
output_mapping list[OutputMap] | None Output mappings (optional)

InputMap

Input mappings are TypedDicts. The mapping type is inferred by the dictionary keys:

InputMapOldId - Map from old input:

Key Type Description
new_id str The input ID on the new node
old_id str The input ID on the old node

InputMapSetValue - Set a fixed value:

Key Type Description
new_id str The input ID on the new node
set_value Any The value to assign

OutputMap

Maps an output from the old node to the new node by index.

Key Type Description
new_idx int Output index on the new node
old_idx int Output index on the old node

Use Cases

  • Node Migration: When updating a custom node pack, register replacements so users can automatically upgrade their workflows
  • API Changes: Map renamed inputs/outputs to maintain backwards compatibility
  • Default Values: Provide sensible defaults for new inputs that didn't exist on the old node

Copy link
Contributor

@christian-byrne christian-byrne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the draft! Left some inline comments. Overall the old_widget_ids approach is the right solution for mapping positional widget values to names.

Summary of suggestions:

  • Rename old_widget_idsold_widget_names (they're names, not IDs)
  • Clarify InputMap.OldId — is it for input slot names or widget names? Consider splitting
  • Add docstrings explaining that old_widget_names order must match serialization order
  • Consider Cache-Control header for the GET endpoint (static per session)

@christian-byrne
Copy link
Contributor

One clarification needed in the docs: is old_widget_ids required to list all widgets in order, or can it be partial? What happens if len(old_widget_ids) != len(widgets_values) in the workflow?

Also worth clarifying that old_widget_ids refers specifically to widget inputs (values stored in widgets_values), not linkable inputs (which are stored in node.inputs[] with their names preserved).

Example scenario that could confuse devs: if a node has 3 linkable inputs and 4 widgets, old_widget_ids should have 4 entries (for the widgets), not 7.

@Kosinkadink
Copy link
Member Author

The purpose of the old_widgets_ids field is to pseudo-assign what the expected input id of the widget field is, such that all widgets can still be referred to by an input id like input links. It's why imo we should call them 'ids' instead of names, even if in the json the inputs have the 'name' property - id,display_name on backend schema -> name,label on json. Ultimately either one will work so I'm not married to either option.

I don't think there is a pragmatic use case for separating widget value transferal with input link transferal - if a link connected to a widget input slot is transferred, it should naturally follow that the value of the widget should also be transferable in that case. The name (id) of the input stored in the json is the same as what the id of the widget would be.

All input transferal should be explicit - if a widget/input on old node is not assigned to something on the new node, then it should not be carried over. So in the case that there are more widgets in the old node than new, the ones that aren't referenced directly should be ignored. Thus if the list of old_widget_ids is shorter than the amount of widgets in the old node, it can be assumed that only those first N widgets need to be enumerated with the id alias, others can be ignored.

We def need to make it clear in the docstring that the old_widget_ids are for the labeling of widgets_values in the json.

@Kosinkadink
Copy link
Member Author

I have changed UseValue (and use_value string) to SetValue (set_value string now), it feels better.

viva-jinyi added a commit to Comfy-Org/ComfyUI_frontend that referenced this pull request Feb 3, 2026
## Summary
Add infrastructure for automatic node replacement feature that allows
missing/deprecated nodes to be replaced with their newer equivalents.

## Changes
- **Types**: `NodeReplacement`, `NodeReplacementResponse` types matching
backend API spec (PR #12014)
- **Service**: `fetchNodeReplacements()` API wrapper
- **Store**: `useNodeReplacementStore` with `getReplacementFor()`,
`hasReplacement()`, `isEnabled()`
- **Setting**: `Comfy.NodeReplacement.Enabled` toggle (experimental)
- **Tests**: 11 unit tests covering store functionality

## Related
- Backend PR: Comfy-Org/ComfyUI#12014

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8364-feat-Add-node-replacement-store-and-types-2f66d73d3650816bb771c9cc6a8e1774)
by [Unito](https://www.unito.io)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Automatic node replacement is now available, allowing missing nodes to
be replaced with their newer equivalents when replacement mappings
exist.
* Added "Enable automatic node replacement" experimental setting in
Workflow preferences (enabled by default).
  * Replacement data is loaded during app initialization.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
* refactor: process isolation support for node replacement API

- Move REGISTERED_NODE_REPLACEMENTS global to NodeReplaceManager instance state
- Add NodeReplacement class to ComfyAPI_latest with async register() method
- Deprecate module-level register_node_replacement() function
- Call register_replacements() from comfy_entrypoint()

This enables pyisolate compatibility where extensions run in separate
processes and communicate via RPC. The async API allows registration
calls to cross process boundaries.

Refs: TDD-002
Amp-Thread-ID: https://ampcode.com/threads/T-019c2b33-ac55-76a9-9c6b-0246a8625f21

* fix: remove whitespace and deprecation cruft

Amp-Thread-ID: https://ampcode.com/threads/T-019c2be8-0b34-747e-b1f7-20a1a1e6c9df
@Kosinkadink Kosinkadink marked this pull request as ready for review February 13, 2026 06:25
@Kosinkadink Kosinkadink added the Core Core team dependency label Feb 13, 2026
guill
guill previously approved these changes Feb 14, 2026
Copy link
Member

@guill guill left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using is_link(input_value) instead of is_instance(input_value, list) is the only change I feel really need to be made. Added a couple other nits, but feel free to ignore them.

Approving as these changes are all trivial and I don't think there's any value in me re-reviewing if those are the only changes made.

@comfyanonymous comfyanonymous merged commit 596ed68 into master Feb 15, 2026
14 checks passed
@comfyanonymous comfyanonymous deleted the jk/node-replace-api branch February 15, 2026 10:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Core Core team dependency

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants