Skip to content
Automation
Skill

/comfyui-frontend-extensions

Authoring ComfyUI v2 frontend extensions with @comfyorg/extension-api, covering defineNode/defineExtension/defineWidget, shell UI (sidebar tabs, commands, hotkeys), typed events, and handles. Use when writing or editing ComfyUI web-UI extension code (custom node JS, sidebar

From plugin
comfyui-mcp
74842 skills4 agents11 commands1 MCP
Install
$ npx -y skills add artokun/comfyui-mcp --skill comfyui-frontend-extensions --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/comfyui-frontend-extensions

Context preview

The summary Claude sees to decide when to auto-load this skill.

Authoring ComfyUI v2 frontend extensions with @comfyorg/extension-api, covering defineNode/defineExtension/defineWidget, shell UI (sidebar tabs, commands, hotkeys), typed events, and handles. Use when writing or editing ComfyUI web-UI extension code (custom node JS, sidebar

SKILL.md

comfyui-frontend-extensions.SKILL.md
name: comfyui-frontend-extensions
description: Authoring ComfyUI v2 frontend extensions with @comfyorg/extension-api, covering defineNode/defineExtension/defineWidget, shell UI (sidebar tabs, commands, hotkeys), typed events, and handles. Use when writing or editing ComfyUI web-UI extension code (custom node JS, sidebar panels, widgets).

ComfyUI v2 Frontend Extension API

The v2 extension API is the published npm package `@comfyorg/extension-api`. It replaces the legacy `app.registerExtension()` / `nodeType.prototype` monkey-patching model with a typed, tree-shakeable, import-based API.

> If you are converting an existing v1 extension, read > [`references/migrate-v1-to-v2.md`](references/migrate-v1-to-v2.md) for a > pattern-by-pattern mapping.

Mental model

| v1 (legacy) | v2 (`@comfyorg/extension-api`) | |-------------|-------------------------------| | One giant `app.registerExtension({...})` call | One `defineX` per concern, each independently disposable | | `window.app` / `app.*` globals | Direct `import` from the package; no `window.app` at module-eval time | | `nodeType.prototype.onExecuted = ...` patching | `node.on('executed', fn)` on a `NodeHandle` | | Mutate `widget.value`, assign `widget.callback` | `widget.setValue(v)` / `widget.on('valueChange', fn)` | | `api.addEventListener('execution_start', fn)` | `execution.on('start', fn)` (typed namespaces) | | Manual `removeEventListener` bookkeeping | Every subscription returns `Unsubscribe`; every `defineX` returns `DisposableHandle` |

Core principles baked into the API:

  • **Import, don't reach for globals.** `import { defineNode } from '@comfyorg/extension-api'`. No `window.app` dependency at module evaluation time.
  • **Read via getters, write via command-dispatch setters.** `getValue()` reads; `setValue()` dispatches an undo-able, serializable command. Read-only invariants (set at construction) are `readonly` accessors (`node.type`, `widget.name`).
  • **Observe via typed `on(...)` subscriptions.** Each returns an `Unsubscribe` cleanup function. No Vue refs/signals are ever exposed; Vue reactivity is the internal engine only.
  • **Everything is disposable.** Every `defineX` returns a `DisposableHandle` with an idempotent, synchronous `dispose()`.

Registration entry points

All imported from `@comfyorg/extension-api`:

| Function | Purpose | Returns | |----------|---------|---------| | `defineNode(opts)` | **Primary entry** — react to node lifecycle (replaces prototype patching) | `NodeExtensionOptions` | | `defineExtension(opts)` | App-scoped lifecycle (`init`/`setup`) + shell UI host | `ExtensionOptions` | | `defineWidget(opts)` | Register a custom widget type (DOM via `mount`) | `WidgetExtensionOptions` | | `defineSidebarTab(opts)` | Add a left-sidebar tab (Vue or custom) | `DisposableHandle` | | `defineBottomPanelTab(opts)` | Add a bottom-panel tab | `DisposableHandle` | | `defineToolbarButton(opts)` | Add an action-bar button | `DisposableHandle` | | `defineCommand(opts)` | Register an invokable command | `DisposableHandle` | | `defineHotkey(opts)` | Bind a key combo to a command id | `DisposableHandle` | | `defineSetting(opts)` | Add a settings-menu entry | `DisposableHandle` | | `defineAboutBadge(opts)` | Add a badge to the About page | `DisposableHandle` |

Imperative carve-outs (fire-and-forget, not `defineX`, no handle): `toast`, `notify`.

A single extension file typically exports a default `defineExtension`/`defineNode` result and calls the shell-UI `defineX` functions inside `setup()` or at module scope. They queue safely before the app boots.

`defineNode` — the primary entry point

Reacts to node lifecycle. `nodeCreated` fires once per node instance (typed in, pasted, duplicated, or loaded without an existing workflow). `loadedGraphNode` fires once when a node is restored from a saved workflow (widget values already populated). Exactly one of them fires per node entity, never both.

import { defineNode, onNodeMounted, onNodeRemoved } from '@comfyorg/extension-api'

export default defineNode({
  name: 'my-org.executed-logger',
  // Filter to specific comfyClass names. Omit to receive every node type.
  nodeTypes: ['KSampler', 'KSamplerAdvanced'],

  // MUST be synchronous. Runs inside a Vue EffectScope; everything registered
  // here (subscriptions, onNodeMounted) auto-disposes when the node is removed.
  nodeCreated(node) {
    // Read-only invariants
    console.log(node.type, node.comfyClass, node.id)

    // Subscribe to backend execution completion (replaces onExecuted patching)
    node.on('executed', (e) => {
      console.log('output:', e.output) // Record<string, unknown>
    })

    // Lifecycle hooks — call SYNCHRONOUSLY (never after an await)
    onNodeMounted(() => {
      // Node fully mounted; DOM/canvas ready.
    })
    onNodeRemoved(() => {
      // Cleanup: abort fetches, close sockets. Does NOT fire on subgraph promotion.
    })
  },

  loadedGraphNode(node) {
    // Node restored from a saved workflow; widget values are already set.
  }
})

`NodeHandle` surface (Phase A)

| Member | Kind | Notes | |--------|------|-------| | `id: string` | readonly | Opaque token. Compare with `node.equals(other)`, never by slicing. | | `equals(other)` | method | Canonical identity comparison. | | `type: string` | readonly | LiteGraph node type. | | `comfyClass: string` | readonly | Backend class name. | | `getProperty<T>(key)` / `getProperties()` / `setProperty(key, v)` | methods | Per-instance props (migration shim — prefer widget values). | | `getInputs()` / `getOutputs()` | methods | `ReadonlyArray<Readonly<SlotInfo>>` — frozen views. | | `on('executed', fn)` | method | Execution complete → `NodeExecutedEvent { output }`. | | `on('removed', fn)` | method | Node deleted (not subgraph promotion). | | `on('configured', fn)` | method | Loaded from saved workflow (after widget values restored). | | `on('beforeSerialize', fn)` | method | **Deprecated** — use widget-level `before

Read more
Ships withcomfyui-mcp

This project is no longer maintained. ComfyUI now ships official agent and MCP tooling — Comfy Agent and Comfy MCP — built and supported by the Comfy-Org team with deeper integration than a community project can match.

Get the whole plugin

Other skills on comfyui-mcp.