From e44c2a53ceff7fe14e66f568f577c44e21f38b6b Mon Sep 17 00:00:00 2001 From: bymyself Date: Fri, 6 Feb 2026 18:54:09 -0800 Subject: [PATCH] docs: add subgraph developer guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add custom-nodes/js/subgraphs.mdx — primary developer guide covering node ID types, recursive traversal, subgraph events, widget promotion, cleanup patterns (AbortController), and known limitations - Add custom-nodes/backend/subgraphs.mdx — backend guide for UNIQUE_ID behavior inside subgraphs - Update docs.json navigation for both pages - Add cross-links from expansion.mdx and subgraph.mdx Fixes #724 Amp-Thread-ID: https://ampcode.com/threads/T-019c3604-fb97-775b-baea-5ecf94822c8e --- custom-nodes/backend/expansion.mdx | 5 + custom-nodes/backend/subgraphs.mdx | 40 ++++++ custom-nodes/js/subgraphs.mdx | 221 +++++++++++++++++++++++++++++ docs.json | 2 + interface/features/subgraph.mdx | 4 + 5 files changed, 272 insertions(+) create mode 100644 custom-nodes/backend/subgraphs.mdx create mode 100644 custom-nodes/js/subgraphs.mdx diff --git a/custom-nodes/backend/expansion.mdx b/custom-nodes/backend/expansion.mdx index 0b147e71..6144be77 100644 --- a/custom-nodes/backend/expansion.mdx +++ b/custom-nodes/backend/expansion.mdx @@ -46,3 +46,8 @@ Even if you don't want to use the `GraphBuilder` for actually building the graph ### Efficient Subgraph Caching While you can pass non-literal inputs to nodes within the subgraph (like torch tensors), this can inhibit caching *within* the subgraph. When possible, you should pass links to subgraph objects rather than the node itself. (You can declare an input as a `rawLink` within the input's [Additional Parameters](./datatypes#additional-parameters) to do this easily.) + +## Related + +- [Subgraphs (Python)](/custom-nodes/backend/subgraphs) — How `UNIQUE_ID` behaves inside subgraphs +- [Subgraphs (Developer Guide)](/custom-nodes/js/subgraphs) — Full guide for extension authors diff --git a/custom-nodes/backend/subgraphs.mdx b/custom-nodes/backend/subgraphs.mdx new file mode 100644 index 00000000..dfe74eb7 --- /dev/null +++ b/custom-nodes/backend/subgraphs.mdx @@ -0,0 +1,40 @@ +--- +title: "Subgraphs (Python)" +description: "How UNIQUE_ID and execution behave when your Python node runs inside a subgraph." +--- + +## UNIQUE_ID in Subgraphs + +When a Python node executes inside a subgraph, its `UNIQUE_ID` becomes a colon-separated +execution path instead of a simple number. + +| Context | UNIQUE_ID | Meaning | +|---------|-----------|---------| +| Root graph | `"789"` | Node 789 at top level | +| One level deep | `"45:789"` | Node 789 inside subgraph-node 45 | +| Two levels deep | `"12:45:789"` | Node 789 → subgraph 45 → subgraph 12 | + +```python +def process(self, **kwargs): + unique_id = kwargs.get('unique_id', '') + # Always get local ID from the last segment + local_id = unique_id.split(':')[-1] + + # Use the full unique_id for cache keys and logging + cache_key = f"my_node_{unique_id}" +``` + + +Never assume `UNIQUE_ID` is a simple integer. Always treat it as a string and split on `:` +when you need the local ID. + + +## Execution Flattening + +At execution time, the frontend flattens the subgraph hierarchy into a flat list of +`ExecutableNodeDTO` objects. Each gets a compound execution ID. The backend sees only +the flattened graph — it has no awareness of the subgraph structure. + +For runtime subgraph creation in Python, see [Node Expansion](/custom-nodes/backend/expansion). + +For the full subgraph developer guide (frontend/TypeScript), see [Subgraphs](/custom-nodes/js/subgraphs). diff --git a/custom-nodes/js/subgraphs.mdx b/custom-nodes/js/subgraphs.mdx new file mode 100644 index 00000000..92e5e6cd --- /dev/null +++ b/custom-nodes/js/subgraphs.mdx @@ -0,0 +1,221 @@ +--- +title: "Subgraphs" +description: "Developer guide for working with subgraphs programmatically in ComfyUI extensions." +--- + + +The subgraph API is under active development. Some internal APIs referenced here may change. +See [GitHub Issue #8137](https://github.com/Comfy-Org/ComfyUI_frontend/issues/8137) for planned improvements. + + +## Overview + +Subgraphs let users group nodes into reusable components. This guide covers what extension +developers need to know when their code interacts with nodes inside subgraphs. + +For the user-facing subgraph guide, see [Subgraph - Simplify your workflow](/interface/features/subgraph). +For backend node expansion (runtime subgraph creation in Python), see [Node Expansion](/custom-nodes/backend/expansion). + +## Node ID Types + +Nodes in subgraphs have three different identifiers. Using the wrong one is the most common source of bugs. + +| Type | Format | Example | Scope | Where you see it | +|------|--------|---------|-------|-----------------| +| **Local Node ID** | `number` | `123` | Unique within one graph level | `node.id`, `this.id` | +| **Node Locator ID** | `uuid:localId` | `"a1b2…:123"` | Globally unique, stable across blueprint instances | Frontend node identification | +| **Node Execution ID** | `path:path:id` | `"45:14:789"` | Globally unique per execution instance | `UNIQUE_ID` in Python, `send_sync` events | + +### Local Node ID + +The basic `node.id` — a number unique only within its immediate graph level. Two nodes in different +subgraphs can share the same local ID. + +### Node Locator ID + +Combines the subgraph's UUID with the local node ID: `:`. +Root graph nodes use just their local ID as a string. Stable across all instances of the same +subgraph blueprint. + +```typescript +// Parsing a locator ID +const parts = locatorId.split(':') +if (parts.length === 2) { + const [subgraphUuid, localNodeId] = parts + // Node is inside a subgraph +} else { + // Node is in the root graph +} +``` + +### Node Execution ID + +A colon-separated path of local node IDs from root to target: `"45:14:789"` means node 789 +inside subgraph-node 14 inside subgraph-node 45. + +This is what Python nodes receive as `UNIQUE_ID` and what `send_sync` events use. + + +Custom nodes currently cannot determine their own execution ID from within the subgraph. +This is a known limitation tracked in [#8137](https://github.com/Comfy-Org/ComfyUI_frontend/issues/8137). + + +```python +# In a Python node's execute method: +def process(self, **kwargs): + unique_id = kwargs.get('unique_id', '') + # If inside subgraphs: "45:14:789" + # Local ID is the last segment + local_id = unique_id.split(':')[-1] +``` + +## Traversing Nodes + +### Current Layer Only + +To iterate nodes in the current graph level (non-recursive): + +```typescript +for (const node of graph.nodes) { + // Only nodes at this level — does NOT enter subgraphs +} +``` + +### All Nodes Recursively + +To find nodes across all subgraph levels, use recursive descent: + +```typescript +function forEachNodeRecursive(graph, fn) { + for (const node of graph.nodes) { + fn(node) + if (node.isSubgraphNode?.() && node.subgraph) { + forEachNodeRecursive(node.subgraph, fn) + } + } +} + +// Usage +forEachNodeRecursive(app.graph, (node) => { + console.log(node.id, node.type) +}) +``` + + +The `isSubgraphNode()` method is the reliable way to detect subgraph nodes. Check for `.subgraph` +property to access the inner graph. + + + +Internal utilities in `graphTraversalUtil.ts` (like `forEachNode`, `getNodeByExecutionId`) exist +but are **not importable** from custom node extensions. The pattern above is the extension-safe equivalent. + + +## Subgraph Events + +The `Subgraph` class fires typed events via `CustomEventTarget`. Listen using standard +`addEventListener` with `AbortController` for cleanup. + +| Event | When Fired | Cancellable | +|-------|-----------|-------------| +| `adding-input` | Before input slot added | No | +| `input-added` | After input slot added | No | +| `removing-input` | Before input slot removed | Yes | +| `adding-output` | Before output slot added | No | +| `output-added` | After output slot added | No | +| `removing-output` | Before output slot removed | Yes | +| `renaming-input` | Input slot renamed | No | +| `renaming-output` | Output slot renamed | No | +| `widget-promoted` | Widget promoted to subgraph surface | No | +| `widget-demoted` | Promoted widget removed | No | +| `input-connected` | SubgraphInput connected internally | No | +| `input-disconnected` | SubgraphInput disconnected | No | + +### Listening Pattern + +```typescript +const controller = new AbortController() + +subgraph.events.addEventListener('input-added', (e) => { + const { input } = e.detail + console.log(`New input: ${input.name} (${input.type})`) +}, { signal: controller.signal }) + +// Cleanup — removes all listeners attached with this controller +controller.abort() +``` + +## Widget Promotion + +When a `SubgraphInput` connects to a node input that has an associated widget, the widget +is "promoted" — a copy appears on the SubgraphNode's surface. + + + + SubgraphInput connects to a node input with an attached widget. + + + `SubgraphNode._setWidget()` creates a copy via `widget.createCopyForNode()`. + The promoted widget's name/label become read-only, delegating to the SubgraphInput. + + + `widget-promoted` fires. On disconnect: `widget-demoted` fires and the widget is removed. + + + +Widget type compatibility is validated by `SubgraphInput.matchesWidget()`, checking type, min, max, +step, and precision. + + +The widget promotion API is still evolving. PR #8352 (dynamic widget promotion) was closed without merging. + + +## Cleanup Patterns + +Subgraph nodes use `AbortController`-based lifecycle management. Follow this pattern in extensions +that interact with subgraph events. + +### The Pattern + +```typescript +class MyExtensionState { + private _controller = new AbortController() + + setup(subgraphNode) { + const { signal } = this._controller + + subgraphNode.subgraph.events.addEventListener('input-added', (e) => { + // Handle event + }, { signal }) + + subgraphNode.subgraph.events.addEventListener('widget-promoted', (e) => { + // Handle event + }, { signal }) + } + + cleanup() { + // One call removes ALL listeners + this._controller.abort() + } +} +``` + +### What SubgraphNode.onRemoved() Does + +1. Aborts the main `_eventAbortController` (removes all subgraph event listeners) +2. Dispatches `widget-demoted` for all promoted widgets +3. Aborts per-input listener controllers +4. Fires `onRemoved` for inner nodes and `onNodeRemoved` callbacks + + +If you store state keyed by node ID, remember that local IDs are not globally unique. +Use execution IDs or locator IDs as keys when tracking nodes across subgraph boundaries. + + +## Known Limitations + +- **No self-identification:** Nodes inside subgraphs cannot determine their own execution ID or locator ID ([#8137](https://github.com/Comfy-Org/ComfyUI_frontend/issues/8137)) +- **Internal-only traversal utilities:** `graphTraversalUtil.ts` functions are not importable from extensions +- **Widget promotion evolving:** Dynamic promotion not yet stable +- **Linked subgraphs broken:** Editing one instance can affect others ([#6639](https://github.com/Comfy-Org/ComfyUI_frontend/issues/6639)) +- **Widget renaming inconsistent:** Names may differ inside vs. outside subgraphs ([#7739](https://github.com/Comfy-Org/ComfyUI_frontend/issues/7739)) diff --git a/docs.json b/docs.json index a9cc70c7..c1bf66d6 100644 --- a/docs.json +++ b/docs.json @@ -605,6 +605,7 @@ "custom-nodes/backend/more_on_inputs", "custom-nodes/backend/lazy_evaluation", "custom-nodes/backend/expansion", + "custom-nodes/backend/subgraphs", "custom-nodes/backend/lists", "custom-nodes/backend/snippets", "custom-nodes/backend/tensors" @@ -627,6 +628,7 @@ "custom-nodes/js/javascript_commands_keybindings", "custom-nodes/js/javascript_topbar_menu", "custom-nodes/js/context-menu-migration", + "custom-nodes/js/subgraphs", "custom-nodes/js/javascript_examples", "custom-nodes/i18n" ] diff --git a/interface/features/subgraph.mdx b/interface/features/subgraph.mdx index 830b0bcc..898c087e 100644 --- a/interface/features/subgraph.mdx +++ b/interface/features/subgraph.mdx @@ -11,6 +11,10 @@ icon: "share-nodes" - Some features like converting subgraph back to nodes will be supported in the future + +For developer documentation on working with subgraphs programmatically, see the [Subgraph Developer Guide](/custom-nodes/js/subgraphs). + +