## Summary Documents the PrimitiveNode copy/paste bug mechanism and connection lifecycle semantics in `WIDGET_SERIALIZATION.md`. This is tribal knowledge from debugging [#1757](https://github.com/Comfy-Org/ComfyUI_frontend/issues/1757) and the related [Slack discussion](https://comfy-organization.slack.com/archives/C09AQRB49QX/p1771806268469089). ## What's documented - **The clone→serialize gap**: `_serializeItems()` calls `item.clone()?.serialize()`. The clone has no `this.widgets` (PrimitiveNode creates them on connection), so `serialize()` silently drops `widgets_values`. - **Why seed survives but control_after_generate doesn't**: Primary widget value is copied from the target on reconnect; secondary widgets read from `this.widgets_values` which was lost. - **Current vs. proposed lifecycle**: Empty-on-copy → morph-on-connect (current) vs. clone-configured-instance → empty-on-disconnect (proposed). - **Design considerations**: `input.widget` override flexibility, deserialization ordering, and the minimal `serialize()` override fix. ## Related - Issue: #1757 - Fix PR: #8938 - Companion: #9102 (initial WIDGET_SERIALIZATION.md), #9105 (type/JSDoc improvements) - Notion: COM-15282 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9119-docs-document-PrimitiveNode-copy-paste-semantics-and-widgets_values-loss-3106d73d3650816ba7f7d9e6f3bb3868) by [Unito](https://www.unito.io)
5.3 KiB
Widget Serialization: widget.serialize vs widget.options.serialize
Two properties named serialize exist at different levels of a widget object. They control different serialization layers and are checked by completely different code paths.
widget.serialize — Controls workflow persistence. Checked by LGraphNode.serialize() and configure() when reading/writing widgets_values in the workflow JSON. When false, the widget is skipped in both serialization and deserialization. Used for UI-only widgets (image previews, progress text, audio players). Typed as IBaseWidget.serialize in src/lib/litegraph/src/types/widgets.ts.
widget.options.serialize — Controls prompt/API serialization. Checked by executionUtil.ts when building the API payload sent to the backend. When false, the widget is excluded from prompt inputs. Used for client-side-only controls (control_after_generate, combo filter lists) that the server doesn't need. Typed as IWidgetOptions.serialize in src/lib/litegraph/src/types/widgets.ts.
These correspond to the two data formats in ComfyMetadata embedded in output files (PNG, GLTF, WebM, AVIF, etc.): widget.serialize → ComfyMetadataTags.WORKFLOW, widget.options.serialize → ComfyMetadataTags.PROMPT.
Permutation table
widget.serialize |
widget.options.serialize |
In workflow? | In prompt? | Examples |
|---|---|---|---|---|
| ✅ default | ✅ default | Yes | Yes | seed, cfg, sampler_name |
| ✅ default | ❌ false | Yes | No | control_after_generate, combo filter list |
| ❌ false | ✅ default | No | Yes | No current usage (would be a transient value computed at queue time) |
| ❌ false | ❌ false | No | No | Image/video previews, audio players, progress text |
Gotchas
addWidget('combo', name, value, cb, { serialize: false })putsserializeintowidget.options, not ontowidgetdirectly. These are different properties consumed by different systems.LGraphNode.serialize()checkswidget.serialize === false(line 967). It does not checkwidget.options.serialize. A widget withoptions.serialize = falseis still included inwidgets_values.LGraphNode.serialize()only writeswidgets_valuesifthis.widgetsis truthy. Nodes that create widgets dynamically (likePrimitiveNode) will have nowidgets_valuesin serialized output if serialized before widget creation — even ifthis.widgets_valuesexists on the instance from a priorconfigure()call.widget.options.serializeis typed asIWidgetOptions.serialize— both properties share the nameserializebut live at different levels of the widget object.
PrimitiveNode and copy/paste
PrimitiveNode creates widgets dynamically on connection — it starts as an empty polymorphic node and morphs to match its target widget in _onFirstConnection(). This interacts badly with the copy/paste pipeline.
The clone→serialize gap
LGraphCanvas._serializeItems() copies nodes via item.clone()?.serialize() (line 3911). For PrimitiveNode this fails:
clone()callsthis.serialize()on the original node (which has widgets, sowidgets_valuesis captured correctly).clone()creates a fresh PrimitiveNode viaLiteGraph.createNode()and callsconfigure(data)on it — this storeswidgets_valueson the instance.- But the fresh PrimitiveNode has no
this.widgets(widgets are created only on connection), so whenserialize()is called on the clone,LGraphNode.serialize()skips thewidgets_valuesblock entirely (line 964:if (widgets && this.serialize_widgets)).
Result: widgets_values is silently dropped from the clipboard data.
Why seed survives but control_after_generate doesn't
When the pasted PrimitiveNode reconnects to the pasted target node, _createWidget() copies theirWidget.value from the target (line 254). This restores the primary widget value (e.g., seed).
But control_after_generate is a secondary widget created by addValueControlWidgets(), which reads its initial value from this.widgets_values?.[1] (line 263). That value was lost during clone→serialize, so it falls back to 'fixed' (line 265).
See ADR-0006 for proposed fixes and design tradeoffs.
Code references
widget.serializechecked:src/lib/litegraph/src/LGraphNode.tsserialize() and configure()widget.options.serializechecked:src/utils/executionUtil.tswidget.options.serializeset:src/scripts/widgets.tsaddValueControlWidgets()widget.serializeset:src/composables/node/useNodeImage.ts,src/extensions/core/previewAny.ts, etc.- Metadata types:
src/types/metadataTypes.ts - PrimitiveNode:
src/extensions/core/widgetInputs.ts - Copy/paste serialization:
src/lib/litegraph/src/LGraphCanvas.ts_serializeItems() - Clone:
src/lib/litegraph/src/LGraphNode.tsclone()