app; how do I preserve
that behavior through the published API?” It is based on the custom-node
conversion corpus, including cases where the correct replacement was not the
API with the most similar name.
The published entry point is always:
JSON.parse,
a workflow file, graphToPrompt, or a backend response, it is data rather than
a live node. Do not replace fields inside serialized data with handle methods.
Start with the behavior
Before choosing a replacement, state what the old code accomplishes for the user. One old mechanism often served several unrelated purposes:
Do not migrate by spelling alone. Trace what the callback reads, what it writes,
when it runs, and which saved or queued bytes it affects.
Quick lookup by legacy surface
This index gives the usual destination. Follow the detailed recipe whenever the row names more than one destination or changes saved workflow topology.Registration and lifecycle
How do I replace app.registerExtension()?
Split the registration by behavior. The old extension object combined node
definition hooks, commands, settings, custom widgets, and application
lifecycle. The published API gives each one an explicit owner.
Old:
How do I run code after a node is restored?
UseonCreated for a live, addressable node and inspect its event:
restored covers nodes carrying saved state, including duplicate and paste.
loading distinguishes a workflow load. Use onConfigured when the behavior
needs the saved node record.
How do I keep private state on a node?
Do not assign custom fields toLGraphNode or NodeHandle. Keep pack state in
a map and include graph scope in its key:
Nodes and graphs
How do I read or change node state?
Handles use methods so reads can resolve current store state and writes can use host mutation behavior.
Assigning a new property to a closed handle is not a mutation API. Use the
setter, or a pack-owned map when the value is not entity state.
How do I enumerate or find nodes?
app.graph._nodes, app.graph.getNodeById(),
canvas.selected_nodes, and renderer selection stores for node selection.
Returned arrays are frozen snapshots.
comfy.graph is the graph currently visible. For document-wide work, preserve
scope rather than flattening IDs:
How do I add, clone, remove, or replace a node?
replace() carries compatible state and links and groups the operation into one
undo step on the visible graph. A defensive write such as
node.type = node.type ?? undefined has no user behavior and should be removed
instead of converted.
How do I make several edits one undo step?
Replacegraph.beforeChange() / graph.afterChange() pairs with a synchronous
scope:
await.
How do I respond to graph changes without polling?
Choose the narrowest semantic signal:
Do not increment
graph._version yourself. Published mutations and committed
widget values advance host change state. Treat graph.version as an opaque
token, not a counter or event log.
How do I accept a file dropped from the browser?
Register the behavior on the node definition instead of replacing renderer drop methods:true from onDragOver asks both renderers to present and route the
drop. Returning true from onDrop claims it; handlers after the claimant do
not run. The host gets the first opportunity, so this extends file handling
rather than replacing behavior another pack or core already owns.
Slots and links
How do I inspect an input connection?
How do I connect or disconnect slots?
input.link, pushing into output.links, and mutating
the graph’s link map. Normal compatibility checks and connection-veto hooks
still run.
How do I add or remove dynamic inputs and outputs?
node.inputs.add/remove and node.outputs.add/remove instead of
addInput, removeInput, addOutput, or array mutation. Removing a slot
disconnects its links through the host.
How do I rename or retype a slot?
Use one atomic patch:slot.name, slot.type, slot.label, and
renderer shape fields. Existing links remain attached.
How do I reorder slots safely?
How do I move links to another output?
How do I veto or observe a proposed connection?
UseonBeforeConnect only when the old hook can refuse:
onConnectInput or onConnectOutput never returned false, it was
an observer. Use onConnectionsChanged instead.
How do I preserve widget-backed input constraints?
A socket converted from a widget carries the declaration a connected Primitive node should render. Read or intersect that declaration through the input handle:mergeWidgetConfig() updates the declaration only when the two widget types
are compatible. It returns undefined and leaves the input unchanged when
they are not. For a dynamic socket that represents one of the node’s widgets,
pass both widget and widgetConfig to inputs.add(), or apply them together
with input.modify(). The widget name must already exist on that node.
How do I handle a link dropped on a node when no one slot fits?
UseonUnplacedLink when the node understands a bundle or pipe and can expand
one gesture into its own connections:
true
only when it placed the link. Both ends can be offered the gesture and the
first claimant wins. This replaces a global connectByType patch without
changing link-routing behavior for every other node in the document.
Widgets
How do I read and write a widget value?
setValue() replaces both a bare widget.value = value and the common pair
widget.value = value; widget.callback?.(value). It uses the host’s commit
protocol: property synchronization, host callback behavior, node change
behavior, one change event, and graph change tracking.
An equal value is a no-op. A programmatic write does not fire activate.
How do I replace a chained widget.callback?
Observe values additively:
How do I hide a converted widget?
widget.type = 'converted-widget' and the bookkeeping that
restored the old type and size function. Hiding and serialization are separate:
setHidden() retains the widget’s existing saved and prompt behavior. Audit any
old serializeValue override separately.
How do I add, remove, or reorder widgets?
reorder() requires every current widget name exactly once. It cannot silently
drop widgets or bypass teardown.
How do I change widget options?
getOptions() returns a frozen view. Do not mutate it or remove and reinsert a
widget to make the renderer notice changes.
How do I replace addDOMWidget() or widget.inputEl?
For a pack-owned control on one node, mount into a host-owned container:
destroy(). Keep one mount per serialized legacy
widget cell unless you have proved that consolidating them preserves
widgets_values.
For behavior on a host-owned multiline editor, do not mount a duplicate merely
to access its element. Subscribe to textInteraction:
How do I replace getCustomWidgets for a backend input type?
Register the renderer for that type before nodes are constructed:
widgets_values cell and keeps the input a widget
instead of turning it into a socket.
How do I replace widget.serializeValue?
Classify the behavior:
workflow, prompt, and embedded. The handler is
synchronous and changes only that write, not the live value.
How do I migrate a name-keyed widgets_values override?
Delete the live override. Runtime widget identity is already the widget name:
onConfigured, and that hook also receives
the original saved node data. Read the pack’s old record there, translate
renamed or retired keys, and commit the recovered values through named widget
handles. That migration is user-data compatibility; the live serialization
override is not.
How do I migrate a custom upload widget?
Inspect the Python input declaration first. If its options already includeimage_upload, animated_image_upload, video_upload, or audio_upload, the
host supplies the chooser, upload, value commit, and preview. Remove the
duplicate frontend widget.
Do not replace it with widgets.mount() merely because the original used DOM.
An extra widget adds a positional widgets_values cell and can shift every
later value. Mount only when the pack provides behavior beyond the declared
upload contract, and preserve the original serialized-cell count.
Drawing, geometry, and editor interaction
How do I replace an onDraw* callback?
Inspect its body first.
It draws a small status label
Use a badge when the content belongs in node chrome rather than in an interactive widget:It actually draws
Move the drawing and hit testing to a pack-owned canvas widget:It enforces size
Declare the constraint once:It polls for state
Move the body to the event that changes the state:widget.on('change'),
onPropertyChanged, onConnectionsChanged, onNodeChanged, or
onWorkflowLoaded.
It positions DOM over the graph
Prefer a mounted widget. If the UI must remain outside the node, readnode.getScreenRect() and update it from comfy.onViewportChanged().
How do I replace renderer geometry constants?
Use the answer to the operation instead of publishing another renderer
constant.
How do I request a repaint?
Do not portcanvas.setDirty() or node.setDirtyCanvas(). Handle mutations
invalidate their own host views. For external data behind a canvas widget, call
that surface’s redraw().
How do I rebuild a node-drag editing gesture?
Observe semantic movement rather than document pointer events and canvas drag state:onNodeMoved works under both renderers. It reports movement, not proof that a
person caused it, so guard mutations made by the gesture against re-entry.
onNodeDragEnd is Nodes 2.0 only because the legacy renderer has no published
drag-completion lifecycle. If the action must work under both renderers, design
an explicit command or button rather than pretending release is observable.
Use comfy.isInteracting() when the old code read several renderer flags only
to ask whether the editor was already in the middle of any gesture.
Menus and application UI
How do I add a node context-menu item?
getExtraMenuOptions and node-menu prototype patches. Entries
from multiple packs compose. Use when, a dynamic label, or items for a
submenu rather than constructing LiteGraph.ContextMenu.
How do I open a menu from my own button or surface?
MouseEvent gives the host an anchor. This does not add a new
hook to a host-owned canvas or slot menu; use it only when the pack owns the
gesture that asks for the menu.
How do I replace DOM insertion into ComfyUI chrome?
Contributions are declarative so host layout, theme, accessibility, and
lifecycle remain host-owned.
How do I replace extension settings?
settings.onChange() instead of polling, including for a core setting the
pack does not own. IDs must be namespaced.
How do I set a temporary graph background?
If the old code replaced the canvas background renderer only to show an image, write the core setting instead:How do I use host palette colors?
Resolve design tokens at the point of use:setTypeColor(type, color) only for a link type the pack owns; it refuses
core-owned types and returns an unsubscribe that restores the prior mapping.
nodeColor() returns the title, body, and group fill colors behind a palette
name. Do not cache or copy the host’s internal color tables.
Execution, previews, and backend services
How do I replace api.queuePrompt() wrappers?
Do not rebuild a prompt wrapper when only one stage is needed.
How do I read or change auto-queue and batch settings?
Use the queue service rather than queue stores or settings IDs:disabled, change, and instant. Call
disableAutoQueue() before a self-interrupting conditional workflow so the
automatic runner does not immediately submit it again. It does not cancel the
run already in progress.
How do I define a frontend-only or virtual node?
Replaceextends LGraphNode, isVirtualNode, and prompt mutation with plain
data and pure resolution:
How do I implement “use this value everywhere” behavior?
Use a supplier rather than scanning and editing the built prompt:graph.resolvedSupplies() exposes the winning edges when a command needs to
materialize the same choices as real links.
How do I correlate execution or preview events with a node?
b_preview, b_preview_with_metadata, and module-level
“currently executing ID” correlation for a node’s own frames. For global
execution UI, use comfy.executingNode(), comfy.executionNode(id), and
comfy.onExecutingNodeChanged().
How do I read images produced by another node?
Definition callbacks are intentionally correlated only with the node type they extend. For a command, panel, or overlay that inspects an arbitrary node, read that node’s output state directly:undefined when no image is singled
out.
How do I replace a graphToPrompt wrapper used for a sidecar cache?
Trace the Python first. A wrapper that only chose a cache filename does not
require access to the built prompt:
- Use the pack’s authenticated route through
comfy.backend.fetch()to read or refresh the sidecar. - If the backend needs connected tensors, run that node and its dependencies
with
comfy.queue.run({ nodes: [node] }). - Refresh correlated state from the definition’s
onExecutedcallback andcomfy.backend.on('execution_cached', ...)when the cached-result path also matters. - Use a backend hidden
UNIQUE_IDinput when simultaneous node instances need distinct identity.
How do I call my Python routes or receive custom messages?
backend.fetch() rather than plain fetch(backend.url(...)) when the
request needs host credentials. Use new URL('./asset.css', import.meta.url)
for a file shipped beside the current module.
How do I load a workflow or store pack documents?
workflow.open() replaces app.loadGraphData() for an explicit user action.
comfy.storage replaces direct user-data APIs or localStorage for named
server-side presets, prompts, and templates.
How do I apply ComfyUI filename and workflow tokens?
Things not to translate
Some old code should disappear rather than acquire a new spelling:
Other mechanisms have no sanctioned equivalent and must not be recreated with
an internal escape hatch:
- patching
LGraph,LGraphNode,LGraphCanvas, renderer, or widget prototypes; - mutating the built prompt or core workflow snapshot;
- selecting and restyling host-owned DOM with global selectors;
- publishing or consuming live internal stores and mutable link records;
- making a supplier or resolver cross a subgraph boundary;
- using a per-frame callback as a general tick.
How do I document a refusal?
A refusal is an architectural decision, not shorthand for “the conversion was hard” or “I did not find a method.” It must answer why the old mechanism or requested capability is outside the published contract. Every refusal record must include:- User behavior: what the feature does from the user’s point of view.
- Mechanism: the exact live object, prototype, store, DOM, prompt, or renderer operation the original used.
- Reason: which ownership, determinism, wire-format, renderer-independence, scope, or lifecycle guarantee that mechanism violates.
- Outcome: the supported replacement and remaining loss. State explicitly when the loss is nothing.
- Boundary: the precise published capability or policy change that would make the refused remainder supportable, or why it must remain host-owned.
- “not supported”;
- “no API”;
- “renderer internals are unavailable”;
- “cannot be converted”;
- a list of removed property names without the behavior they implemented.
Migration safety checklist
- Trace every
inputs,outputs,links,widgets, andpropertiesvalue to determine whether it is live state or serialized data. - State the user-visible behavior before selecting a replacement.
- Preserve workflow and prompt wire format, including positional widget cells and link IDs.
- Use the narrowest semantic event instead of polling or draw-time work.
- Keep frontend resolvers and suppliers pure and graph-scoped.
- Use methods for handle reads and writes; do not attach arbitrary properties.
- Probe optional behavior with
comfy.supports(). - Verify both behavioral equivalence and the specific failure the migration is intended to fix.