Skip to content

Conversation

@Kosinkadink
Copy link
Member

@Kosinkadink Kosinkadink commented Jan 22, 2026

This draft PR can now be used to work on the frontend.

Everything here is subject to change if we find anything to add before merging.

Unrelated to frontend, I will have claude code refactor the classes in _node_replace.py to use pydantic that should be able to cover our use case perfectly. Not a blocker.

Can also add some validation potentially, but also not a blocker for initial work.


Note: 99% of the PR text below was generated by Claude Code and I have reviewed it to be correct. Here is some of my personal notes:

  • Input replacements should be explicit - if it is not in the schema, it should not be assumed to be connected.
    • Reasoning - there could be overlap between input ids of old/new nodes, but that does not mean the id should be automatically connected to in the new node.
    • This does mean if the input id is actually the same between old and new node, it would still need to be registered.
  • For outputs, outputs should be implicit - for anything not explicitly mapped, assume it will be carried over at the next compatible index..
  • The schema below accounts for input widgets potentially needing to receive specific values (including DynamicCombo, etc) to maintain expected behavior of the node being replaced.
    • use_value assign_type is made for this case

Code snippet:

from comfy_api.latest import node_replace

node_replace.register_node_replacement(node_replace.NodeReplace(
            new_node_id="Example2",
            old_node_id="Example",
            input_mapping=[
                node_replace.InputMap(new_id="image2", assign=node_replace.InputMap.OldId("image")),
                node_replace.InputMap(new_id="string_field2", assign=node_replace.InputMap.UseValue("Hello world!2"))
                ],
            output_mapping=[node_replace.OutputMap(new_idx=1, old_idx=0), node_replace.OutputMap(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 has the following structure:

{
  "new_id": "string",
  "assign": {
    "assign_type": "old_id" | "set_value",
    "old_id": "string",       // only if assign_type is "old_id"
    "value": <any>            // only if assign_type is "set_value"
  }
}
Field Type Description
new_id string The input ID on the new node
assign.assign_type string Either "old_id" (map from old input) or "set_value" (use a fixed value)
assign.old_id string The input ID on the old node to connect (when assign_type is "old_id")
assign.value any The fixed value to use for this input (when assign_type is "set_value")

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",
          "assign": {
            "assign_type": "old_id",
            "old_id": "model"
          }
        },
        {
          "new_id": "steps",
          "assign": {
            "assign_type": "old_id",
            "old_id": "num_steps"
          }
        },
        {
          "new_id": "scheduler",
          "assign": {
            "assign_type": "set_value",
            "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 node_replace

# Register a simple replacement with input and output mappings
node_replace.register_node_replacement(
    node_replace.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
            node_replace.InputMap(
                new_id="new_input_name",
                assign=node_replace.InputMap.OldId("old_input_name")
            ),
            # Set a fixed value for a new input widget
            node_replace.InputMap(
                new_id="new_param",
                assign=node_replace.InputMap.SetValue(512)
            ),
        ],
        output_mapping=[
            # Map output at index 0 of old node to index 0 of new node
            node_replace.OutputMap(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

Maps an input from the old node to the new node.

Parameter Type Description
new_id str The input ID on the new node
assign InputMap.OldId | InputMap.SetValue How to assign the value

InputMap.OldId

Connect an input from the old node to the new node.

Parameter Type Description
old_id str The input ID on the old node

InputMap.SetValue

Use a fixed value for the new node's input widget.

Parameter Type Description
value Any The value to assign

OutputMap

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

Parameter 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)

new_node_id: str,
old_node_id: str,
old_widget_ids: list[str] | None=None,
input_mapping: list[InputMap] | None=None,
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider renaming to old_widget_names — these are ordered widget names, not IDs.


class OldId(_Assign):
"""
Connect the input of the old node with given id to new node when replacing.
Copy link
Contributor

Choose a reason for hiding this comment

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

The docstring says "input of the old node with given id" but old_id could refer to either an input slot name or a widget name (from old_widget_ids). Consider splitting into OldInputName and OldWidgetName subclasses, or clarify in the docstring which it references.

Defines a possible node replacement, mapping inputs and outputs of the old node to the new node.

Also supports assigning specific values to the input widgets of the new node.
"""
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider adding a docstring to NodeReplace explaining that old_widget_ids must be in the same order as the old node's widgets_values serialization order. This is crucial for custom node devs to understand — the positional mapping is the key insight.

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants