Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions custom-nodes/v2/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
---
title: "Overview"
description: "Build JavaScript extensions for ComfyUI nodes with the published frontend API."
---

The Custom Nodes V2 frontend SDK is the published JavaScript API for extending ComfyUI node behavior, widgets, graphs, and interface features.

A pack's frontend extension can:

- extend node definitions with custom behavior, badges, previews, and lifecycle hooks;
- add widgets, panels, menus, commands, and other interface features;
- read and edit graphs, nodes, slots, and links through documented handles;
- drive queueing, execution feedback, settings, storage, and authenticated backend requests.

This section documents the JavaScript API only. Python node authoring is documented separately.

<CardGroup cols={2}>
<Card title="Main concepts" icon="lightbulb" href="/custom-nodes/v2/javascript/concepts">
Learn handles, lifecycle, snapshots, capabilities, and how the frontend API is structured.
</Card>
<Card title="Extend a node with JavaScript" icon="graduation-cap" href="/custom-nodes/v2/javascript/tutorial">
Follow a complete tutorial that adds a badge, a menu action, and lifecycle behavior to an existing node.
</Card>
<Card title="How-to guides" icon="list-check" href="/custom-nodes/v2/javascript/registration">
Find focused guides for registration, definitions, graphs, slots, widgets, UI, execution, and services.
</Card>
<Card title="Reference" icon="book" href="/custom-nodes/v2/reference-overview">
Look up imports, common patterns, rules, versioning, and exact API signatures.
</Card>
</CardGroup>

## What you can build

Anyone who can write JavaScript can change how a node looks and behaves in the ComfyUI editor: custom widgets, live previews, node badges, context menus, commands, panels, and graph automation.

Almost any frontend behavior built against `app`, `LiteGraph`, or prototype patching has a path to V2. Keep the behavior users rely on while replacing private implementation hooks with documented handles, events, and UI contributions.

<Info>
You bring the user experience. Comfy provides the editor, the graph model, and a stable extension API.
</Info>

## How the API fits together

| Part | Import | Use it for |
| --- | --- | --- |
| JavaScript `comfy` | `import { comfy } from '/comfy/api/v2.js'` | Node definitions, graphs, nodes, slots, widgets, queueing, commands, settings, storage, backend calls, and UI |

The normal authoring flow is:

1. Ship the extension module inside the pack's `v2/web/` distribution.
2. Import `comfy` from `/comfy/api/v2.js`.
3. Declare hard dependencies with `comfy.require()` and probe optional ones with `comfy.supports()`.
4. Extend a node definition and attach lifecycle, widget, and UI behavior.
5. Read and mutate graph state through documented handles instead of private objects.

## Extend a node through `comfy`

The JavaScript API represents ComfyUI nodes, graphs, widgets, and services through documented handles. Use those handles instead of patching private application objects.

```javascript
import { comfy } from '/comfy/api/v2.js'

comfy.defs.extend('ScaleImage', (definition) => {
definition.onCreated((node) => {
node.addBadge(() => ({ text: 'V2' }))
})
})
```

This keeps node behavior consistent as ComfyUI evolves and across supported frontend renderers.

Check warning on line 70 in custom-nodes/v2/index.mdx

View check run for this annotation

Mintlify / Mintlify Validation (dripart) - vale-spellcheck

custom-nodes/v2/index.mdx#L70

Did you really mean 'renderers'?

## V1 and V2 pack layout

A converted pack keeps its existing V1 distribution at the top level and adds a complete V2 replacement under `v2/`.

```text
my_pack/
├── __init__.py # V1
├── nodes/ # V1
├── web/ # V1
└── v2/
├── __init__.py # V2
├── nodes/ # V2
└── web/ # V2
```

The host selects either the top level as the V1 pack root or `v2/` as the V2 pack root. It does not merge the two trees or fall back to a V1 file that is missing from `v2/`.

V2 frontend modules and their assets live under `v2/web/`. A similarly named file in the root `web/` tree is not a V2 asset.

