aimusic / ComfyUI /custom_nodes /koolook /docs /maintainers /node-versioning.md
vidfom's picture
Upload folder using huggingface_hub (part 7)
e4ab0d4 verified
|
Raw
History Blame Contribute Delete
9.6 kB
# Node versioning β€” how to change a node without breaking saved workflows
> **⚠️ Scope β€” OPT-IN for Koolook nodes, MANDATORY for forks.**
> Backward compatibility is **not** a default constraint for Koolook-created
> nodes (the root `k_*.py` custom nodes); for those, **ignore everything below**
> and make the cleanest change β€” rename/drop/reorder freely β€” *unless* the
> maintainer says `check backward compatibility`. **For anything under
> `forks/`** (upstream ports/wrappers, including the Koolook-original nodes
> exposed via `SKIP_VERSION_SUFFIX`) **these rules still apply by default** β€”
> forks keep full back-compat discipline with no request needed. See
> [`CLAUDE.md`](../../CLAUDE.md) β†’ *Change management*.
This is a hard-won list. ComfyUI's workflow format silently breaks in
several non-obvious ways when you change a node's inputs, outputs, or
class name. The rules below are what to follow whenever you touch a
**fork** node's `INPUT_TYPES` / `RETURN_TYPES` / `NODE_CLASS_MAPPINGS`,
or a **Koolook** node's once `check backward compatibility` has been
requested.
## How ComfyUI loads a saved workflow
When the frontend opens a workflow JSON, it reconstructs each node by:
1. **Looking up the node by its registered ID** (the key in
`NODE_CLASS_MAPPINGS`). If the lookup fails, the node renders as a
red "missing" stub and the workflow won't run.
2. **Mapping connections by input/output name.** Most cases.
3. **Mapping widget values by *position* in the widget array** β€” this is
the source of most breakage.
4. **Filling missing inputs with their default**, if they're declared in
`INPUT_TYPES["optional"]`. Missing *required* inputs β†’ validation
error.
## What breaks vs what doesn't
| Change | Breaks saved workflows? | Why |
|---|---|---|
| Add a new input to `INPUT_TYPES["required"]` | **YES** | Old workflow has no value for it β†’ validation fails |
| Add a new input to `INPUT_TYPES["optional"]` *at the end* | **safe** | Missing inputs use defaults |
| Add a new widget *between* existing widgets | **YES** | Widget array indices shift β†’ values land in wrong widgets |
| Add a new widget *at the end* | mostly safe | Old workflows just don't have that index β†’ uses default |
| Rename an existing input | **YES** | Saved value can't map by name |
| Rename the class / node ID | **YES** | Lookup fails entirely |
| Reorder existing combo options | **YES** for indexed serialization | Some workflows store combo as index, not string |
| Add new combo option *at the end* | **safe** | Existing string values still match |
| Remove an input | soft break | Saved value silently dropped, may lose semantic meaning |
| Change `RETURN_TYPES` order or types | **YES** | Downstream connections targeting `output[1]` now hit wrong type |
| Change a default value | "stealth break" | Old workflows have the old default baked in; new workflows get the new default β€” silent behavior divergence |
> "*Field gets messed up*" almost always = **widget position shift**.
> ComfyUI serializes widgets as `widgets_values: [val0, val1, val2]`.
> Insert a new widget at index 1 β†’ every subsequent value shifts one
> slot β†’ wrong values land in wrong widgets.
## The five rules
1. **Never rename a registered node ID.** When you really must, register
both old and new IDs pointing to the same class for a few releases
(deprecation alias), then remove the old one with a CHANGELOG note.
2. **Append new inputs/widgets to the END.** Never insert in the middle.
Order in `INPUT_TYPES` dict matters because Python preserves insertion
order and the frontend uses it.
3. **New inputs go in `INPUT_TYPES["optional"]` with a default**, not in
`required`. Old workflows that don't have the field will use the
default; new workflows can wire it up.
4. **Never reorder combo options.** Append new options. If you need to
remove one, leave it as a hidden alias or deprecation stub for a
release first.
5. **Treat `RETURN_TYPES` as immutable.** If you need to change outputs,
make a new node ID β€” `MyNodeV2`. The Comfy ecosystem (KJ Nodes, Was
Node Suite, Crystools) does this constantly.
## When you can't avoid a breaking change
| Pattern | Example |
|---|---|
| **Suffix-version a new node** | `EasyAIPipeline` β†’ `EasyAIPipelineV2`. Both register, both work. |
| **Mark old as deprecated in display name** | Old: `Easy AI Pipeline (deprecated, use V2)` β€” display name change is fine, ID is what matters |
| **Document migration in CHANGELOG** | "Old `EasyAIPipeline` retained; new pipeline work should use `EasyAIPipelineV2` which adds X/Y/Z" |
| **Keep deprecated nodes for β‰₯2 minor versions** before removing |
This is exactly the pattern used for `EasyResize` β†’ `EasyResize_Koolook`
in v0.1.6 (see [`../../CHANGELOG.md`](../../CHANGELOG.md)). Both IDs
register, both load, the bare-name one's display says "deprecated", and
the bare-name one will be removed in a future major release once the
deprecation has had time to propagate.
## Concrete pattern β€” the rename + alias
```python
# In whichever k_*.py file holds the class:
class MyNode:
""" ... existing implementation, unchanged ... """
# ...
# Register BOTH IDs against the same class:
NODE_CLASS_MAPPINGS = {
"MyNode_Koolook": MyNode, # new canonical ID
"MyNode": MyNode, # legacy alias (deprecated)
}
NODE_DISPLAY_NAME_MAPPINGS = {
"MyNode_Koolook": "My Node (Koolook)",
"MyNode": "My Node (deprecated, use 'My Node (Koolook)')",
}
```
That's it. Saved workflows that reference `MyNode` keep loading. New
workflows pick `MyNode_Koolook` from the search. The bare-name display
nudges users to switch.
## Concrete pattern β€” adding a new optional input
Suppose `EasyAIPipeline` currently has these inputs:
```python
"required": {
"shot_duration": ("INT", {...}),
"seed": ("INT", {...}),
"base_path": ("STRING", {...}),
"shot_name": ("STRING", {...}),
"ai_method": ("STRING", {...}),
"version": ("INT", {...}),
},
```
You want to add a new `pass_name` field. **Do not** add it to `required`
between existing fields β€” that breaks every saved workflow. Instead:
```python
"required": { ... unchanged ... },
"optional": {
"pass_name": ("STRING", {"default": "", "tooltip": "..."}),
},
```
- Old workflows that don't have `pass_name` use the default `""`.
- New workflows that do have it use the value the user sets.
- Output behavior must remain backward-compatible when `pass_name=""`
(i.e. equivalent to the pre-change behavior). If `pass_name=""` would
change the output filename, you need a `MyNodeV2` instead.
## Concrete pattern β€” output change
Suppose `EasyAIPipeline` currently returns `(output_path,)` and you want
to add a `(output_path, output_dir)` tuple. That's a `RETURN_TYPES`
change β†’ downstream connections targeting `output[1]` would hit the new
output unexpectedly (or just exist where they didn't before).
Don't modify the existing class. Make a new one:
```python
class EasyAIPipeline_V2(EasyAIPipeline):
"""Same as EasyAIPipeline, plus emits the resolved output directory."""
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("output_path", "output_dir")
# ... reuse or override the execute method as needed ...
NODE_CLASS_MAPPINGS = {
"EasyAIPipeline": EasyAIPipeline, # unchanged
"EasyAIPipeline_V2": EasyAIPipeline_V2, # new
}
```
## Anti-patterns to refuse β€” *for forks, or after `check backward compatibility`*
These are anti-patterns for **any fork node**, and for a **Koolook node only
once the maintainer has asked** to preserve backward compatibility. For a
Koolook node with no such request, every one of them is the *correct* clean
change β€” append-vs-insert is the lone exception below.
- ❌ "I'll just add the field in the middle for clarity, the dict order
doesn't matter." It does. Append. *(This one is worth following even
without a back-compat request β€” a mid-list widget insertion silently
scrambles the `widgets_values` of any workflow already on disk, including
your own dev graphs, for zero benefit. Appending is free.)*
- ❌ "I'll rename the input to be clearer." For a fork node, or any node
under a back-compat request: add a new optional input with the clearer
name and deprecate the old one in display only. For a Koolook node with
no request, **just rename it.**
- ❌ "It's a small change, no need to bump the node version." For a fork
node, or under a back-compat request, a surface change that could break a
saved workflow β†’ new node ID. For a Koolook node with no request, change
in place and note it in the CHANGELOG.
- ❌ "Nobody's using the old node yet." For a fork node, or under a
back-compat request, you can't assume that. For a Koolook node with no
request you *do* assume it β€” clean change, move on.
## Tooling β€” what we have, what's still missing
- βœ… **Workflow-fixture smoke test** β€” `tools/preflight_release.py`
check `workflows` walks `tests/workflows/*.json`, extracts node IDs
by both heuristic and AST-extracted set, and reports any reference
that no longer resolves. The `preflight-release` skill in
`.claude/skills/` runs all four checks before a release.
- ⏳ **`node-api-change` agent skill** β€” a check that would run before
any edit to `INPUT_TYPES` / `RETURN_TYPES` / class names, diff
against the rules above, and refuse unsafe changes. Same shape as
`license-pre-check`. Not built yet; scoped for the next time someone
changes a node's input/output surface.