Skip to main content
Custom-node JavaScript is loaded as an ES module. Register published behavior from the module body, then use explicit lifecycle signals for work that needs a running graph or a loaded workflow.

Importing the API

The frontend installs one API instance immediately before it loads custom-node modules. /comfy/api/v2.js re-exports that instance rather than constructing a new registry. Extension modules should import comfy so the dependency is explicit, the published contract is clear, and tooling can understand it. Do not make a global object part of your pack’s runtime contract. These legacy globals and modules are not part of this contract:
  • window.comfyAPI;
  • window.app;
  • /scripts/app.js, /scripts/api.js, /scripts/widgets.js;
  • LiteGraph and renderer globals.

Versioning and capability probes

Use supports() for optional behavior and require() for a feature without which the extension cannot work. Do not compare application versions, parse comfy.version, or probe an internal member. comfy.capabilities() returns a frozen list of everything the host provides. comfy.forMajor(major) pins a public major when a pack deliberately maintains more than one implementation.

Registration at module load

The API is ready for declarations before the graph has completed setup. These operations normally belong at module scope:
Registration IDs share host-wide namespaces. Prefix setting, command, tab, dialog, top-bar badge, and action-button IDs with a stable pack name.

Extending backend node definitions

defs.extend(selector, apply) is the replacement for beforeRegisterNodeDef and prototype patching:
A selector can be:
  • an exact type string;
  • an array of type strings;
  • a type-name regular expression;
  • { category: string | RegExp };
  • a predicate over NodeDef when the other forms cannot express the match.
Prefer an indexable selector. A predicate must inspect every registered definition and should be reserved for structural questions such as “any node with a VAE input.” The builder’s def is the frozen definition after earlier extensions have run. Registered callbacks compose; there is no previous prototype callback to capture or invoke.

Defining a frontend-owned node type

Use plain data rather than subclassing LGraphNode:
The type must be globally unique. define() returns an unregister function. execution: 'frontend' keeps the node out of the backend prompt. A resolver is optional: without one, the node is simply omitted. See Execution and resolution before defining execution behavior.

Defining an input widget type

defineWidgetType() replaces getCustomWidgets for a Python input type:
Type-level widget construction happens before the owner has joined a graph. The render callback therefore receives a value accessor and a WidgetTypeContext, not a node handle. Use context.onNodeReady() when behavior genuinely needs the owning NodeHandle. The element belongs to the pack’s mounted UI surface; it is not a supported way to change ComfyUI’s host page. Use the supplied container and published UI contributions instead of querying or changing host-owned page elements.

Lifecycle signals

Application ready

At this point the canvas, settings, graph, and node definitions exist. A listener registered after readiness still runs on the next microtask.

Workflow loaded

This fires after every workflow load. It is the replacement for a one-time setup hook when behavior belongs to each document.

Definition lifecycle

Use NodeDefBuilder or NodeDefinition callbacks for individual instances:
  • onCreated after the node joins a graph;
  • onConfigured after saved data is applied;
  • onRemoved when it leaves;
  • onExecuted and onPreview for backend results;
  • onConnectionsChanged, onResized, onHover, onDoubleClick, onPropertyChanged, onDragOver, and onDrop for semantic editor behavior.
NodeCreatedEvent.restored distinguishes fresh nodes from nodes carrying saved state. NodeCreatedEvent.loading distinguishes workflow load from paste or duplication.

Cleanup and ownership

Registrations and subscriptions commonly return Unsubscribe:
Match cleanup to ownership:
  • module registrations may live for the page;
  • a sidebar tab or dialog releases listeners, observers, and timers from its destroy callback;
  • a mounted widget releases them from MountDef.destroy or the function returned by a widget type’s render;
  • per-node state should be removed from onRemoved.
Do not attach private fields to a node handle. Keep pack-owned state in a map keyed by graph ID and node ID, and release it with the node lifecycle.

Type contract

Generate the complete declarations from the matching frontend revision:
If a type or member is absent from that file, it is not published. The runtime global is not a substitute for missing declarations.