For an existing pack, the recommended migration path is MAGIC PATCH, a local conversion tool that drives Claude Code or Codex on your own machine. See [Migrate legacy frontend code](/custom-nodes/v2/javascript/migration-recipes) for the frontend mappings it produces.

## Build durable node code

Use documented `comfy` handles, events, and UI contributions instead of depending on `window.app`, LiteGraph internals, application stores, or host DOM. Published extension points can be versioned, tested, and kept compatible as ComfyUI evolves.

<Tip>
Avoid monkeypatching frontend prototypes, application stores, or the web page. Those changes depend on private implementation details and can break without notice. Prefer a published `comfy` capability. If the API cannot express a legitimate use case, tell us what extension point is missing. We expect to add capabilities as node authors find new needs. Keep any unavoidable private integration optional and separate from the main V2 path.

Check warning on line 98 in custom-nodes/v2/index.mdx

View check run for this annotation

Mintlify / Mintlify Validation (dripart) - vale-spellcheck

custom-nodes/v2/index.mdx#L98

Did you really mean 'monkeypatching'?
</Tip>

## Where to go next

1. Read [Use JavaScript handles and lifecycle](/custom-nodes/v2/javascript/concepts) for the frontend mental model.
2. Follow the [Extend a node with JavaScript](/custom-nodes/v2/javascript/tutorial) tutorial.
3. Study the [example packs](/custom-nodes/v2/javascript/example-packs) for tested, runnable patterns.
4. Use the focused [JavaScript how-to guides](/custom-nodes/v2/javascript/registration) while building a real pack.
5. Open the [Reference overview](/custom-nodes/v2/reference-overview) when you need a rule or exact signature.

The generated API reference comes from `comfy-api.d.ts`. If prose and a generated signature disagree, follow the declaration and report the documentation mismatch.
221 changes: 221 additions & 0 deletions custom-nodes/v2/javascript/concepts.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
---
title: "Use JavaScript handles and lifecycle"
description: "Understand definitions, handles, snapshots, graph scopes, mutations, lifecycle, serialization, resolution, and API errors."
---

The published API is organized around definitions, handles, snapshots, scopes,
and host-owned behavior. Understanding those five ideas prevents most migration
mistakes.

## Definitions and instances are different surfaces

A node definition describes a type. A node handle addresses one node in one
graph.

| Surface | Purpose | Typical entry point |
| ---------------- | ----------------------------------------------------------- | ---------------------------------------------- |
| `NodeDef` | Frozen metadata read from a backend or frontend definition. | `comfy.defs.get(type)` |
| `NodeDefBuilder` | Register behavior for every matching instance. | `comfy.defs.extend(selector, apply)` |
| `NodeDefinition` | Declare a frontend-owned node type as plain data. | `comfy.defs.define(definition)` |
| `NodeHandle` | Read or edit one live node. | A lifecycle callback or `comfy.graph.node(id)` |

Register type behavior at module load. Use an instance handle only after the
node has joined a graph. This is why `onCreated` runs later than a legacy
constructor hook: an ID-backed handle cannot address a node that has no graph or
ID yet.

## Handles are closed, ID-backed capabilities

`NodeHandle` and `WidgetHandle` are closed proxy objects. A handle stores public
identity and resolves the current entity on each access; it does not expose the
live internal object.

Consequences:

- unknown members are not an escape hatch to internal data;
- a handle does not keep a deleted entity alive;
- mutations pass through host-owned behavior;
- no handle exposes a constructor, prototype, store, renderer, or Vue proxy;
- `isDeleted` is available on every entity handle.

Keep handles when convenient, but check `isDeleted` before writing through a
long-lived one. Identity reads remain useful after deletion. Other reads return
no value, while a write to a deleted entity throws `ComfyDeletedError` instead
of being silently discarded.

### Handle identity

Do not assume object identity across API instances, majors, events, or graph
scopes:

```js
first === second // only reliable inside one handle cache
comfy.sameEntity(first, second) // supported identity comparison
```

