13 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 saved entry gets module: true and the compatibility module tag 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. New saves store module state as first-classmodule: true; the tag is still honored for old entries and for the manual right-click Tags workflow. - 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, normalizes Comfy/LiteGraph link records, configures each node with stale saved link IDs stripped, setsnode.id = -1soapp.graph.add()allocates fresh ids that can't collide with the live canvas, and finally recreates internal connections viaoriginNode.connect(...)(which auto-allocates fresh link ids). Selection saves that include ComfyUI subgraph wrapper nodes also carry the requireddefinitions.subgraphsentries so a later native Load has the definitions available. - 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;
the tag editor keeps the first-class module flag in sync.
Right-click menus
- Workflow row: Load / Insert into canvas / Update from selection or canvas / Rename / Duplicate / Tags / Move to archive (or Restore from archive if already archived) / + New directory / + New subdirectory / Delete. Existing-folder moves are handled by drag-and-drop so the menu stays short in large libraries.
- 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): Clean up archive runs across the whole selected Archive folder, groups entries by original workflow/setup name, keeps the newest archived entry from the last 5 minutes, last hour, and last day for each group (plus one fallback if none land in those windows), and confirms the keep/delete counts before mutating; Delete archive (N) removes every archived workflow in this directory in one go. Active workflows in the same directory are untouched by both actions.
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: drag a workflow row onto the target directory row
- 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. |
Comfy workflow draft quota gotcha
ComfyUI also keeps its own browser-side workflow draft cache in localStorage:
| Key | Owner |
|---|---|
Comfy.Workflow.Drafts |
ComfyUI frontend draft autosave |
Comfy.Workflow.DraftOrder |
ComfyUI frontend draft LRU order |
When large workflows are repeatedly loaded or imported with changing workflow IDs, Comfy can accumulate multiple large drafts. Once browser storage quota is hit, Comfy shows repeated "Failed to save workflow draft" toasts. Koolook hit this first in v0.3.6 with sidebar workflow loads; the fix was to give sidebar loads a stable temporary workflow ID derived from the sidebar path/name so the same workflow replaces the same draft entry instead of creating a new one each time.
The same symptom can reappear when large workflow JSONs are imported directly from disk, bypassing the sidebar load path. For Koolook frontend extensions that ship large timeline/editor data, keep two rules in mind:
- Do not churn workflow IDs for equivalent exported/imported workflow files.
- Keep hidden widget payloads plain JSON and strip preview-only blobs before serialization.
The LTX Director timeline extension also installs a small guard around Comfy's
draft-cache writes. If the browser throws QuotaExceededError while writing
only the Comfy draft keys above, the guard clears those draft entries and lets
Comfy retry. It does not touch Koolook /userdata, snapshots, workflow library
entries, render outputs, or user project files.
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 */ },
"module": true,
"tags": ["module"],
"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. Link handling accepts both serialized array links (
[id, origin, slot, target, slot, type]) and object-shapedLLinkrecords fromgraph.links. - Subgraph wrappers are special. The saved selection includes transitively referenced
definitions.subgraphs, but insert still cannot register a never-loaded subgraph definition because ComfyUI's subgraph registration path lives inside nativeapp.loadGraphData. If Insert reports an unregistered subgraph definition, native-Load the workflow once in that browser session, then retry Insert. - 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 background auto-pruning of archives. Re-saving the same name can still leave many timestamped archive entries. Right-click the synthetic Archive folder → Clean up archive to triage all archived entries in that folder by original workflow/setup name, keeping the newest archived entry from the last 5 minutes, last hour, and last day for each group. Use Delete archive (N) / per-row Delete when you want explicit removal.
- Same-pattern distribution as
starter_preset.json— seecurated-sidebar.mdfor the seeding semantics that also apply here.