Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
melonask avatar

Create Comfyui Node

  • 2 installs
  • Updated April 19, 2026
  • melonask/create-comfyui-node-skills

Teaches building and debugging modern ComfyUI custom nodes and frontend extensions using the V3 Python schema and JavaScript extension APIs.

About

Guides creating ComfyUI custom nodes with the V3 io.ComfyNode schema and frontend JavaScript extensions, preferring modern patterns over legacy V1. A developer uses it to build, modify, or debug ComfyUI nodes, extensions, or plugins.

  • Modern V3 Python schema with io.ComfyNode, io.Schema, io.NodeOutput
  • Covers frontend JavaScript extensions via registerExtension

Create Comfyui Node by the numbers

  • 2 all-time installs (skills.sh)
  • Ranked #13,958 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
  • Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/melonask/create-comfyui-node-skills --skill create-comfyui-node

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs2
Last updatedApril 19, 2026
Repositorymelonask/create-comfyui-node-skills

What it does

Teaches building and debugging modern ComfyUI custom nodes and frontend extensions using the V3 Python schema and JavaScript extension APIs.

Files

SKILL.mdMarkdownGitHub ↗

ComfyUI Custom Node Development Guide

This skill teaches you how to build modern (V3 Schema) custom nodes and frontend JavaScript extensions for ComfyUI. Always prefer V3 patterns over legacy V1 unless maintaining existing code.

1. The Modern V3 Python Schema

Use comfy_api.latest for all new nodes. The V3 API uses object-oriented definitions with io.ComfyNode, io.Schema, and io.NodeOutput.

Basic Node Anatomy

from comfy_api.latest import ComfyExtension, io, ui

class MyCustomNode(io.ComfyNode):
    @classmethod
    def define_schema(cls) -> io.Schema:
        return io.Schema(
            node_id="MyPrefix_MyCustomNode",
            display_name="My Custom Node",
            category="my_category/sub_category",
            description="What this node does.",
            inputs=[
                io.Image.Input("image", tooltip="Input image"),
                io.Int.Input("strength", default=1, min=0, max=10),
                io.String.Input("text_prompt", multiline=True)
            ],
            outputs=[
                io.Image.Output(display_name="processed_image")
            ]
        )

    @classmethod
    def execute(cls, image, strength, text_prompt) -> io.NodeOutput:
        processed = image * strength
        return io.NodeOutput(processed, ui=ui.PreviewImage(processed, cls=cls))

Key differences from V1:

  • Inherit from io.ComfyNode instead of a plain class
  • Use define_schema() instead of INPUT_TYPES classmethod
  • execute is always a @classmethod (no self, use cls)
  • Return io.NodeOutput(...) instead of bare tuples
  • Node IDs should be globally unique — prefix them (e.g., AuthorName_NodeAction)

Extension Entrypoint

Replace NODE_CLASS_MAPPINGS / NODE_DISPLAY_NAME_MAPPINGS with ComfyExtension and comfy_entrypoint:

class MyExtension(ComfyExtension):
    async def get_node_list(self) -> list[type[io.ComfyNode]]:
        return [MyCustomNode]

async def comfy_entrypoint() -> MyExtension:
    return MyExtension()

V1 to V3 Property Mapping

V1 PropertyV3 Schema FieldNotes
RETURN_TYPESoutputs in SchemaList of Output objects
RETURN_NAMESdisplay_name in OutputPer-output display names
FUNCTIONAlways executeMethod name is standardized
CATEGORYcategory in SchemaString value
OUTPUT_NODEis_output_node in SchemaBoolean flag
DEPRECATEDis_deprecated in SchemaBoolean flag
IS_CHANGEDfingerprint_inputs()Renamed for clarity

2. Data Types and Tensors

ComfyUI relies on PyTorch tensors. Shape mismatches are the most common bug source.

