> ## Documentation Index
> Fetch the complete documentation index at: https://docs.comfy.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Subgraphs

> Working with subgraphs in ComfyUI extensions: node IDs, graph traversal, events, widget promotion, and cleanup.

## Overview

Subgraphs let users group nodes into reusable, nestable components. Each subgraph is its own `LGraph` with a UUID. For the user-facing guide, see [Subgraphs](/interface/features/subgraph).

## Node Identifiers

ComfyUI uses three distinct node identifier types. Using the wrong one causes silent failures.

| Type | Format | Used for |
| - | - | - |
| `node.id` | `42` (number) | Local to its immediate graph level. `graph.getNodeById(id)` |
| Execution ID | `"1:2:3"` (colon-separated string) | Backend progress messages, `UNIQUE_ID` |
| Locator ID | `"<uuid>:<localId>"` or `"<localId>"` | UI state: badges, errors, images |

To construct a node's locator ID from within an extension:

```javascript theme={null}
function getLocatorId(node) {
  const graphId = node.graph?.id
  return graphId ? `${graphId}:${node.id}` : String(node.id)
}
```

## Traversing Nodes

### Current layer only

```javascript theme={null}
for (const node of app.graph.nodes) {
  console.log(node.id, node.type)
}
```

### All nodes recursively

To walk into nested subgraphs, use a recursive helper that calls a callback on every node:

```javascript theme={null}
function walkGraph(graph, callback) {
  for (const node of graph.nodes ?? []) {
    callback(node, graph)
    if (node.subgraph) walkGraph(node.subgraph, callback)
  }
}
```

Full example:

```javascript theme={null}
import { app } from "../../scripts/app.js"

function walkGraph(graph, callback) {
  for (const node of graph.nodes ?? []) {
    callback(node, graph)
    if (node.subgraph) walkGraph(node.subgraph, callback)
  }
}

app.registerExtension({
  name: "MyExtension.SubgraphWalker",
  async afterConfigureGraph() {
    walkGraph(app.graph, (node, graph) => {
      console.log(`[${graph.id ?? "root"}] node ${node.id}: ${node.type}`)
    })
  }
})
```

## Root vs Active Graph

| You want to... | Use |
| - | - |
| Operate on all nodes in the workflow | `app.graph` (root) |
| Operate on only the visible layer | `app.canvas?.graph` |
| Access a specific subgraph | `someNode.subgraph` |

```javascript theme={null}
// All nodes (including nested subgraphs)
walkGraph(app.graph, (node) => { /* ... */ })

// Only nodes the user currently sees
for (const node of app.canvas?.graph?.nodes ?? []) { /* ... */ }
```

## Events

### Subgraph-level events

Dispatched on `subgraph.events`:

| Event | Payload | When |
| - | - | - |
| `widget-promoted` | `{ widget, subgraphNode }` | Widget promoted to parent node |
| `widget-demoted` | `{ widget, subgraphNode }` | Widget removed from parent node |
| `input-added` | `{ input }` | Input slot added |
| `removing-input` | `{ input, index }` | Input slot being removed |
| `output-added` | `{ output }` | Output slot added |
| `removing-output` | `{ output, index }` | Output slot being removed |
| `renaming-input` | `{ input, index, oldName, newName }` | Input slot renamed |
| `renaming-output` | `{ output, index, oldName, newName }` | Output slot renamed |

### Canvas-level events

Dispatched on `app.canvas.canvas` (the HTML canvas element):

| Event | Payload | When |
| - | - | - |
| `subgraph-opened` | `{ subgraph, closingGraph, fromNode }` | User navigates into a subgraph |
| `subgraph-converted` | `{ subgraphNode }` | Selection converted to a subgraph |

### Listening pattern

```javascript theme={null}
import { app } from "../../scripts/app.js"

app.registerExtension({
  name: "MyExtension.SubgraphEvents",
  async setup() {
    app.canvas.canvas.addEventListener("subgraph-opened", (e) => {
      const { subgraph, fromNode } = e.detail
      console.log(`Opened subgraph from node ${fromNode.id}`)
    })
  }
})
```

## Widget Promotion

When a `SubgraphInput` connects to a widget inside a subgraph, a copy of that widget appears on the parent subgraph node. This fires `widget-promoted`. Removing the connection fires `widget-demoted`.

<Warning>
  Widget promotion behavior is still evolving and may change in future releases.
</Warning>

```javascript theme={null}
import { app } from "../../scripts/app.js"

function walkGraph(graph, callback) {
  for (const node of graph.nodes ?? []) {
    callback(node, graph)
    if (node.subgraph) walkGraph(node.subgraph, callback)
  }
}

app.registerExtension({
  name: "MyExtension.WidgetPromotion",
  async afterConfigureGraph() {
    walkGraph(app.graph, (node) => {
      if (!node.subgraph) return
      if (node._promCleanup) node._promCleanup.abort()
      const controller = new AbortController()
      node._promCleanup = controller
      const { signal } = controller

      node.subgraph.events.addEventListener("widget-promoted", (e) => {
        console.log(`Widget "${e.detail.widget.name}" promoted`)
      }, { signal })

      node.subgraph.events.addEventListener("widget-demoted", (e) => {
        console.log(`Widget "${e.detail.widget.name}" demoted`)
      }, { signal })

      const origRemoved = node.onRemoved
      node.onRemoved = function () {
        controller.abort()
        origRemoved?.apply(this, arguments)
      }
    })
  }
})
```

## Cleanup

Use an `AbortController` to clean up all event listeners when a node is removed.

```javascript theme={null}
import { app } from "../../scripts/app.js"

app.registerExtension({
  name: "MyExtension.Cleanup",
  async nodeCreated(node) {
    if (!node.subgraph) return

    const controller = new AbortController()
    const { signal } = controller

    node.subgraph.events.addEventListener("input-added", (e) => {
      console.log(`Input added: ${e.detail.input.name}`)
    }, { signal })

    node.subgraph.events.addEventListener("removing-input", (e) => {
      console.log(`Input removing: ${e.detail.input.name}`)
    }, { signal })

    const origRemoved = node.onRemoved
    node.onRemoved = function () {
      controller.abort()
      origRemoved?.apply(this, arguments)
    }
  }
})
```

<Tip>
  `onRemoved` can also fire during subgraph conversion, not just deletion. Guard teardown logic if you need to preserve state across restructuring.
</Tip>

## See Also

* [Subgraphs (User Guide)](/interface/features/subgraph)
* [Extension Hooks](/custom-nodes/js/javascript_hooks)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.