`comfy.adopt(handle)` re-resolves a foreign node handle into the current API
instance. It returns `undefined` for a non-handle, a deleted node, or a handle
kind that cannot be adopted independently.

## Collection reads are snapshots

List-shaped reads return frozen array snapshots:

```js
const nodes = comfy.graph.nodes()
const links = node.outputs.get('IMAGE')?.links() ?? []

for (const link of links) {
// Safe even though disconnecting changes the live graph.
node.outputs.get('IMAGE')?.disconnect(link.targetNodeId)
}
```

The array does not update after it is returned. Ask again when you need current
state. Objects such as `NodeSnapshot`, `SlotSnapshot`, `LinkInfo`, definition
metadata, execution results, and resolver views are also inert, read-only data.

Use collection operations to mutate live state; never mutate an array returned
by `all()`, `nodes()`, `links()`, `names()`, or similar methods.

## Graph scope is part of identity

`comfy.graph` means the graph currently shown by the editor. It can be the root
graph or a subgraph the user entered.

Check warning on line 84 in custom-nodes/v2/javascript/concepts.mdx

View check run for this annotation

Mintlify / Mintlify Validation (dripart) - vale-spellcheck

custom-nodes/v2/javascript/concepts.mdx#L84

Did you really mean 'subgraph'?

Use:

- `comfy.graph.root()` for the document root even while another graph is shown;
- `comfy.graph.subgraphs()` for subgraph definitions;

Check warning on line 89 in custom-nodes/v2/javascript/concepts.mdx

View check run for this annotation

Mintlify / Mintlify Validation (dripart) - vale-spellcheck

custom-nodes/v2/javascript/concepts.mdx#L89

Did you really mean 'subgraph'?
- `GraphScopeHandle.node(id)` to resolve an ID inside its owning graph;
- `node.graphId` when recording a node outside the callback that supplied it.

Do not flatten a document into a map keyed only by node ID. Independently
authored subgraph definitions may contain the same IDs. A durable in-memory key

Check warning on line 94 in custom-nodes/v2/javascript/concepts.mdx

View check run for this annotation

Mintlify / Mintlify Validation (dripart) - vale-spellcheck

custom-nodes/v2/javascript/concepts.mdx#L94

Did you really mean 'subgraph'?
must include graph and node identity.

Supply and frontend-node resolution also run per graph scope. A supplier inside
a subgraph may feed unconnected inputs in that subgraph, but resolution does not

Check warning on line 98 in custom-nodes/v2/javascript/concepts.mdx

View check run for this annotation

Mintlify / Mintlify Validation (dripart) - vale-spellcheck

custom-nodes/v2/javascript/concepts.mdx#L98

Did you really mean 'subgraph'?

Check warning on line 98 in custom-nodes/v2/javascript/concepts.mdx

View check run for this annotation

Mintlify / Mintlify Validation (dripart) - vale-spellcheck

custom-nodes/v2/javascript/concepts.mdx#L98

Did you really mean 'subgraph'?
cross the boundary and create invisible dependencies on outside state.

## Mutations use host behavior

Set values through methods such as `setTitle`, `setValue`, `modify`,
`connectTo`, and `replace`. These methods preserve the host behavior associated
with the edit: callbacks, property synchronization, link identity, layout,
redraw, serialization, and undo boundaries as applicable.

`WidgetHandle.setValue()` is a full programmatic commit. It behaves like a user
edit for the value protocol:

- writes the value;
- synchronizes a property-backed widget;
- runs the widget callback chain and node widget-change behavior;
- notifies `change` listeners;
- advances graph change state.

It does not fire `activate`, because activation reports a user act. Writing the
current value again is a no-op.

Use `graph.batch()` for a synchronous compound edit that should be one undo
step:

```js
comfy.graph.batch(() => {
const first = comfy.graph.add('CheckpointLoaderSimple')
const second = comfy.graph.add('KSampler')
first.outputs.get('MODEL')?.connectTo(second.id, 'model')
})
```

