Skip to main content

Subgraphs

A subgraph is a block of your graph collapsed into a single node. Unlike a preset — which is flattened once, when you drop it — a subgraph keeps a live definition: every instance of it points at the same block, so editing the block changes every place you used it.

Subgraphs are local to the graph file they live in. The definition travels in the graph's own subgraphs list, so a saved or exported graph is still one portable artifact with nothing to install alongside it.

Collapsing a selection​

  1. Select two or more nodes (Shift+click, or Shift+drag a box around them).
  2. Right-click one of them and choose Collapse to subgraph.
  3. Give the block a name. The default is Subgraph; you can rename it later from the breadcrumb.

The selection is replaced by one instance node. Every edge that crossed the selection becomes a boundary port on that node, named after the inner port it stands for. Trigger edges from Start are remembered too: one edge into the block fans back out to exactly the nodes that were triggered before.

Collapse and expand are each a single undo step — one Ctrl+Z puts the graph back exactly as it was, definition included.

When collapse refuses​

Collapse says no rather than producing a graph that reads wrong:

RefusalWhy
Fewer than two nodes selectedA block of one node is just the node.
A Start node is selectedStart marks where the whole graph begins. Inside a reusable block it would mean every instance adds its own entry point.
A note is selectedNotes are annotations, not computation.
A node between two selected nodes is left outThe block would feed that node and be fed by it — a loop on the canvas that the flattened graph does not actually have. The message names the nodes to add to your selection.
An input of a selected node has more than one wire, at least one from outside the selectionAn input takes one source. Only a graph saved before that rule can still hold such an input, and a block would hide which wire it reads. Delete the extra wires, or route them through a Switch, then collapse again.
The graph is open read-onlyA read-only tab (for example a graph saved by a newer CodefyUI) cannot be changed. The message is "This graph is open read-only".

Editing a block​

Double-click an instance, or right-click it and select Enter subgraph, to open its definition on the canvas. The breadcrumb at the top shows a path such as Main ▸ MyBlock. You can drag, connect, delete, edit parameters, and undo changes. To open Node details, select the instance and press Enter. Its Subgraph tab maps each boundary port to an internal node and port, and also provides an Enter subgraph button. A block with nothing in it says so on its canvas.

Undo inside a block stays inside it: the block gets its own history while you are in there, and your outer history is put back when you leave — so you can never accidentally undo your way out through the boundary.

Leaving a block is itself one undo step. Everything you changed inside lands as a single entry on the outer history, so one Ctrl+Z after you come out reverts the whole visit and leaves whatever you did outside the block untouched. Entering and leaving without changing anything adds no undo entry at all.

Click the block's name in the breadcrumb to rename it (not offered on a graph opened read-only), Back to leave one level, or Main to jump all the way out.

Deleting a node inside a block also removes the boundary port it provided, and the outer edge that named that port goes with it.

Two instances, one definition​

Copy an instance node, or collapse the same block twice, and both nodes point at the same definition. Edit it through either one and both change — this is the reuse a flattened preset cannot give you.

Delete the last instance of a block and its definition stops being saved with the graph, so a file never carries a block nothing on the canvas can reach. Undo brings both back.

:::note Per-instance parameters are out of scope in v1 Two instances of a subgraph are identical blocks. There is no way to give one instance a different learning rate from another; if you need that, keep the parameter outside the block and wire it in. Per-instance overrides are a follow-up. :::

Running a graph with subgraphs​

Nothing special is required. Before a run the server inlines every instance, exactly as it already inlines presets, so a collapsed graph executes precisely like the graph it was collapsed from. Collapse, run, expand, run again: same numbers.

While the run is in flight the instance node shows one aggregate status — running when the first node inside starts, completed when the last one finishes, error or interrupted the moment anything inside fails or stops early.

Open a block while a run is in flight, or start the run from inside it, and each node inside shows its own status: running, completed, cached, skipped, error, interrupted or bypassed. A block nested inside the open one shows the aggregate of what is inside it. Two copies of one block keep their statuses apart, and opening a block after the run shows what its nodes did in that run.

