Published workflow setup registry
Issue #210 introduced the first server-side registry for published workflow setups. Issue #211 adds the curator publish path from saved sidebar workflows, and issue #212 adds publish-time conversion from visual workflow graphs to Comfy API prompt JSON for the first supported callable setup shape. The registry remains separate from sidebar workflow and snapshot storage so external apps can consume a stable catalog without knowing Kforge Labs sidebar internals.
For the product/design contract behind the external frontend surface, read
published-setup-external-ui-contract.md.
This document focuses on storage, validation, routes, and implementation
details.
Storage
The registry loads JSON from a koolook-published-setups/setups.json folder
beside the configured snapshot library (the libraryPath setting /
KFORGELABS_PRESETS env, default <ComfyUI user directory>/koolook-presets/),
so published setups follow the same location as snapshots:
<snapshot-library parent>/koolook-published-setups/setups.json
On first use, any pre-relocation registry at the old fixed path
(<ComfyUI user directory>/koolook-published-setups/setups.json) is copied to
the new location when the new one does not exist yet — non-destructively, so
the original file is left in place. The copy is atomic (temp file +
os.replace), so an interrupted migration can never leave a partial
setups.json at the new path. If the file at the new path exists but does not
parse while the legacy file is intact, the registry keeps serving the legacy
file and logs a diagnostic — a corrupt relocation target is never allowed to
mask real data behind an empty catalog or the bundled sample.
The file may be either a bare array of setup objects or an object with a
setups array:
{
"setups": []
}
When the user registry file does not exist, the catalog falls back to the
bundled sample at ../../web/published_setups_sample.json.
An existing empty setups.json remains empty. A corrupt or unreadable primary
setups.json does not fall back to the sample, because that would hide the
operator's real registry problem; the catalog returns no rows and logs a
diagnostic instead. Publishing a setup writes to the primary user registry file.
Schema
Each published setup object uses this shape:
{
"schemaVersion": 1,
"id": "stable-url-safe-id",
"version": 1,
"updatedAt": "2026-06-06T08:00:00Z",
"metadata": {
"title": "Setup title",
"description": "Optional user-facing description",
"category": "Video",
"tags": ["tag"],
"previewImage": ""
},
"visualGraph": {},
"apiPrompt": {
"12": {
"class_type": "Text Multiline",
"inputs": {
"text": "Default prompt"
}
}
},
"inputContract": {
"inputs": [
{
"key": "prompt",
"label": "Prompt",
"type": "text",
"required": true,
"target": {
"node": "12",
"input": "text"
}
}
]
},
"outputContract": {
"outputs": [
{
"key": "video",
"label": "Video",
"type": "video"
}
]
},
"setupSurface": {
"sourceInputs": [
{
"group": "Koolook Input",
"nodes": [{ "id": "12", "type": "Load Image", "title": "Source image" }]
}
],
"outputs": [
{
"group": "Koolook Output",
"nodes": [{ "id": "20", "type": "Preview Image", "title": "Preview" }]
}
],
"controls": []
},
"source": {
"kind": "sidebar-workflow",
"path": "Models/RMGB/Publish/rmgb-publish-v04",
"inventoryPath": ["Models", "RMGB", "Publish"],
"name": "rmgb-publish-v04"
},
"validation": {
"status": "valid",
"diagnostics": []
}
}
apiPrompt is nullable only for non-callable draft records, such as bundled
catalog smoke-test samples. A setup with validation.status: "valid" must have
a concrete API prompt. Prefer a ComfyUI-exported API prompt as the execution
source of truth; visual-graph conversion is only a fallback for older/simple
flows. Execution slices should require validation.status: "valid" and a
concrete apiPrompt before a setup can run. Only schemaVersion: 1 is
accepted by this first registry; future schema versions must add explicit
migration or validation support before they pass.
Public Boundary
The registry module is ../../koolook_setups.py.
External callers should use:
PublishedSetupRegistry.listSetups()PublishedSetupRegistry.getSetup(id)PublishedSetupRegistry.publishSetup(visualGraph, metadata, inputContract, outputContract, source, apiPrompt=None)
Execution lives behind ../../koolook_setup_runner.py.
External routes and future app adapters should use:
PublishedSetupRunner.runSetup(id, inputs)PublishedSetupRunner.getRun(runId)
The runner hides prompt cloning, input injection, ComfyUI /prompt
submission, run-id mapping, history/queue polling, and output flattening from
route callers.
Invalid setup objects are omitted from list/detail results. Diagnostics are
kept on registry.diagnostics and logged by the HTTP adapter. File-level
storage diagnostics, such as unreadable JSON, are reported through the same
channel.
publishSetup builds a setup from a saved sidebar workflow graph and, when
provided, stores ComfyUI's exported API prompt as the executable source of
truth. If no API prompt is supplied, the registry falls back to the narrow
visual-graph converter. Publishing validates required metadata and contract
shape, checks that each declared input target points at both the visual graph
and the stored API prompt, then replaces any existing setup with the same id in
storage. Unsupported graphs fail publish with curator diagnostics rather than
being silently stored as callable records.
Catalog API
GET /koolook/api/setups
Returns catalog summaries only: id, version, updated timestamp, metadata,
validation status, and input/output summaries. It intentionally does not return
visualGraph, apiPrompt, or raw storage internals.
GET /koolook/api/setups/{id}
Returns the full published setup contract for one setup, including visual graph,
optional API prompt, full input/output contracts, source reference, and
validation state. Treat validation.status: "valid" plus a non-null
apiPrompt as the public signal that the prompt is current and executable.
Unknown, invalid, or stale setup ids return 404.
POST /koolook/api/setups
Publishes one setup. Body:
{
"visualGraph": {},
"apiPrompt": {
"12": { "class_type": "Text Multiline", "inputs": { "text": "prompt" } }
},
"metadata": {
"id": "director-demo",
"title": "Director Demo",
"description": "A curated video workflow",
"category": "Video",
"tags": ["director", "video"],
"previewImage": ""
},
"inputContract": {
"inputs": [
{
"key": "prompt",
"label": "Prompt",
"type": "text",
"required": true,
"target": { "node": "12", "input": "text" }
}
]
},
"outputContract": {
"outputs": [{ "key": "preview", "label": "Preview", "type": "image" }]
},
"source": {
"kind": "sidebar-workflow",
"path": "Demos/Director Demo",
"inventoryPath": ["Demos"],
"name": "Director Demo"
}
}
Success returns { "ok": true, "setup": { ... } } with the stored
apiPrompt alongside the original visualGraph. When the registry is
file-backed (the default), the response also carries storagePath — the
absolute path of the setups.json the record was written to — so the
sidebar can show, copy, and open where the setup landed. Validation
failures return HTTP 400 with { "ok": false, "errors": [...] }.
source.inventoryPath is the sidebar folder breadcrumb array for the workflow
that was published. External frontends may use it to recreate the sidebar's
administrator-defined catalog hierarchy. source.path remains the
human-readable compatibility path, and source.name is the workflow/setup name
inside that folder.
POST /koolook/api/setups/reveal
Opens the published-setups directory (the folder containing setups.json) in
the host's OS file manager and returns { "ok": true, "path": "<dir>" }. This
is a local maintainer convenience backing the publish success card's Open
folder action — it is grounded at the registry's own storage folder, distinct
from the snapshot-library reveal (POST /koolook/presets/reveal). Returns HTTP
404 when the directory does not exist yet (nothing published).
Run API
POST /koolook/api/setups/{id}/run
Queues a callable published setup on the managed ComfyUI server. The body must
be a JSON object with an inputs object:
{
"inputs": {
"prompt": "A cinematic close-up"
}
}
Only fields declared by inputContract.inputs, setupSurface.app.inputs, or
setupSurface.app.outputs are accepted. setupSurface.app.switch is also
accepted when present. The runner deep-clones the stored apiPrompt, injects
approved values into their declared targets, and submits
{ "prompt": <cloned prompt> } to ComfyUI /prompt; the stored setup record
is not mutated by a run request.
Success returns:
{
"ok": true,
"run": {
"runId": "run-000001",
"promptId": "f0c2...",
"status": "queued"
}
}
Failure responses use the standard route error shape:
{
"ok": false,
"errors": ["input 'seed' is not declared by this setup"]
}
Missing setup ids return 404; invalid inputs and non-callable draft setups
return 400; ComfyUI queue failures return 502.
GET /koolook/api/runs/{runId}
Looks up a stable Koolook run id and translates ComfyUI history/queue data into external state:
{
"ok": true,
"run": {
"runId": "run-000001",
"setupId": "ltx-director-demo",
"promptId": "f0c2...",
"status": "succeeded",
"comfyStatus": {
"completed": true,
"status_str": "success"
},
"outputs": [
{
"key": "video",
"label": "Video",
"type": "video",
"items": [
{
"nodeId": "20",
"kind": "videos",
"filename": "demo.mp4",
"subfolder": "koolook",
"type": "output"
}
]
}
]
}
}
Status is one of queued, running, succeeded, failed, or lost.
History entries produce terminal state, the raw ComfyUI status object as
comfyStatus, and output items; otherwise the runner checks ComfyUI queue
data to distinguish running from still queued. If a prompt is missing from
both history and queue, the runner reports terminal status lost so clients do
not poll until timeout. Unknown run ids return 404; ComfyUI status lookup
failures return 502.
For group-authored setups, status output summaries also include
setupSurface.app.outputs and setupSurface.app.results. This keeps the
external app aligned with the publish-contract-node surface even when
outputContract.outputs is empty. Result fields include their declared target,
default value, visibility, and any matching ComfyUI history items for the
target node. Koolook_PublishResult emits its resolved string through
ComfyUI UI text history, and the runner flattens that into a result item whose
value is the selected output path.
Comfy-Native Setup Surface
Issue #219 amends the #209 direction: curators should define a setup's app surface visually in ComfyUI where possible, while the backend stores the machine-readable contract. The reserved group names are:
Koolook Input: required source/input area for app-style setups. PutKoolook_PublishInputhere. Nearby source/helper nodes may overlap this group for human review, but they do not define the external app contract.Koolook Output: required output/result area. PutKoolook_PublishOutputhere. For switch-selected outputs, putKoolook_PublishRouterin this group and wire its switch-aligned outputs to the branch writer nodes. OptionalKoolook_PublishResultnodes may still be used to report a custom result string. Nearby save/preview/helper nodes may overlap this group for human review, but they do not define the external app form.Koolook Controls: future optional controls area for prompt, seed, strength, size, mode, or other user-tweakable fields.
Sidebar selection saves preserve ComfyUI groups that overlap selected nodes.
Publish infers a machine-readable setupSurface from the reserved groups.
Group membership is spatial: nodes whose rectangles overlap the reserved group
are listed in sourceInputs / outputs. The external app surface under
setupSurface.app is narrower: it is inferred only from recognized
Koolook_Publish* node classes inside the matching reserved group.
{
"sourceInputs": [
{
"group": "Koolook Input",
"nodes": [{ "id": "12", "type": "Load Image", "title": "Source image" }]
}
],
"outputs": [
{
"group": "Koolook Output",
"nodes": [{ "id": "20", "type": "Preview Image", "title": "Preview" }]
}
],
"controls": [],
"app": {
"inputs": [],
"outputs": []
}
}
When the publish dialog uses the group-first path, inputContract.inputs and
outputContract.outputs are submitted as empty arrays and the server requires
non-empty Koolook Input and Koolook Output groups. Explicit JSON contracts
still work as the advanced fallback for older or unusual workflows.
Publish Contract Nodes
Use the controlled Koolook publish nodes instead of scattered third-party text nodes when a setup should be callable from an external app:
Koolook Publish Input -> place in Koolook Input
Koolook Publish Output -> place in Koolook Output
Koolook Publish Router -> place in Koolook Output when one switch selects writers
Koolook Publish Result -> optional reporting node in Koolook Output
Koolook Publish Input exposes stable multiline fields and outputs:
mode dropdown: EXR, QT, Img, Prompt
sequence_folder STRING
qt_file STRING
single_file STRING
prompt STRING
switch INT output derived from mode
Koolook Publish Output exposes stable fields and outputs:
folder STRING
name STRING
version STRING
It is the shared destination/naming parameter node for downstream writer and path-building branches. It does not own the final mode-selected result.
Koolook Publish Router declares the execution route for switch-selected
writer branches:
selector INT from Koolook Publish Input.switch
payload image/string/other payload from the setup body
EXR output slot 0
QT output slot 1
Img output slot 2
Prompt output slot 3
Wire the setup payload, such as a mask image, through this router and connect
each output slot to the matching writer branch. At publish time Koolook stores
an executionMap that records the router, selected switch key, and writer
nodes reachable from each output slot. At run time the runner uses that map to
keep only the selected writer branch instead of rediscovering intent from the
whole graph.
Koolook Publish Result is optional. It exposes a custom result value after
workflow writer/path logic has run:
result STRING
Use it when the external app should display a path/status value that cannot be read from the selected writer/history output. It is no longer required to make switch-selected writer execution work.
Publish detects these node classes and stores setupSurface.app with stable
keys, user-facing labels, defaults, injection targets, result targets, and
switch options. The external app should render the switch first, preserve the
numeric switch values, and hide internal-only options such as Prompt while
keeping their index stable for the workflow. For the first version, field
visibility comes from setupSurface.app.switch.options[*].input: the selected
visible option names the one source input field to show.
Switch-Selected Writer Branches
Preferred router-authored setups use one Koolook_PublishInput.switch value
to drive both source selection and writer selection. The supported pattern is:
Koolook_PublishInput.switch -> source switch select
Koolook_PublishInput.switch -> Koolook_PublishRouter.selector
main setup payload -> Koolook_PublishRouter.payload
Koolook_PublishRouter.EXR -> EXR writer
Koolook_PublishRouter.QT -> QT writer
Koolook_PublishRouter.Img -> image writer
Koolook_PublishRouter.Prompt -> prompt/no-op branch when needed
At publish time, Koolook stores an explicit executionMap for this router.
At run time, Koolook records the submitted app switch value, selects the
matching router branch from the map, and prunes the queued API prompt to that
writer plus its upstream dependencies. This keeps unselected writer branches
from executing while still materializing the selected result file.
Older setups without executionMap may still use the legacy result-switch
fallback:
Koolook_PublishInput.switch -> result/output switch select
result/output switch value0/value1/value2/... -> branch result path
result/output switch output -> Koolook_PublishResult.result
When ComfyUI history includes a Koolook_PublishResult item, that result node
remains the preferred source of truth. If history omits the result node but
does include the selected branch node's output item, the runner uses the stored
app switch value plus the API prompt links to attach that selected branch item
to the published result summary.
Callable API Prompt Standard
Prefer ComfyUI's own API workflow export when publishing a callable setup. That
export is the same prompt shape submitted to /prompt, so it preserves custom
nodes exactly as the live server executes them and avoids editor-only artifacts
such as labels, reroutes, EasyUse state helpers, and subgraph wrapper ids.
The sidebar publish action should capture this automatically; manual API JSON
export is a diagnostic/testing workaround, not the product workflow.
Koolook still stores the visual workflow separately for setup surface inference,
review, and future editing. The visual graph is not treated as the executable
source when a Comfy-exported apiPrompt is present.
Callable Visual Workflow Fallback
The first supported conversion shape is intentionally narrow:
visualGraph.nodesmust be a list of node objects with stableidand non-emptytype.- Each converted API node is keyed by the visual node id as text and stores
class_typefrom the visual nodetype. - Widget-backed inputs must appear in
node.inputswith anameandwidgetobject. Their values are read fromnode.widgets_valuesin widget input order. - Nodes may also save
widgets_valuesas an object keyed by widget/input name; those values are read by name. - Subgraph wrapper nodes are expanded from the matching
definitions.subgraphsentry and internal node ids are namespaced as<wrapper-id>:<internal-id>. - Some simple Comfy nodes serialize widget values without corresponding
node.inputsentries. The converter supports known widget-only mappings forText Multiline(text) and KoolookEasyAIPipelineso simple grouped setup workflows can still publish into runnable API prompt inputs. - Linked inputs must have a
linkid that resolves invisualGraph.links. Array links such as[101, 12, 0, 20, 0, "STRING"]and object links withorigin_id/origin_slotare supported. The API prompt value becomes["12", 0]. - Visual-only nodes such as
Label (rgthree)andNoteare omitted.Reroutenodes are resolved as passthrough links instead of being submitted as API nodes. - Partial/module workflow sentinel links, such as links from node
-10, are not callable yet and fail publish.
Example visual input:
{
"nodes": [
{
"id": 12,
"type": "Text Multiline",
"inputs": [{ "name": "text", "widget": { "name": "text" } }],
"widgets_values": ["Default prompt"]
},
{
"id": 20,
"type": "Text Concatenate",
"inputs": [
{ "name": "text_a", "link": 101 },
{ "name": "delimiter", "widget": { "name": "delimiter" } }
],
"widgets_values": ["_"]
}
],
"links": [[101, 12, 0, 20, 0, "STRING"]]
}
Generated API prompt:
{
"12": {
"class_type": "Text Multiline",
"inputs": { "text": "Default prompt" }
},
"20": {
"class_type": "Text Concatenate",
"inputs": {
"text_a": ["12", 0],
"delimiter": "_"
}
}
}
Publish Diagnostics
Curators should treat publish diagnostics as setup authoring feedback:
visualGraph.nodes must be a list: the submitted workflow is not a visual graph object.visualGraph.nodes[N].type must be non-empty text: a node is missing the Comfy class/type needed for API prompt conversion.visualGraph.nodes[N].id duplicates visualGraph.nodes[M].id: duplicate node ids would overwrite generated API prompt entries, so publish is rejected.visualGraph.nodes[N].inputs must be a list when present: the node has a malformed input list. Missinginputsis allowed for nodes with no inputs.visualGraph.links[N].id duplicates visualGraph.links[M].id: duplicate link ids would make linked inputs ambiguous, so publish is rejected.visualGraph.links[ID].origin_id not found in visualGraph: a linked input points to a source node that is not present in the submitted graph.visualGraph.links[ID].origin_slot must be a non-negative integer: a linked input has a malformed source output slot.visualGraph.links[ID].target does not match visualGraph.nodes[N].inputs[M]: a visual input references a link whose target node/slot metadata points somewhere else.visualGraph.nodes[N].inputs.<name> references missing link: a linked input points at a link id that is absent fromvisualGraph.links.visualGraph.links[ID] uses unsupported module graph sentinel node: the graph includes partial/module placeholder links and cannot be made callable by this slice.inputContract.inputs[N].target.node not found in visualGraph: the input contract points at a missing visual node.inputContract.inputs[N].target.input not found in generated apiPrompt: the contract points at a visual input that is not injectable in the stored API prompt.setupSurface must be a JSON object for group-authored setups: a stored group-first setup has empty input/output contracts but lacks its persisted inferred app surface, so it is hidden from list/detail responses.setupSurface.sourceInputs[N].nodes[M].type must be non-empty text: a stored setup surface node summary is malformed.visualGraph.groups[N].bounding must contain numeric x, y, width, height: a reserved setup group has malformed rectangle data, so publish cannot safely infer membership.visualGraph.nodes[N].pos must contain numeric x and y for setup surface inference: a visual node has malformed placement data that could infer the wrong group.setupSurface.sourceInputs requires a non-empty Koolook Input group: the group-first publish path was used, but no node overlapped aKoolook Inputgroup.setupSurface.outputs requires a non-empty Koolook Output group: the group-first publish path was used, but no node overlapped aKoolook Outputgroup.- Legacy prompts containing visual-only artifacts such as
Reroute,Label (rgthree),Note,SetNode, or subgraph wrapper ids are normalized from the visual graph when possible. ComfyUI-exported API prompts are preserved even when they differ from the fallback converter.
Curator Publish Flow
In the sidebar Workflows section, right-click an active saved workflow and choose Publish setup.... Archived workflows do not expose this action.
The dialog captures:
- setup id, title, optional description, category, tags, and optional preview/card reference
- the source workflow reference, shown read-only as
Folder/Workflow nameand stored with structured inventory breadcrumbs - inferred
Koolook Input/Koolook Outputnode summaries - ComfyUI's API prompt for the workflow, captured automatically by the publish action before the setup is submitted to the registry
- advanced input/output contract JSON when group inference is not enough
The client validates that the selected saved workflow still exists before
calling the publish API. The intended path is that the client captures the same
API prompt shape ComfyUI would export and sends it as apiPrompt; the setup
author should not manually attach an exported JSON file. The server validates
metadata/schema shape, stores the provided ComfyUI API prompt when present,
falls back to visual conversion only when necessary, stores the inferred setup
surface, and checks explicit input targets against both the submitted graph and
stored prompt when advanced contracts are used. Ordinary saved workflows are not
published automatically; only the explicit context-menu publish action writes
to the registry.
On a successful publish the dialog no longer closes silently — it is replaced by
a confirmation card that reuses the snapshot Save dialog's "Saved to … Open
folder ↗" language. The card shows the setup id, source workflow, validation
status, and the registry storagePath, with Open folder (reveals the
published-setups directory via POST /koolook/api/setups/reveal), Copy path,
and Close. A draft publish that returns no storagePath still gets an
explicit confirmation, just without the open/copy affordances.
A successful publish also tags the source workflow published (a normal,
user-manageable tag). That gives the workflow a small published badge in the
Workflows tree, lists it in the Tags section's published pool, and makes it
selectable by the "P" toggle in the sidebar Tools row, which prunes the
Workflows tree to just the published setups (folders force-expanded) so they
are visible in their original folder structure. The tag is the discovery
signal, not the source of truth — the registry setups.json remains
authoritative, so the two can drift if a setup is later removed without
untagging.
External App Simulator
Use ../../web/setup_runner_simulator.html
to validate the external-app path without adding maintainer-only controls to
the Kforge Labs sidebar. First publish at least one setup from the sidebar
Workflows context menu; the simulator consumes published records, it does not
publish workflows itself.
For live review, open the simulator through the stable Koolook route on the same host/port as the running ComfyUI instance:
http://127.0.0.1:<comfy-port>/koolook/setup_runner_simulator.html
Opening the file directly from disk is allowed for inspection, but same-origin
API calls are not available from file://. In that case the simulator prefills
http://127.0.0.1:8188 as a common ComfyUI API base; change the port if the
server is running elsewhere. If the browser blocks that cross-origin request,
use the Koolook route above. Use
web/setup_runner_simulator.html?demo=1 only to verify the simulator UI without
a live published setup.
The simulator can also load exported/offline canonical setup JSON for contract
review, either as one setup record or a { "setups": [...] } wrapper. File
loaded records render through the same setupSurface.app form path, but they
are inspect-only: Run still uses the live Koolook registry API, so publish
the setup into the running ComfyUI registry before trying to execute it.
The simulator uses the same public execution boundary an external frontend uses:
GET /koolook/api/setupsto list published setups.GET /koolook/api/setups/{id}to inspect the selected setup contract.POST /koolook/api/setups/{id}/runwith the JSON inputs supplied in the simulator.GET /koolook/api/runs/{runId}until the run reachessucceeded,failed, orlost, or the client-side timeout expires.
The simulator displays the stable Koolook run id, ComfyUI prompt id,
queued/running/final status, returned output summaries, raw ComfyUI terminal
status details, and raw Koolook error payloads. It does not re-convert sidebar
workflow graphs at run time and does not call ComfyUI /prompt directly; the
runner owns prompt cloning, input injection, queue submission, and history/queue
translation.
Contract Authoring Rules
Input contract fields should describe the external app's friendly controls and where each value lands in the workflow:
{
"key": "prompt",
"label": "Prompt",
"type": "text",
"required": true,
"target": { "node": "12", "input": "text" }
}
key: stable external field key, such asprompt,seed, oraspect_ratio.label: curator-facing/user-facing label.type: simple value type such astext,number,boolean,image, orvideo.required: boolean; omit or setfalsefor optional controls.target.node: visual graph node id as text.target.input: input/port/widget name expected on that node.
Output contract fields describe what an external app can read back:
{
"key": "preview",
"label": "Preview",
"type": "image"
}
Keep outputs minimal until the runner slice defines richer execution payloads.