Do not hold a batch across `await`; unrelated user actions during the wait would
be folded into the pack's edit.

## Lifecycle is explicit

An extension module can register definitions, settings, commands, widget types,
and listeners immediately. Use lifecycle signals for state that is not ready at
module evaluation:

| Signal | Meaning |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `comfy.onReady` | The application, definitions, settings, and graph have finished initial setup. Fires on the next microtask if already ready. |

Check warning on line 142 in custom-nodes/v2/javascript/concepts.mdx

View check run for this annotation

Mintlify / Mintlify Validation (dripart) - vale-spellcheck

custom-nodes/v2/javascript/concepts.mdx#L142

Did you really mean 'microtask'?
| `comfy.onWorkflowLoaded` | A new workflow has finished loading. Fires for every load. |
| `NodeDefBuilder.onCreated` | One matching node has joined a graph and can be addressed. |
| `NodeDefBuilder.onConfigured` | Saved node data was applied. |
| `NodeDefBuilder.onRemoved` | One matching node left its graph. |

Most registrations and subscriptions return `Unsubscribe`. Retain it when the
registration has a shorter lifetime than the module or owning node.

## Observe the narrowest behavior

Prefer a semantic event to polling, repaint hooks, or broad document scans:

1. a widget's `change`, `activate`, `textInteraction`, or `beforeSerialize`;

Check warning on line 155 in custom-nodes/v2/javascript/concepts.mdx

View check run for this annotation

Mintlify / Mintlify Validation (dripart) - vale-spellcheck

custom-nodes/v2/javascript/concepts.mdx#L155

Did you really mean 'widget's'?
2. a definition lifecycle hook such as `onConnectionsChanged` or `onResized`;
3. root observers such as `onNodeChanged`, `onNodeMoved`, or
`onViewportChanged`;
4. `graph.version` only as an opaque structural-change token.

`graph.version` includes widget values committed through the host protocol, but
it is still an opaque change token rather than a universal edit log. Never
subtract versions or assume consecutive increments. Data a pack keeps outside
the graph and widget model does not affect it.

## Saved workflow and queued prompt are separate destinations

The API distinguishes:

- graph state in the saved workflow;
- input values sent in the API prompt;
- the workflow embedded into an output generated by that prompt.

Mounted widgets expose separate `serialize` and `sendToPrompt` flags. Existing
widgets can replace their value for one serialization destination with a
synchronous `beforeSerialize` listener:

```js
widget.on('beforeSerialize', (event) => {
if (event.context === 'prompt') {
event.setSerializedValue(expandReferences(String(event.value)))
}
})
```

The live widget value is unchanged. Ignoring `event.context` changes all three
destinations. Serialization handlers are synchronous; starting asynchronous
work inside one does not delay the prompt or workflow write.

## Frontend execution is pure resolution

A frontend-only node is ordinary editor state that does not reach the backend.
At prompt time it either:

- is omitted;
- forwards an output to one of its inputs;
- supplies a literal value.

Resolvers and suppliers receive frozen read views and return data. They do not
mutate the graph or a prompt draft. This keeps prompt construction deterministic
and prevents a failed resolver from leaving the document half-edited.

Use ordinary node and graph methods for permanent editor actions. Use resolution
only to describe what execution means.

## Errors are part of the contract

API failures are plain `Error` subclasses with no internal object attached:

| Error | Cause |
| ------------------------- | ----------------------------------------------------- |
| `ComfyApiError` | Base class for API failures. |
| `ComfyDeletedError` | A mutation targeted a deleted handle. |
| `ComfyReadonlyError` | Code attempted to assign a read-only public property. |
| `ComfyAmbiguousSlotError` | A name matched more than one slot. |
| `ComfyUnsupportedError` | `require()` named a capability the host lacks. |

The constructors are not currently exported from `/comfy/api/v2.js`. Prefer
capability checks, `isDeleted`, and `undefined` handling for expected absence;
use ordinary `Error` fields when reporting an unexpected failure.

Loading
Loading