comfyui-node-frontend
ComfyUI frontend JavaScript extensions - hooks, widgets, sidebar tabs, commands, settings, toasts, dialogs. Use when adding UI features to custom nodes, creating custom widgets, or extending the ComfyUI frontend.
How do I install this agent skill?
npx skills add https://github.com/jtydhr88/comfyui-custom-node-skills --skill comfyui-node-frontendIs this agent skill safe to install?
- Gen Agent Trust Hubpass
This skill provides documentation and technical references for developing ComfyUI frontend extensions. It outlines standard platform APIs, lifecycle hooks, and UI component registration. No security issues or malicious patterns were detected.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
- Runlayerpass
2 files scanned · No issues
What does this agent skill do?
ComfyUI Frontend Extensions
Custom nodes can extend the ComfyUI frontend with JavaScript. Extensions register hooks, widgets, commands, settings, and UI components.
Quick Start
1. Export WEB_DIRECTORY in Python
# __init__.py
WEB_DIRECTORY = "./js"
__all__ = ["WEB_DIRECTORY"]
2. Create JavaScript Extension
// js/my_extension.js
import { app } from "../../scripts/app.js";
app.registerExtension({
name: "my_nodes.my_extension",
async setup() {
console.log("Extension loaded!");
},
});
All .js files in WEB_DIRECTORY are loaded automatically when ComfyUI starts.
Extension Hooks (Lifecycle Order)
init — After canvas created, before nodes
app.registerExtension({
name: "my.ext",
async init(app) {
// Modify core behavior, add global listeners
},
});
addCustomNodeDefs — Modify node definitions
async addCustomNodeDefs(defs, app) {
// defs is a dict of all node definitions
// Can add or modify definitions before registration
defs["MyFrontendNode"] = {
input: { required: { text: ["STRING", {}] } },
output: ["STRING"],
output_name: ["text"],
name: "MyFrontendNode",
display_name: "My Frontend Node",
category: "custom",
};
},
getCustomWidgets — Register custom widget types
getCustomWidgets(app) {
return {
MY_WIDGET(node, inputName, inputData, app) {
const widget = node.addWidget("text", inputName, "", () => {});
widget.serializeValue = () => widget.value;
return { widget };
},
};
},
beforeRegisterNodeDef — Modify node prototype
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "MyNode") {
// Chain onto prototype methods
const origOnCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
origOnCreated?.apply(this, arguments);
// Add custom widget, modify behavior, etc.
this.addWidget("button", "Run", null, () => {
console.log("Button clicked!");
});
};
}
},
nodeCreated — After node instance created
nodeCreated(node, app) {
if (node.comfyClass === "MyNode") {
// Modify this specific node instance
node.color = "#335";
}
},
setup — After app fully loaded
async setup(app) {
// Add event listeners, register UI components
app.api.addEventListener("executed", (event) => {
console.log("Node executed:", event.detail);
});
},
loadedGraphNode — When loading saved graph
loadedGraphNode(node, app) {
if (node.comfyClass === "MyNode") {
// Restore state from saved graph
}
},
registerCustomNodes — Register additional node types
registerCustomNodes(app) {
// Register custom LiteGraph node types
},
beforeRegisterVueAppNodeDefs — Modify node defs before Vue registration
beforeRegisterVueAppNodeDefs(defs, app) {
// Modify definitions before they reach the Vue app
},
beforeConfigureGraph / afterConfigureGraph
async beforeConfigureGraph(graphData, missingNodeTypes, app) {
// Before graph data is applied
},
async afterConfigureGraph(missingNodeTypes, app) {
// After graph is fully configured
},
getSelectionToolboxCommands — Add commands to selection toolbox
getSelectionToolboxCommands(selectedItem) {
// Return array of command IDs to show when item is selected
return ["my.ext.doSomething"];
},
onNodeOutputsUpdated — Execution outputs changed
onNodeOutputsUpdated(nodeOutputs) {
// nodeOutputs: Record<NodeLocatorId, output data>, fired when node outputs update
},
Authentication Hooks
onAuthUserResolved(user, app) {
// Fires when user authentication resolves
},
onAuthTokenRefreshed() {
// Fires when auth token is refreshed
},
onAuthUserLogout() {
// Fires when user logs out
},
Custom Widgets
Adding DOM Widgets
beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "MyNode") {
const origOnCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
origOnCreated?.apply(this, arguments);
const container = document.createElement("div");
container.innerHTML = `<input type="color" value="#ff0000">`;
container.querySelector("input").addEventListener("change", (e) => {
this.widgets.find(w => w.name === "color").value = e.target.value;
});
this.addDOMWidget("colorPicker", "custom", container, {
serialize: true,
getValue() { return container.querySelector("input").value; },
setValue(v) { container.querySelector("input").value = v; },
});
};
}
},
Widget Hooks
// Called before prompt is queued
// isPartialExecution is true when only selected nodes are queued
widget.beforeQueued = function ({ isPartialExecution } = {}) {
// Prepare widget value
};
// Called after prompt is queued
widget.afterQueued = function ({ isPartialExecution } = {}) {
// Reset or update widget (e.g. control_after_generate)
};
// Custom serialization
widget.serializeValue = function (node, index) {
return JSON.stringify(this.value);
};
Declarative Extension Properties
Commands
app.registerExtension({
name: "my.ext",
commands: [
{
id: "my.ext.doSomething",
label: "Do Something",
icon: "pi pi-bolt",
function: () => { console.log("Executed!"); },
},
],
});
Keybindings
keybindings: [
{
commandId: "my.ext.doSomething",
combo: { key: "d", ctrl: true, shift: true },
},
],
Settings
settings: [
{
id: "my.ext.mySetting",
name: "My Setting",
type: "boolean",
defaultValue: true,
onChange: (value) => { console.log("Setting changed:", value); },
},
{
id: "my.ext.mode",
name: "Processing Mode",
type: "combo",
options: ["fast", "quality", "balanced"],
defaultValue: "balanced",
},
],
Setting types: boolean, number, slider, knob, combo, radio, text, image, color, url, hidden, backgroundImage
Sidebar Tabs
async setup(app) {
app.extensionManager.registerSidebarTab({
id: "my-sidebar",
title: "My Panel",
icon: "pi pi-cog",
type: "custom",
render: (container) => {
container.innerHTML = "<h3>My Custom Panel</h3>";
},
destroy: () => {
// Cleanup
},
});
},
Bottom Panel Tabs
bottomPanelTabs: [
{
id: "my-panel",
title: "My Panel",
type: "custom",
render: (container) => {
container.innerHTML = "<div>Panel content</div>";
},
},
],
Menu Commands
menuCommands: [
{
path: ["My Extension"],
commands: ["my.ext.doSomething"],
},
],
About Page Badges
aboutPageBadges: [
{ label: "v1.0.0", url: "https://github.com/...", icon: "pi pi-github", severity: "warn" },
// severity is optional: "danger" | "warn"
],
Top Bar Badges
topbarBadges: [
{
text: "My Extension", // required
label: "BETA", // optional badge label
variant: "info", // "info" | "warning" | "error"
icon: "pi pi-star", // optional icon
tooltip: "Extension info", // optional tooltip
},
],
Action Bar Buttons
actionBarButtons: [
{
icon: "pi pi-bolt", // required
label: "My Action", // optional label
tooltip: "Run my action", // optional tooltip
onClick: () => { /* ... */ }, // required click handler
},
],
API Events
Listen to execution events:
// Node execution completed
app.api.addEventListener("executed", ({ detail }) => {
const { node, output } = detail;
// output contains images, text, etc.
});
// Execution progress
app.api.addEventListener("progress", ({ detail }) => {
const { value, max, node } = detail;
});
// Execution started/completed
app.api.addEventListener("execution_start", ({ detail }) => {});
app.api.addEventListener("execution_success", ({ detail }) => {});
app.api.addEventListener("execution_error", ({ detail }) => {});
// Status updates
app.api.addEventListener("status", ({ detail }) => {
const { exec_info } = detail;
});
Server-to-Client Communication
Python (server side):
from server import PromptServer
PromptServer.instance.send_sync(
"my_extension.update",
{"status": "complete", "data": result}
)
JavaScript (client side):
app.api.addEventListener("my_extension.update", ({ detail }) => {
console.log("Received:", detail);
});
Toast Notifications
app.extensionManager.toast.add({
severity: "info", // "success", "info", "warn", "error"
summary: "Title",
detail: "Message content",
life: 3000, // auto-dismiss after ms
});
Dialogs
// Confirmation dialog
const result = await app.extensionManager.dialog.confirm({
title: "Confirm Action",
message: "Are you sure?",
});
// Prompt dialog
const value = await app.extensionManager.dialog.prompt({
title: "Enter Value",
message: "Provide a name:",
defaultValue: "default",
});
ExtensionManager Utilities
Setting Access
// Read a setting value
const val = app.extensionManager.setting.get("my.ext.mySetting");
// Write a setting value
app.extensionManager.setting.set("my.ext.mySetting", newValue);
Execution Errors (read-only)
// Last node-level errors (keyed by node ID)
const nodeErrors = app.extensionManager.lastNodeErrors;
// Last execution-level error
const execError = app.extensionManager.lastExecutionError;
Markdown Rendering
// Render markdown to sanitized HTML (marked + DOMPurify, safe for innerHTML)
const html = app.extensionManager.renderMarkdownToHtml(markdownStr, baseUrl);
Context Menu Items
app.registerExtension({
name: "my.ext",
// Canvas right-click menu
getCanvasMenuItems(canvas) {
return [{
content: "My Action",
callback: () => { console.log("Canvas menu clicked"); },
}];
},
// Node right-click menu
getNodeMenuItems(node) {
if (node.comfyClass === "MyNode") {
return [{
content: "Custom Action",
callback: () => { console.log("Node:", node.id); },
}];
}
return [];
},
});
Node Instance Properties (LGraphNode Augmentations)
// Available on node instances:
node.comfyClass // ComfyUI node type name
node.isVirtualNode // true for frontend-only nodes
node.imgs // preview images array
node.imageIndex // current preview image index
// Callbacks:
node.onExecuted = function(output) { /* execution result */ };
node.onExecutionStart = function() { /* about to execute */ };
node.onDragOver = function(event) { /* file drag over */ };
node.onDragDrop = function(event) { /* file dropped */ };
Recent Breaking Changes (frontend ≥ 1.4x, mid-2026)
Changes extension authors should be aware of:
- Group nodes removed — group nodes can no longer be created; workflows containing them auto-convert to subgraphs on load.
GroupNodeHandler/GroupNodeConfigremain re-exported for backward compatibility only. Use subgraphs for new work. LiteGraph.use_uuidsremoved — node IDs are always numeric. Don't read or set this flag (UUIDs are still used internally for subgraph/slot IDs).LGraph.onGetNodeMenuOptionsremoved;LGraph.onBeforeChangedeprecated — assigning it still works but warns; useLGraphCanvas.onBeforeChangeinstead.IBaseWidget.entityIdremoved — widget identity is thewidgetIdbranded key (graphId:nodeId:name); promoted subgraph widgets are store-backed.node.sizeis Proxy-backed — bothnode.size[1] = handnode.size = [w, h]now correctly reflow Vue-rendered nodes; no polling workarounds needed.
Frontend Scripts API
Custom node JavaScript can import from the frontend's src/scripts/ modules. Imports use the Vite shim pattern:
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
Symbols are also accessible via window.comfyAPI.<module>.<export>.
Stability Levels
| Level | Modules | Notes |
|---|---|---|
| Stable | scripts/app, scripts/api | Guaranteed public API |
| Internal (console warning) | scripts/widgets, scripts/domWidget, scripts/utils, scripts/pnginfo, scripts/changeTracker, scripts/defaultGraph, scripts/metadata/* | Usable but may change |
| Deprecated | scripts/ui | Will be removed; use Vue alternatives |
Key Modules
scripts/api—ComfyApiclass:fetchApi(),queuePrompt(),getNodeDefs(), WebSocket events, settings, user data, system statsscripts/app—ComfyAppsingleton (app): graph operations,registerExtension(),extensionManager, clipboard, coordinate conversionscripts/widgets—ComfyWidgetsregistry (INT, FLOAT, STRING, BOOLEAN, COMBO, IMAGEUPLOAD, etc.),addValueControlWidgets()scripts/domWidget—addDOMWidget(),DOMWidgetImpl,ComponentWidgetImpl(Vue component wrapper)scripts/utils—clone(),addStylesheet(),uploadFile(),downloadBlob(), storage helpersscripts/pnginfo—getPngMetadata(),getWebpMetadata(),importA1111(), format-specific extractors
For full API details, see the API Reference.
See Also
comfyui-node-basics- Backend node structurecomfyui-node-packaging- Project structure with JS extensionscomfyui-node-inputs- Backend input types
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/jtydhr88/comfyui-custom-node-skills/comfyui-node-frontend">View comfyui-node-frontend on skillZs</a>