LGraph from serialized data now requires an active
Pinia instance. LGraph.configure() clears and rebuilds the graph directly
against the ECS stores, so a test harness, worker, or standalone LiteGraph
consumer that calls new LGraph(data) or graph.configure(data) without an
active Pinia now fails outright instead of silently producing an empty
graph. This is the same requirement documented in
Link registration migration.
Extensions running in ComfyUI use the application’s active Pinia instance.
Do not create or activate a separate Pinia instance from extension code.
node.onConfigure, node.onSerialize, graph.onConfigure, and
graph.onSerialize keep their existing signatures and firing order. For
connection-timing hooks see
Connection callbacks migration; for
node.widgets and widget value storage see
Widget system migration.
onConfigure now receives a shallow copy of the serialized data rather than
the caller’s own object. Adding, removing, or reassigning top-level keys on the
callback argument does not change the caller’s workflow JSON and is not
persisted. Nested objects (for example inputs, pos, properties) are still
shared with the caller, so mutating them in place still leaks; extensions that
relied on either kind of mutation must store custom state on node.properties
or graph.extra instead.
Store custom data in properties or extra
Usenode.properties for per-node extension data and graph.extra for
per-graph extension data. These remain the stable, intended extension
surfaces for persisted data.
onSerialize and onConfigure are called with this bound to the live
node/graph object; the argument passed to the callback is the serialized
data object (ISerialisedNode/ISerialisedGraph), not the live node/graph.
Because this is still the live object, a hook can mutate any field on it,
including fields that are now store-backed. Don’t use these hooks to
overwrite core fields: a write via this from inside
onSerialize/onConfigure goes straight through to the store, with no
schema, ownership, or undo boundary of its own.
onSerialize runs after the frontend has copied graph.extra and
node.properties into the data object, so a key that the hook adds to
this.extra or this.properties is missing from the current save. Inside
onSerialize, write to the callback argument instead: data.extra for a
graph, data.properties for a node. Otherwise, update graph.extra or
node.properties before serialization starts.
onSerialize holds one callback, so keep any hook another extension already
installed:
onSerialize. Subgraph definitions are saved without
calling a hook on the subgraph, so store subgraph data in subgraph.extra
before the workflow is saved.
A custom top-level key that a hook adds to data stays at the top level in
LGraph.asSerialisable(). The legacy LGraph.serialize() entry point moves it
under data.extensions instead, and leaves out values that are not
JSON-compatible. When the workflow loads, onConfigure receives the key at the
top level again. Keys under extra keep their place in both entry points.
Serialize nodes instead of enumerating properties
If your extension builds a serialized or debug representation of a node by enumerating its own properties, switch it tonode.serialize().
id, type, title, flags, mode, color, bgcolor, shape, and
showAdvanced are now accessor properties over the node data store instead
of own data properties on the node instance. Object.keys(node),
for...in, and { ...node } no longer surface these fields, though direct
reads and writes (node.title, node.mode = ...) keep working unchanged.
inputs, outputs, and widgets remain own-enumerable, so
spread/enumeration still picks those three up.
For widget-level serializeValue, a separate serialization layer that
builds the API prompt payload, not the workflow JSON, see
Widget system migration.