Tag any saved workflow with the literal tag `module` and the Kforge Labs sidebar starts treating it as a reusable building block: a green `pi pi-plus-circle` icon replaces the file glyph, left-click **inserts** the cluster at the viewport center (with internal links re-created and the new nodes left selected), and a distinct hover tooltip surfaces the changed semantics. Designed for the `EXR-output stack` / `depth-only render` / `Wan 2.2 prompt module` pattern: small clusters the maintainer wants to drop into many workflows without rebuilding them by hand. What changed: 1. New non-destructive primitive `insertWorkflowOntoCanvas` in `web/sidebar/canvas_io.js`. Pre-flights every referenced node type against `LiteGraph.registered_node_types` and aborts cleanly with a toast naming the missing types — a partial insert with stub nodes is worse than no insert. Then deep-clones the saved graph, calls `node.configure(...)` per node with `node.id = -1` so `app.graph.add()` allocates fresh ids that can never collide with the live canvas, and finally walks the saved `links[]` to recreate internal connections via `originNode.connect(...)` (which auto-allocates fresh link ids — no manual link remap needed). Bbox placement reuses the existing CSS-pixel correction so the cluster lands at the visible viewport center on HiDPI displays. 2. Right-click on any workflow row now offers `Insert into canvas` alongside the existing `Load` — Load remains available for every row regardless of tag, so a module can still be loaded as a full session if the user wants. 3. Convention-driven module classification in `web/sidebar/constants.js` (`MODULE_TAG = module`). Picked a tag instead of a new `isModule` schema field so existing saved entries can be promoted to modules with one right-click → Tags add, no migration needed, and the existing Tags section groups them for free. Tree rendering looks up the tag at row-render time (`isModuleWorkflow` helper) and switches both the leaf icon (green `pi pi-plus-circle`) and the left-click handler (Insert instead of Load). Active rows in the Workflows section AND every row under the Tags section honor the flip; archived rows stay on Load (they're old versions, `review` not `splice`). 4. `showSaveWorkflowModal` gains a `Save as module` checkbox. The selection-save toolbar button passes `defaultModule: true` (the typical `module` candidate is a small cluster the user just selected); the whole-canvas save passes `defaultModule: false`. The tag is added in the SAME `persistMutation` as the entry write (folded into the `mutate` callback in `saveAndToast`), so a commit failure rolls back BOTH the save and the tag together — no `module`-but-untagged ghosts. Docs: new `Modules` section in `docs/maintainers/workflows-sidebar.md` covering the convention, the checkbox flow, per-surface behavior, and how to promote an existing saved workflow to a module without re-saving. Co-authored-by: Cursor <cursoragent@cursor.com>
9.6 KiB
Workflows sidebar — quick reference
The "Workflows" section of the Kforge Labs sidebar tab; implementation
lives in web/sidebar/, with the workflow store + ops
in workflows_store.js and the tree
rendering in tree.js.
Save / load loop
- Click 💾 (whole canvas) or ✂ (selection only) in the Workflows action bar.
- Modal: Directory (flat path picker) → Base on existing (optional) → Action → Workflow name (shown only when needed).
- Saved entry appears at the bottom of its directory in the tree.
- Click any workflow row to load it onto a fresh tab; the tab title takes the workflow's name.
The directory dropdown lists every directory path in the tree as a flat
list (UP-scale, UP-scale / Type-A, Depth, …). The "+ New directory…"
option creates a new top-level directory. Subdirectories are created via
right-click on an existing directory (see below).
Action semantics
| Action | Resulting name | Archive behavior |
|---|---|---|
| New name | typed | auto-archives only if the typed name happens to collide |
| Use existing name (archive previous) | base | always archives the existing entry |
| Modify existing name | base, edited | no archive (different name) |
The archive trigger is a name collision with any existing active entry,
regardless of which action was selected. The Action dropdown is UX
guidance; the underlying logic flows from the resulting (name, dir) pair.
Modules — splice a saved cluster into your live canvas
A saved workflow tagged with the literal tag module behaves like a
reusable building block: clicking it inserts its nodes into the existing
canvas (placed at the viewport center, freshly id-remapped, internal links
re-created, the inserted nodes left selected) instead of replacing the
graph the way a normal Load does.
Typical use cases this is designed for:
- EXR output stack — a Save EXR + path-builder + format selector cluster the user wants to drop into any render-time workflow.
- Depth-only render — a depth-map node + preview wired up, ready to be spliced into a wider pipeline.
- Wan 2.2 prompt module — the
EasyWan22Promptnode + a CLIP encoder- any conditioning glue, saved once, dropped into ten different workflows.
How it works:
| Surface | Behavior |
|---|---|
| Save modal (selection save) | Includes a Save as module checkbox, pre-checked for selection saves. Whole-canvas saves get the same checkbox but unchecked by default. The tag is added in the same atomic persistMutation as the save — a commit failure rolls both back. |
| Workflows section | Module-tagged entries get a green pi pi-plus-circle icon and a distinct hover tooltip. Left-click inserts instead of loading. Right-click still offers both Load and Insert into canvas — Load stays available for every row regardless of tag. |
| Tags section | Same flip — a workflow tagged module is treated as a module no matter which tag folder it shows up under. |
| Archive folder | Archived module entries still left-click to Load (they're old versions, treated as "review" rather than "splice"). Right-click still has both Load and Insert. |
Implementation pieces:
- The literal tag string lives in
web/sidebar/constants.jsasMODULE_TAG. - The non-destructive insert primitive is
insertWorkflowOntoCanvas(dirPath, wfName)inweb/sidebar/canvas_io.js. It pre-flights every referenced node type againstLiteGraph.registered_node_types, aborts cleanly with a toast when any are missing (a partial insert with stub nodes is worse than no insert), then deep-clones the saved graph, callsnode.configure(...)per node withnode.id = -1soapp.graph.add()allocates fresh ids that can't collide with the live canvas, and finally walks the savedlinks[]to recreate internal connections viaoriginNode.connect(...)(which auto-allocates fresh link ids — no manual link remap needed). - Bbox-of-cluster placement uses CSS pixels (
clientWidth/clientHeight), not the HiDPI backing buffer — same correction as the existingplaceAtCanvasCenterhelper.
To turn an existing saved workflow into a module: right-click → Tags…
→ add module. (No re-save needed; the row re-renders on the next
workflows-changed event.) To un-module: remove the module tag the same way.
Right-click menus
- Workflow row: Load / Insert into canvas / Rename /
→ <other path>(lists every other directory in the tree) / Move to archive (or Restore from archive if already archived) / Delete. - Directory row (any depth): Create subdirectory… / Rename / Delete (with confirm if non-empty — the message names workflow + subdirectory counts).
- Archive folder (synthetic — appears only when a directory has archived workflows): Delete archive (N) — removes every archived workflow in this directory in one go (active workflows in the same directory are untouched).
Drag-and-drop (Tier 1 — moves only; reordering is alphabetical)
| Drag | Drop on | Effect |
|---|---|---|
| Workflow row | Directory row | Move workflow to that directory (no-op if same dir) |
| Workflow row | Archive folder | Archive in that directory (move first if cross-dir) |
| Directory row | Directory row | Nest the dragged dir as a child of the target |
Cycle prevention: dragging a directory onto itself, its current parent, or any of its descendants is rejected. Name collisions (the destination already has a sibling with the same name) are rejected. Reserved name: dropping a directory literally named Archive into a non-root parent is rejected (would shadow the synthetic Archive folder).
Visual feedback: drop targets get a blue outline on hover. Failed drops surface a toast.
Sort within a level remains alphabetical — Tier 1 doesn't introduce a custom-order schema. (A future Tier 2 could.)
Subdirectories
Every directory can host nested subdirectories at arbitrary depth. Each nested directory behaves like a top-level directory: it can hold workflows, its own Archive subfolder (when any of its workflows are archived), and further subdirectories.
- Create: right-click a directory → "Create subdirectory…"
- Save into: pick the nested path from the save modal's directory dropdown (
UP-scale / Type-A) - Move across: right-click a workflow → choose any other path in the move submenu
- Reserved name: subdirectory names cannot be
Archive(case-insensitive) — that string is reserved for the synthetic Archive folder rendered for archived workflows. Top-level directories namedArchiveare allowed (no collision at root).
Storage
| Where | What |
|---|---|
/userdata/koolook_workflows.json (ComfyUI install) |
Primary. Per-install. |
localStorage["koolook.workflows.fallback.v1"] |
Used only if /userdata is unreachable. |
localStorage["koolook.workflows.seeded.v1"] |
"1" once defaults have been seeded for this install. |
web/workflow_defaults.json (in repo) |
Distributed starter pack — seeded once into /userdata on first load with empty workflow data. |
JSON shape (recursive — directories lives both at the root AND inside
every directory node):
{
"directories": {
"<dir>": {
"workflows": {
"<name>": {
"savedAt": "<ISO timestamp>",
"graph": { /* serialized ComfyUI workflow JSON */ },
"archived": true
}
},
"directories": {
"<sub>": {
"workflows": { /* … */ },
"directories": { /* recurses */ }
}
}
}
}
}
archived is optional (false/missing means active). Pre-v0.3 stores
without the nested directories field still load fine — normalize
treats a missing directories as {} and the rest of the code assumes
it always exists after normalization.
Reset (force re-seed from defaults)
Delete the userdata file in the live ComfyUI install:
rm <ComfyUI>/user/default/userdata/koolook_workflows.json
Then in the ComfyUI page DevTools console:
localStorage.removeItem("koolook.workflows.seeded.v1");
localStorage.removeItem("koolook.workflows.fallback.v1");
location.reload();
Things that surprised us — keep in mind
- Selection saves are partial graphs. Only the selected nodes + links between them survive; links into/out of non-selected nodes are nulled out, so the loaded workflow never has dangling references.
- Workflow tab name comes from
app.loadGraphData(graph, true, true, name, {})'s 4th arg. The tab flips from "Unsaved Workflow (N)" to the saved name, and Ctrl+S pre-fills with that name. (Verified againstComfy-Org/ComfyUI_frontendsrc/scripts/app.ts.) - Folder expansion state persists across re-renders (the
pathStatesMap inweb/sidebar/tree.js). Saving never collapses the directory you were viewing — and a save into a nested path opens every ancestor folder. - Archive sub-folder is rendered above active workflows in each directory (and below nested subdirectories), so the latest active workflow sits closest to the bottom of its directory — easy to spot.
- Directory header counts include all descendants. A parent dir with no direct workflows but multiple subdirectories shows the recursive total.
- No auto-pruning of archives. Re-saving the same name many times leaves many timestamped archive entries. Right-click → Delete to remove individual ones when no longer useful.
- Same-pattern distribution as
curated_defaults.json— seecurated-sidebar.mdfor the seeding semantics that also apply here.