* refactor(nodes): drop keyframe_batch alias from Easy Image Batch The keyframe_batch input was a deprecated alias of keyframes_insert, added in 0.4.1 during PR #246 review to keep pre-rename workflows loading. The old name only ever existed in unreleased dev builds before the rename, so no published workflow depends on it and the alias only cluttered the node with a second IMAGE input. Remove the input, its parameter, and the alias-routing fallback; the node now exposes a single keyframes_insert insert input. Tests and the behavior spec updated; CHANGELOG notes the removal under [Unreleased]. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(policy): make backward compatibility opt-in for Koolook nodes Invert the default: backward compatibility is no longer a standing constraint for Koolook-created nodes (the root k_*.py custom nodes). By default, make the cleanest change — rename/drop/reorder IDs, inputs, and outputs freely, no deprecation aliases or _v2 suffixes. Compatibility with saved workflows is only preserved when the maintainer explicitly says "check backward compatibility". Forks are explicitly excluded: anything under forks/ (including the Koolook-original nodes exposed via SKIP_VERSION_SUFFIX) keeps full back-compat discipline by default, no request needed. The __koolook_vX_Y_Z collision suffix always applies. Scoped across the binding instruction files and skills: CLAUDE.md, copilot-instructions, node-versioning.md, versioning.md, and the preflight-release / add-external-fork / tdd skills. The preflight workflows check is reframed as a drift detector for Koolook IDs and a hard back-compat gate for fork IDs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: clarify keyframe_batch alias removal * fix: retain keyframe_batch compatibility alias --------- Co-authored-by: AI Machine <ai.machine@x.baconcph.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
9.4 KiB
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_*.pycustom nodes); for those, ignore everything below and make the cleanest change — rename/drop/reorder freely — unless the maintainer sayscheck backward compatibility. For anything underforks/(upstream ports/wrappers, including the Koolook-original nodes exposed viaSKIP_VERSION_SUFFIX) these rules still apply by default — forks keep full back-compat discipline with no request needed. SeeCLAUDE.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:
- 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. - Mapping connections by input/output name. Most cases.
- Mapping widget values by position in the widget array — this is the source of most breakage.
- 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
- 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.
- Append new inputs/widgets to the END. Never insert in the middle.
Order in
INPUT_TYPESdict matters because Python preserves insertion order and the frontend uses it. - New inputs go in
INPUT_TYPES["optional"]with a default, not inrequired. Old workflows that don't have the field will use the default; new workflows can wire it up. - 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.
- Treat
RETURN_TYPESas 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). 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
# 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:
"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:
"required": { ... unchanged ... },
"optional": {
"pass_name": ("STRING", {"default": "", "tooltip": "..."}),
},
- Old workflows that don't have
pass_nameuse 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). Ifpass_name=""would change the output filename, you need aMyNodeV2instead.
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:
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_valuesof 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.pycheckworkflowswalkstests/workflows/*.json, extracts node IDs by both heuristic and AST-extracted set, and reports any reference that no longer resolves. Thepreflight-releaseskill in.claude/skills/runs all four checks before a release. - ⏳
node-api-changeagent skill — a check that would run before any edit toINPUT_TYPES/RETURN_TYPES/ class names, diff against the rules above, and refuse unsafe changes. Same shape aslicense-pre-check. Not built yet; scoped for the next time someone changes a node's input/output surface.