Files
malkuthro-ComfyUI-Koolook/docs/maintainers/node-versioning.md
T
malkuthroandClaude Opus 4.7 413b153702 docs(maintainers): refresh stale references + add conventions.md
Audit triggered while debugging the v0.2.0 release surfaced drift in
the maintainer docs vs current code: dead curated_defaults.json
references (file replaced by starter_preset.json in PR #72), the
v1_0_1 fork example (removed in v0.1.5), and a "tooling we don't have
yet" entry that's now covered by tools/preflight_release.py. Adds a
new conventions.md for cross-cutting rules — currently the
Mermaid-for-diagrams convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 10:38:07 +02:00

178 lines
7.8 KiB
Markdown

# Node versioning — how to change a node without breaking saved workflows
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
`INPUT_TYPES`, `RETURN_TYPES`, or `NODE_CLASS_MAPPINGS`.
## 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
- ❌ "I'll just add the field in the middle for clarity, the dict order
doesn't matter." It does. Append.
- ❌ "I'll rename the input to be clearer." Don't. Add a new optional
input with the clearer name; deprecate the old one in display only.
- ❌ "It's a small change, no need to bump the node version." If the
input/output surface changes in a way that could break a saved
workflow → new node ID. Period.
- ❌ "Nobody's using the old node yet." You don't actually know. Default
to alias-then-deprecate; cost is one extra dict entry.
## 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.