What a node inside a block produces reaches the same places it would at the top level. A picture, clip or chart it draws appears in the Execution Log, labelled with the instance and the node inside it (blk › plot), and on that node's Inspector and Node details rows while the block is open, whether or not Record node outputs is on. The instance node's output edges show the values its exposed ports carried. A node that was skipped because a node before it failed says so in the Inspector, instead of waiting for the run or reporting expired data.

Inner nodes get namespaced ids while they run, <instance>/<node>, which is what you see in the Teaching Inspector and in validation messages. Select an instance node and its Steps and Backward tabs list what the nodes inside it recorded, each entry under that node's id inside the block.

Validation across the boundary​

Because validation inlines instances too, it checks the block's insides: port types across the boundary, required inputs left unconnected, and — the one you cannot see on the canvas — a cycle inside a definition. Those are reported with a path naming both sides:

Graph contains a cycle: blk1/relu -> blk1/conv -> blk1/relu
(crosses subgraph instance(s): blk1)

Every enclosing block is named, so a loop two boundaries down points at both the block you can see and the one inside it (crosses subgraph instance(s): blk1, blk1/inner).

A subgraph that contains itself, directly or through another subgraph, is refused by name before anything runs. So is a trigger wired into a block with no nodes inside, which has nothing to start. Notes inside a block are ignored, so a block holding only notes counts as empty.

Nesting is allowed up to 10 levels deep, the same budget preset nesting gets.

What is stored where​

In a project directory the split follows the same rule as the top level:

  • graphs/<name>.graph.json — the definitions: nodes, edges, and the interface.
  • layout/<name>.layout.json — subgraphPositions, the positions of the nodes inside each block.

So rewiring a block shows up in a review, and dragging inside one does not.

Exporting to Python​

Export → Export as Python emits one function per subgraph instance:

# ========================== Subgraph functions ==========================


def subgraph_myblock(ctx, results, provided):
"subgraph 'myblock' - instance 'blk1'."
# ScalarMultiply -> Print
results['blk1/mul'] = n03_scalarmultiply(
ctx,
tensor=_port(results['src'], 'tensor'),
)
results['blk1/p'] = n04_print(
ctx,
value=_port(results['blk1/mul'], 'tensor'),
)


# ============================ Flow functions ============================


def flow_1(ctx, results, provided):
'Start -> TensorCreate -> ScalarMultiply -> Print'
start = results['start'] = n01_start(ctx)
src = results['src'] = n02_tensorcreate(ctx)
subgraph_myblock(ctx, results, provided)

The block structure survives into the exported file instead of dissolving into a flat run of node calls. Each generated node function also carries a comment saying which subgraph it came from.

One exception, and the export says so when it applies: a block can only become a single function if all of its nodes can run back to back. If something outside the block has to run in the middle of it — because you wired the block into a node that then feeds it again, or because the block contains a node with no inputs of its own — the export emits its nodes in their real running order instead, with a comment naming what got in the way. The script still runs, and still computes what the canvas computes; it just does not get the extra function.

Limitations in v1​

  • No per-instance parameter overrides (see the note above).
  • Subgraphs are local to one graph; there is no shared library of them yet.
  • A GraphInput / GraphOutput node inside a subgraph is not part of the graph's published API contract — keep those at the top level.
  • The canvas provides Bypass for an instance node through its context menu or Ctrl/Cmd+B. Execution then fails validation and names the instance: Bypass is not supported on subgraph instance(s): blk1. A bypass can forward a value only between ports declared by a node class, and boundary ports have no such declaration. Remove the bypass from the instance, or enter the block and bypass its internal nodes.
  • Export → Export as Subgraph creates a preset, not a block; both features use the term "subgraph." The export rejects a canvas that contains collapsed blocks and lists their names. A server-side preset stores nodes and edges but cannot store a block definition. Without this validation, the preset would contain a subgraph:<id> node without its definition and would not run independently of the source graph. Expand the blocks first by right-clicking each one and selecting Expand subgraph here, and then export.