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. - Selection saves preserve ComfyUI group boxes that overlap selected nodes. Insert translates those saved groups by the same placement delta as the inserted node cluster, so group titles and boxes survive module round trips. This is intentionally limited to groups touching the saved selection; unrelated canvas groups are not copied into partial workflow saves.
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 keeps its own browser-side workflow draft cache in localStorage,
and the key scheme has changed across frontend versions. All generations
share the Comfy.Workflow.Draft prefix:
| Generation | Keys | Written when |
|---|---|---|
| V1 (legacy; still written on tab switches as of frontend 1.44) | Comfy.Workflow.Drafts (one blob of all drafts) + Comfy.Workflow.DraftOrder (LRU) |
switching/loading workflows |
| V1 per-workspace (interim) | Comfy.Workflow.Drafts:<ws> + Comfy.Workflow.DraftOrder:<ws> |
older 1.4x frontends; the 1.44 migration reads these once without deleting them (later frontends added cleanup on successful migration, and an interrupted migration can still leave them behind) |
| V2 (frontend 1.44+) | Comfy.Workflow.DraftIndex.v2:<ws> (index) + Comfy.Workflow.Draft.v2:<ws>:<hash> (one payload per draft) |
~512 ms after every graph edit |
Once the origin's storage quota is hit, draft saves fail and Comfy shows "Failed to save workflow draft" toasts. Worse, the 1.44+ V2 layer only evicts its own current-workspace drafts on a quota error, and when that isn't enough it latches a session-wide storage unavailable flag — after which every edit toasts until the page is reloaded, regardless of which workflow is open.
Koolook hit the symptom first in v0.3.6 with sidebar workflow loads; the fix was 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 per load. For Koolook frontend extensions that ship large timeline/editor data, the standing rules remain:
- 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 systemic countermeasure is the global guard in
web/koolook_draft_guard.js (loaded on every page; behavior covered by
tests/js/test_draft_guard.mjs). It matches draft keys by prefix, so a
future upstream rename inside the prefix degrades to last-resort eviction
instead of going silently stale (which is how the previous exact-name
guard, then embedded in the LTX Director extension, died on frontend
1.44). Two mechanisms:
- Boot prune — deletes suffixed-V1 families already migrated to V2 (dead weight 1.44-era migrations leave behind), corrupt keys (scoped to the offending key only), oversized entries (>750k chars), and enforces a ~2M-char total draft budget, oldest drafts first.
localStorage.setItemquota guard — onQuotaExceededErrorfor a draft key, evicts the oldest draft across all generations and retries insidesetItem, so the V2 layer's own catch never sees the error: the save succeeds, no toast, and the storage-unavailable latch never trips.
The guard never touches Koolook /userdata, snapshots, workflow library
entries, render outputs, or any non-draft localStorage key.
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. - Selection saves preserve relevant groups. Any ComfyUI group box that
overlaps selected nodes is stored with the partial graph and restored on Load
or Insert. This supports the published-setup convention where
Koolook InputandKoolook Outputgroups mark the setup's app surface. - 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.