File size: 9,600 Bytes
e4ab0d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# 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.