Function Graphs & Blueprint Mode
A logic.function node (see the Logic section of the Node
Reference) can be authored two ways, toggled by its mode
config field:
"code"(the default) — you hand-type the function body directly into the node's config panel, exactly like writing a normal JavaScript function."blueprint"— the function body is instead an entirely separate, nested visual node graph, opened via the node's "Open Blueprint Graph" button. You wire nodes together on this second canvas exactly like the main canvas, and it compiles one-directionally into the function's body.
Editing a function graph: tabs, not a popup
Double-clicking a blueprint-mode Function node (or its "Open Blueprint Graph" button) opens that function's graph in its own tab, next to an always-present "Main Graph" tab — the same way a code editor handles multiple open files, rather than a full-screen popup. Open several function graphs at once (reopening one that's already open just switches to its existing tab instead of duplicating it), and use the ◀ / ▶ buttons to step back and forth through recently visited tabs. Every edit saves itself automatically the moment you make it, straight back into the outer Function node — there's no Save/Save & Close/Cancel step, and nothing is lost by switching tabs or closing one.
This isn't limited to one level below the main canvas: a tab's own graph can contain
another blueprint-mode node (a Function, a Handler Function, or a Promise — see
"Promises can nest inside each other" below), and double-clicking that opens its
graph as a further-nested tab, to arbitrary depth. The breadcrumb row shows the full
ancestor chain (e.g. FunctionA > SomeNestedThing > AnotherNestedThing), live-sync
keeps working at every level, and closing a tab also closes all of its descendant tabs.
The nested graph's own entry and exit
A function's blueprint graph has its own entry/exit concept, distinct from the main
canvas's express.init/routes:
logic.graphEntry("Start") — the graph's sole entry point: one execution output to kick off the first node in the chain, plus one dynamic value output pin per current function parameter. You never add this manually — it's managed by the Function Graph editor's always-visible "Function Details" panel (add/rename/remove parameters live, no reopening required). Renaming a parameter there relabels any wires already connected to it, so wiring survives a rename.logic.graphReturn("Return") — ends the chain it's wired into withreturn <expr>;. Unlike Start, any number of Return instances are allowed, each an ordinary node added from the picker (like Branch/Switch) rather than a managed singleton, and each with its own execution-in pin: wire one from inside a Branch/Switch arm to return early, right there, without running the rest of the graph. A Return left with its execution-in pin unwired falls back to the original behavior — its value is appended once after the whole compiled function body — so a graph authored before Return had an exec pin keeps compiling identically.
Both of these node types exist only inside a Function Graph — they're hidden from the
main canvas's node picker entirely, since the main canvas already has its own structural
entry points (express.init, logic.begin).
Early returns
Wiring a Return's execution-in pin from inside a Branch (or Switch) arm makes that arm's
return a real early return, letting the rest of the graph run only when no earlier arm
returned — the visual equivalent of an if/else if grading chain:
function grade(score) {
if (score >= 90) {
return "A"; // Return #1, wired from this Branch's True arm
} else {
if (score >= 80) {
return "B"; // Return #2, wired from the nested Branch's True arm
} else {
return "C"; // Return #3, wired from the nested Branch's False arm
}
}
}
This is a second, more direct pattern than the read-a-value-back-through-a-variable workaround below — reach for a Return-per-arm when each arm's outcome really is "done, return this," and for the variable workaround when an arm needs to hand a value to sibling logic that keeps running afterward.
Recursion
Functions can call themselves recursively. Inside your function's blueprint graph, when
you add a Function Call node, the picker shows a "Functions in This File" section listing
all sibling functions — including the current function itself, labeled "(recursive)".
Wire a recursive call exactly like any other function call; it compiles to a bare function
name (e.g., factorial(n - 1)), enabling real recursive logic.
Everything else works exactly like the main canvas
Inside a blueprint-mode function's graph you can use logic.functionCall (including
recursive calls to the same function), variable.get/variable.set, debug.consoleLog,
handler.sendJson (the way a Handler Function's graph responds), all 17 operator nodes,
and controlFlow.branch/controlFlow.switch for real branching. See Node
Categories for the full main-canvas/Function-Graph
availability breakdown.
Local variables and Module Variables
Every function graph has its own local variable list, never cross-checked against the main canvas's. But it can also see the main canvas's variables, via a second Module Variables panel shown right alongside the local Variables panel in the graph's side panel — drag one out the same way you'd drag a local variable to drop a Get/Set node for it. Editing a module variable from inside the graph (renaming it, changing its type, adding or removing one) updates the main canvas immediately, since it's the exact same underlying list, not a copy.
Promises can nest inside each other
A logic.promise ("Promise") node's executor — the code that eventually resolves or
rejects it — can be authored in "blueprint" mode, exactly like a Function's body: it's
opened and edited through the same tab system described above, with the same automatic
live-sync back into the owning Promise node.
Because a Promise's executor graph is just another blueprint graph, it can itself contain another Promise node, whose own executor can also be blueprint-mode, and so on to arbitrary depth — this is the concrete case that makes the arbitrary-depth tab nesting described above actually matter. Double-clicking a blueprint-mode Promise node inside any already-open tab opens its executor graph as a further-nested tab.
One wrinkle specific to nested Promises, since it's the same kind of cross-scope access the "Module Variables" section above describes for variables: a Promise nested inside another Promise's executor graph can resolve or reject the enclosing Promise directly, not just its own — its graph's Start node grows extra "Outer Resolve"/"Outer Reject" pins when this applies. Full details are on the Promise node reference.
A worked example
04-function-graph-branch is a complete isEven
function authored entirely in blueprint mode: Start → Branch (condition
n % 2 === 0) → each arm sets a function-scoped variable to true/false → a
Get Variable node feeds Return. Reading the result back out through a variable
(rather than returning directly from inside each Branch arm) is deliberate — see
How Code Generation Works for why a Branch/Switch
arm's own locally-computed values can't be read directly by a downstream node outside
that arm.