Critical tensor shapes:

  • IMAGE: torch.Tensor shape [B, H, W, C] — channel-last (C=3 for RGB)
  • MASK: torch.Tensor shape [B, H, W] — often needs unsqueeze(-1) to [B, H, W, 1]
  • LATENT: dict with key "samples", tensor shape [B, C, H, W] — channel-first (C=4)

V3 Input Types Reference:

TypeExample
io.Int.Input("count", default=1, min=0, max=100)Integer with optional min/max/step
io.Float.Input("strength", default=1.0, min=0.0, max=10.0)Float with optional bounds
io.String.Input("text", default="", multiline=True)String; multiline for text areas
io.Boolean.Input("enabled", default=True)Boolean toggle
io.Combo.Input("mode", options=["A", "B", "C"])Dropdown selector
io.Image.Input("image")ComfyUI IMAGE tensor
io.Mask.Input("mask", optional=True)ComfyUI MASK tensor
io.Latent.Input("latent")ComfyUI LATENT dict
io.Model.Input("model")Model object
io.CLIP.Input("clip")CLIP object
io.VAE.Input("vae")VAE object
io.Conditioning.Input("positive")Conditioning object
io.Custom("MY_TYPE").Input("data")Custom datatype

All input types share these parameters: id (required), display_name, optional, tooltip, lazy, raw_link, advanced.

3. Advanced Node Features

Hidden Inputs

Access execution context like node ID and prompt metadata. In V3, use io.Hidden in the schema and cls.hidden in execute:

@classmethod
def define_schema(cls) -> io.Schema:
    return io.Schema(
        node_id="MyNode",
        inputs=[...],
        hidden=[io.Hidden.unique_id, io.Hidden.prompt],
    )

@classmethod
def execute(cls, ...) -> io.NodeOutput:
    node_id = cls.hidden.unique_id
    prompt_data = cls.hidden.prompt
    return io.NodeOutput(result)

Available hidden values: unique_id, prompt, extra_pnginfo, dynprompt, auth_token_comfy_org, api_key_comfy_org.

Lazy Evaluation

Skip evaluating inputs that aren't needed. Mark inputs as lazy=True and implement check_lazy_status:

@classmethod
def define_schema(cls) -> io.Schema:
    return io.Schema(
        node_id="MySwitch",
        inputs=[
            io.Boolean.Input("switch"),
            io.Image.Input("on_false", lazy=True),
            io.Image.Input("on_true", lazy=True),
        ],
        outputs=[io.Image.Output()],
    )

@classmethod
def check_lazy_status(cls, switch, on_false=None, on_true=None):
    if switch and on_true is None:
        return ["on_true"]
    if not switch and on_false is None:
        return ["on_false"]
    return []

@classmethod
def execute(cls, switch, on_false, on_true) -> io.NodeOutput:
    return io.NodeOutput(on_true if switch else on_false)

Output Nodes

Mark nodes that save files or trigger execution:

io.Schema(
    node_id="MySaveNode",
    is_output_node=True,
    ...
)

DynamicCombo (Dropdown-Dependent Inputs)

Show/hide inputs based on a dropdown selection:

io.DynamicCombo.Input("resize_type", options=[
    io.DynamicCombo.Option("scale by dimensions", [
        io.Int.Input("width", default=512, min=0, max=8192),
        io.Int.Input("height", default=512, min=0, max=8192),
    ]),
    io.DynamicCombo.Option("scale by multiplier", [
        io.Float.Input("multiplier", default=1.0, min=0.01, max=8.0),
    ]),
]),

In execute, the DynamicCombo parameter becomes a dict containing the selected key and its sub-inputs.

MatchType (Generic Type Matching)

Create inputs/outputs that dynamically snap to the same connected type:

template = io.MatchType.Template("switch")
io.MatchType.Input("on_false", template=template, lazy=True),
io.MatchType.Input("on_true", template=template, lazy=True),
# ...
io.MatchType.Output(template=template, display_name="output"),

Autogrow (Variable Number of Inputs)

