Files
malkuthro-ComfyUI-Koolook/docs/maintainers/node-versioning.md
malkuthroandClaude Opus 4.7 ee8ccefdf8 docs(reorg) + chore: roll [Unreleased] forward + bump 0.1.7 (#37)
Bundles two changes for v0.1.7:

1. PR #36 (already on main, just rolling [Unreleased] -> [0.1.7]):
   suffix all node display names with " (Koolook)" so they group
   together in ComfyUI's node-add search. Class IDs unchanged.

2. Docs reorg:
   - git mv Glossary.md            -> docs/reference/glossary.md
   - git mv RELEASING.md           -> docs/maintainers/releasing.md
   - Three audience buckets under docs/: user_guide/ (end users),
     reference/ (lookups), maintainers/ (project ops). Each gets a
     short index README.md.
   - Top-level docs/README.md as the navigation entry point.
   - New: docs/maintainers/registry-api.md captures the Comfy Registry
     undocumented version-management API (deprecate/yank/status enum,
     auto-deprecation behavior, curl recipes, optional GH Actions
     workflow YAML for codified mgmt without exposing the token).
   - New: docs/maintainers/node-versioning.md codifies the rules from
     the v0.1.5 PR-review discussion for changing INPUT_TYPES /
     RETURN_TYPES / class names without breaking saved workflows.
   - Imported the existing untracked docs/ images that the maintainer
     started locally (node inventory screenshot, Easy Image Batch
     helper image).
   - Cross-references updated in README.md, CLAUDE.md, the issue
     template, and .claude/skills/add-external-fork/SKILL.md.

Root drops from 6 -> 4 markdown files. The four that stay (README,
LICENSE, CHANGELOG, CLAUDE.md) are the ones that *must* live there for
GitHub / Comfy Registry / tooling / Claude Code reasons.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 23:26:46 +02:00

7.7 KiB

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). 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_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:

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 we don't have yet (worth adding when you next touch a node)

  • A CI smoke test that loads a fixture workflow JSON against the current pack and asserts every node still resolves. This would catch most of the failure modes above before they ship. See loose-end #C in the v0.1.5 PR description.
  • A .claude/skills/node-api-change/ skill that runs before any edit to INPUT_TYPES / RETURN_TYPES / class names — checks the diff against the rules above and refuses unsafe changes. Same shape as license-pre-check.

Both are scoped for "whenever Cristian next plans to touch a node's input or output surface."