| # 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`](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: |
|
|
| ```text |
| <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: |
|
|
| ```json |
| { |
| "setups": [] |
| } |
| ``` |
|
|
| When the user registry file does not exist, the catalog falls back to the |
| bundled sample at [`../../web/published_setups_sample.json`](../../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: |
|
|
| ```json |
| { |
| "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`](../../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`](../../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: |
|
|
| ```json |
| { |
| "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: |
|
|
| ```json |
| { |
| "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: |
|
|
| ```json |
| { |
| "ok": true, |
| "run": { |
| "runId": "run-000001", |
| "promptId": "f0c2...", |
| "status": "queued" |
| } |
| } |
| ``` |
|
|
| Failure responses use the standard route error shape: |
|
|
| ```json |
| { |
| "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: |
|
|
| ```json |
| { |
| "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. Put |
| `Koolook_PublishInput` here. 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. Put `Koolook_PublishOutput` |
| here. For switch-selected outputs, put `Koolook_PublishRouter` in this group |
| and wire its switch-aligned outputs to the branch writer nodes. Optional |
| `Koolook_PublishResult` nodes 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. |
|
|
| ```json |
| { |
| "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: |
|
|
| ```text |
| 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: |
|
|
| ```text |
| 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: |
|
|
| ```text |
| 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: |
|
|
| ```text |
| 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: |
|
|
| ```text |
| 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: |
|
|
| ```text |
| 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: |
|
|
| ```text |
| 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.nodes` must be a list of node objects with stable `id` and |
| non-empty `type`. |
| - Each converted API node is keyed by the visual node id as text and stores |
| `class_type` from the visual node `type`. |
| - Widget-backed inputs must appear in `node.inputs` with a `name` and |
| `widget` object. Their values are read from `node.widgets_values` in widget |
| input order. |
| - Nodes may also save `widgets_values` as an object keyed by widget/input name; |
| those values are read by name. |
| - Subgraph wrapper nodes are expanded from the matching |
| `definitions.subgraphs` entry and internal node ids are namespaced as |
| `<wrapper-id>:<internal-id>`. |
| - Some simple Comfy nodes serialize widget values without corresponding |
| `node.inputs` entries. The converter supports known widget-only mappings for |
| `Text Multiline` (`text`) and Koolook `EasyAIPipeline` so simple grouped |
| setup workflows can still publish into runnable API prompt inputs. |
| - Linked inputs must have a `link` id that resolves in `visualGraph.links`. |
| Array links such as `[101, 12, 0, 20, 0, "STRING"]` and object links with |
| `origin_id` / `origin_slot` are supported. The API prompt value becomes |
| `["12", 0]`. |
| - Visual-only nodes such as `Label (rgthree)` and `Note` are omitted. `Reroute` |
| nodes 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: |
|
|
| ```json |
| { |
| "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: |
|
|
| ```json |
| { |
| "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. Missing `inputs` is 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 from `visualGraph.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 a `Koolook Input` |
| group. |
| - `setupSurface.outputs requires a non-empty Koolook Output group`: the |
| group-first publish path was used, but no node overlapped a `Koolook Output` |
| group. |
| - 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 name` |
| and stored with structured inventory breadcrumbs |
| - inferred `Koolook Input` / `Koolook Output` node 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`](../../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: |
|
|
| ```text |
| 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: |
|
|
| 1. `GET /koolook/api/setups` to list published setups. |
| 2. `GET /koolook/api/setups/{id}` to inspect the selected setup contract. |
| 3. `POST /koolook/api/setups/{id}/run` with the JSON inputs supplied in the |
| simulator. |
| 4. `GET /koolook/api/runs/{runId}` until the run reaches `succeeded`, |
| `failed`, or `lost`, 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: |
|
|
| ```json |
| { |
| "key": "prompt", |
| "label": "Prompt", |
| "type": "text", |
| "required": true, |
| "target": { "node": "12", "input": "text" } |
| } |
| ``` |
|
|
| - `key`: stable external field key, such as `prompt`, `seed`, or `aspect_ratio`. |
| - `label`: curator-facing/user-facing label. |
| - `type`: simple value type such as `text`, `number`, `boolean`, `image`, or `video`. |
| - `required`: boolean; omit or set `false` for 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: |
|
|
| ```json |
| { |
| "key": "preview", |
| "label": "Preview", |
| "type": "image" |
| } |
| ``` |
|
|
| Keep outputs minimal until the runner slice defines richer execution payloads. |
|
|