Automatically add input sockets as the user connects nodes:

template = io.Autogrow.TemplatePrefix(
    input=io.Image.Input("image"),
    prefix="image",
    min=2,
)
io.Schema(
    inputs=[io.Autogrow.Input("images", template=template)],
    outputs=[io.Image.Output()],
)

# In execute, images is a dict mapping input names to their values
@classmethod
def execute(cls, images: io.Autogrow.Type) -> io.NodeOutput:
    image_list = list(images.values())
    return io.NodeOutput(torch.cat(image_list, dim=0))

Async Execution and Progress Reporting

from comfy_api.latest import ComfyAPI

api = ComfyAPI()

class AsyncNode(io.ComfyNode):
    @classmethod
    async def execute(cls, images) -> io.NodeOutput:
        total = len(images)
        for i, img in enumerate(images):
            # process img...
            await api.execution.set_progress(
                value=i + 1,
                max_value=total,
                preview_image=img,
            )
        return io.NodeOutput(result)

4. UI Helpers (Preview and Save)

V3 provides built-in helpers in the ui module for common output patterns:

from comfy_api.latest import ui

# Preview image in node
return io.NodeOutput(images, ui=ui.PreviewImage(images, cls=cls))

# Preview mask (auto-converts to 3-channel)
return io.NodeOutput(mask, ui=ui.PreviewMask(mask, cls=cls))

# Preview audio
return io.NodeOutput(audio, ui=ui.PreviewAudio(audio, cls=cls))

# Preview text
return io.NodeOutput(ui=ui.PreviewText("Result text"))

# Save image with metadata
return io.NodeOutput(ui=ui.ImageSaveHelper.get_save_images_ui(
    images=images, filename_prefix=prefix, cls=cls
))

# Pass cls=cls to embed workflow metadata when using hidden prompt/extra_pnginfo

5. Node Replacement (Migration)

If renaming or restructuring nodes, register replacements so existing workflows auto-upgrade. Use ComfyAPI in your extension's on_load:

from comfy_api.latest import ComfyAPI, ComfyExtension, io

api = ComfyAPI()

class MyExtension(ComfyExtension):
    async def on_load(self) -> None:
        await api.node_replacement.register(io.NodeReplace(
            new_node_id="MyPrefix_NewNode",
            old_node_id="OldNodeName",
            old_widget_ids=["param1", "param2"],
            input_mapping=[
                {"new_id": "image", "old_id": "input_image"},
                {"new_id": "method", "set_value": "lanczos"},
            ],
            output_mapping=[
                {"new_idx": 0, "old_idx": 0},
            ],
        ))

6. Frontend JavaScript Extensions

Declare the web directory in your Python module:

WEB_DIRECTORY = "./js"

Place .js files in that directory. Use modern extension hooks, not legacy prototype monkey-patching.

Basic Extension

import { app } from "../../scripts/app.js";

app.registerExtension({
  name: "MyNamespace.MyExtension",
  async setup() {
    console.log("Extension loaded");
  },
});

Context Menus

app.registerExtension({
  name: "MyNamespace.MyMenus",
  getCanvasMenuItems(canvas) {
    return [
      null,
      {
        content: "My Canvas Action",
        callback: () => {
          /* ... */
        },
      },
    ];
  },
  getNodeMenuItems(node) {
    if (node.comfyClass === "MyPrefix_MyNode") {
      return [
        {
          content: "Do Something",
          callback: () => {
            /* ... */
          },
        },
      ];
    }
    return [];
  },
});

Commands, Keybindings, and Selection Toolbox

app.registerExtension({
  name: "MyNamespace.Commands",
  commands: [
    {
      id: "myExt.doAction",
      label: "Do Action",
      function: () => {
        /* ... */
      },
    },
  ],
  keybindings: [
    {
      combo: { key: "k", ctrl: true, shift: true },
      commandId: "myExt.doAction",
    },
  ],
  getSelectionToolboxCommands: (selectedItem) => {
    const count = app.canvas.selectedItems?.size || 0;
    if (count > 1) return ["myExt.doAction"];
    return [];
  },
});

Sidebar and Bottom Panel Tabs

app.extensionManager.registerSidebarTab({
  id: "mySidebar",
  icon: "pi pi-list",
  title: "My Tools",
  type: "custom",
  render: (el) => {
    el.innerHTML = "<div>Content</div>";
  },
});

app.registerExtension({
  name: "MyNamespace.BottomTab",
  bottomPanelTabs: [
    {
      id: "myLogs",
      title: "Custom Logs",
      type: "custom",
      render: (el) => {
        el.innerHTML = "<div>Log data</div>";
      },
    },
  ],
});

Dialogs, Toasts, Settings

// Prompt dialog
app.extensionManager.dialog
  .prompt({ title: "Input", message: "Enter value:" })
  .then((result) => {
    /* result is string or null */
  });

// Confirm dialog
app.extensionManager.dialog
  .confirm({ title: "Confirm", message: "Continue?" })
  .then((result) => {
    /* result is boolean or null */
  });

// Toast notification
app.extensionManager.toast.add({
  severity: "success",
  summary: "Done!",
  life: 3000,
});

// Settings
app.registerExtension({
  name: "MyNamespace.Settings",
  settings: [
    {
      id: "MyExt.enableDebug",
      name: "Enable Debug Mode",
      type: "boolean",
      defaultValue: false,
      onChange: (newVal) => {
        console.log("Debug:", newVal);
      },
    },
  ],
});

About Page Badges and Top Bar Menus

app.registerExtension({
  name: "MyNamespace.Badges",
  aboutPageBadges: [
    {
      label: "GitHub",
      url: "https://github.com/my/repo",
      icon: "pi pi-github",
    },
  ],
  menuCommands: [
    { path: ["Extensions", "My Extension"], commands: ["myExt.doAction"] },
  ],
});

For deeper frontend APIs (subgraphs, i18n, node docs, detailed JS object reference), see references/advanced_frontend_and_i18n.md.

7. Directory Structure

Use this standard layout for ComfyUI Manager compatibility:

my-custom-nodes/
├── __init__.py          # Exports WEB_DIRECTORY and comfy_entrypoint
├── nodes.py             # V3 ComfyNode definitions
├── requirements.txt     # Pip dependencies (Manager auto-installs)
├── install.py           # Optional: run by Manager on install
├── js/                 # WEB_DIRECTORY for frontend extensions
│   └── my_extension.js
├── subgraphs/           # .json files become available as subgraph blueprints
└── example_workflows/   # .json + .jpg files appear in Workflow Templates browser

8. Checklist: AI Instructions

When building ComfyUI custom nodes:

1. Ask clarifying questions if data types or tensor shapes are ambiguous 2. Always use V3 API (from comfy_api.latest import io). Only generate V1 INPUT_TYPES dicts if maintaining existing legacy code 3. Prefix node IDs to ensure global uniqueness (e.g., AuthorName_NodeAction) 4. Remember: Images are [B, H, W, 3] (channel-last), Latents are {"samples": [B, 4, H, W]} (channel-first), Masks are [B, H, W] 5. For UI modifications, use modern extension hooks (getCanvasMenuItems, getNodeMenuItems, commands, settings) rather than LGraphCanvas.prototype monkey-patching 6. When scaffolding a new package, include example_workflows/ and requirements.txt for Manager compatibility 7. Return io.NodeOutput(...) from execute, always — even io.NodeOutput() for no-output nodes 8. Use ui.PreviewImage / ui.ImageSaveHelper for common output patterns instead of manual file handling 9. For lazy evaluation, add lazy=True to input definitions and implement check_lazy_status 10. Use io.DynamicCombo instead of manual JS widget toggling for mode-dependent inputs

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.