A ComfyUI workflow loaded into a NodeTool node keeps doing exactly what it did in ComfyUI. What changes is everything on either side of it: where the prompt comes from, how many times it runs, where the files go, and who presses the button.
This page is that second half. For the three nodes and their properties, see ComfyUI. For getting a server the node can reach, see ComfyUI Setup.
The shape of every recipe
A loaded workflow becomes a node with typed handles. Every dynamic input is
keyed <comfyNodeId>:<field> and takes the value ComfyUI would otherwise read
from the prompt JSON. Every dynamic output is keyed <comfyNodeId>:<kind> and
carries a NodeTool media ref, one per file. The ids are the keys of your
API-format export, so a CLIPTextEncode saved as node 6 exposes 6:text,
a LoadImage saved as node 10 exposes 10:image, and a SaveImage saved as
node 9 emits on 9:image. Which fields are offered, and which are never
offered because ComfyUI wires them internally, is
What becomes an input or an output.
Two facts shape every recipe below:
- A handle only exists if you asked for it. Media inputs and save outputs are derived automatically. Seeds, steps, CFG, and prompt text are offered as a checkbox list in the Load Workflow dialog. Until you tick one there is no handle, and the prompt keeps its exported value. See Loading a workflow.
- The node runs on the server, not in your browser. All three ComfyUI nodes
are server-side, so
endpointhas to resolve from wherever the NodeTool backend runs. A127.0.0.1:8188that works on your laptop is not the same address to a cloud install.
Recipe 1 · A model writes the prompt
What it does. The positive prompt is generated per run instead of typed into the ComfyUI graph, so one saved workflow covers a brief rather than a sentence.
Nodes. nodetool.input.StringInput, nodetool.agents.EnhancePrompt,
lib.comfy.RunWorkflow, nodetool.output.Output
Wiring. EnhancePrompt’s text output goes to the comfy node’s 6:text
handle. nodetool.agents.Agent works the same way when you want tools or a
system prompt, and its text output is the one to wire.
Watch out. 6:text is not there until you tick that CLIPTextEncode
field in the loader dialog. A negative prompt is a second CLIPTextEncode with
its own id, so leaving it as a literal and driving only the positive one is the
usual arrangement.
Recipe 2 · One workflow, many runs
What it does. The same diffusion graph runs once per item, unattended.
Nodes. nodetool.input.StringInput, nodetool.generators.ListGenerator,
lib.comfy.RunWorkflow, nodetool.control.Collect, nodetool.output.Output
Wiring. ListGenerator emits item (a string) once per generated line, and
each item drives one run of the comfy node. nodetool.control.ForEach does the
same for a list you already have, and nodetool.image.LoadImageAssets does it
for a folder of image assets, streaming image and name per file.
nodetool.control.Collect gathers the results back into one list.
Watch out. Every run gets its own timeout, not a shared one, so a
fifty-item batch on a 600 second timeout can take hours before anything fails.
On Comfy Cloud, the plan’s concurrent-job limit (1, 3, or 5) is the real width
of the fan-out: a full queue answers 429, the SDK retries for 60 seconds, and
then the node fails with Comfy queue is full.
Recipe 3 · A stored asset in, a stored asset out
What it does. Feeds a NodeTool image asset into a LoadImage node and puts
what comes back into an asset folder.
Nodes. nodetool.input.ImageInput, lib.comfy.RunWorkflow,
nodetool.image.SaveImage
Wiring. A connected image, audio, or video ref is uploaded to the ComfyUI
server before the prompt is submitted (POST /upload/image, which ComfyUI
accepts for any input file) and the stored filename is substituted into the
prompt. On Comfy Cloud the same ref goes through the SDK’s asset API and is
deduplicated by a blake3 hash of the bytes, so re-running with the same file
uploads nothing the second time.
Watch out. Outputs arrive as media refs carrying the bytes, not as saved
assets. Nothing is written to your asset library until a node writes it, which
is what nodetool.image.SaveImage is for (nodetool.video.SaveVideo for
video). And a LoadImage field you leave as a literal filename still resolves
against the ComfyUI server’s own input directory, which is why an unwired
workflow keeps working and a wired one replaces the file.
Recipe 4 · Frames into a cut
What it does. A batch of stills out of ComfyUI becomes a video without a second tool.
Nodes. lib.comfy.RunWorkflow, nodetool.video.FrameToVideo,
nodetool.output.Output
Wiring. The comfy node emits one media ref per output file on the same
slot, so a batch of four images is four frames rather than one list.
nodetool.video.FrameToVideo takes a streamed input and collects them into a
video at the fps you set. For a timeline instead of a flat encode, send the
slot through nodetool.control.Collect into nodetool.timeline.AddClips
(which has an image_duration_ms property) and render it with
nodetool.timeline.RenderTimeline.
Watch out. Nodes ComfyUI serves from its cache never emit an executed
event, so their files are reconciled from /history/<prompt_id> after the run
rather than streamed during it. A fully cached batch therefore arrives all at
once at the end. Frame order follows emission order, so a graph with several
save nodes gives you several slots to wire separately rather than one ordered
stream.
Recipe 5 · Behind a form, in a mini app
What it does. Puts the workflow in front of someone who should not have to read a node graph, or install ComfyUI.
How. Save the graph, open its app tab’s Design view, add fields wired to the workflow’s Input nodes, add a Button whose On click action is Run workflow, and wire an Image result widget to the Output node carrying the comfy slot. App Builder covers the editor, and Mini Apps covers how one runs.
Watch out. Widgets wire to Input and Output nodes, not to comfy handles, so
every parameter the form should expose needs its own nodetool.input.* node in
front of the handle. Publishing locks in the current state of every workflow the
app runs, so a later edit to the graph does not reach the published app until
you publish again.
Recipe 6 · Ask for it in chat
What it does. Reaches a saved ComfyUI workflow by name instead of by tab.
How. Save the workflow in the editor, then select it in chat or ask for it by name. The results land in the thread. See Chat & Agents.
Watch out. The agent runs the graph as saved, so whatever the ComfyUI node
holds in its workflow property is what runs. This is the same server-side run
as every other path, which makes it the quickest check that endpoint is
reachable from the backend rather than only from your machine.
Running it without the editor
The CLI harnesses
validate first, because it costs nothing:
npm run dev:nodetool -- validate <workflow_id>
npm run dev:nodetool -- validate comfy-graph.json --json
It checks the NodeTool graph: unknown node types, missing required
properties, dangling or mis-typed edges, and model properties naming a provider
or a model id that does not exist (which is what catches a bad model on the
EnhancePrompt node in Recipe 1). It never opens a socket to ComfyUI, so
everything inside the workflow string is invisible to it. A missing
checkpoint, an uninstalled custom node, and a class_type that server does not
have all pass validation and fail at submit with Submit failed (400). An empty
workflow property passes too, and the node raises
ComfyUI workflow is required at run time.
Then debug, which runs the graph and keeps everything it emitted:
npm run dev:nodetool -- debug <workflow_id>
npm run dev:nodetool -- debug comfy-graph.json --params '{"brief":"a red bicycle"}'
npm run dev:nodetool -- debug <workflow_id> --trace
The bundle’s server/messages.jsonl holds the ComfyUI lifecycle the node
forwards into the run log: execution start, the count of reused nodes, the class
name of each executing node, sampler progress, and the error text ComfyUI
returned. That is the ComfyUI console without a ComfyUI browser tab.
--watch re-runs a file target on every save and prints a diff of the verdict.
To just run it:
npm run dev:nodetool -- workflows run <workflow_id> --params '{"brief":"a red bicycle"}'
npm run dev:nodetool -- run comfy-graph.ts --json
With --json, an image or video payload over 64 KiB is written to
nodetool-output/<job_id>/payload-N.<ext> and appears as a {"$file": …}
reference instead of being inlined. Flag reference:
CLI › validate,
CLI › debug,
Harness reference.
The DSL
packages/dsl/src/generated/lib.comfy.ts exports one function per node:
runWorkflow, runWorkflowOnWorker, and runWorkflowOnCloud. A file like this
runs with nodetool run:
import { libComfy, output, workflow } from "@nodetool-ai/dsl";
const prompt = {
"3": {
class_type: "KSampler",
inputs: { seed: 42, steps: 20, cfg: 7, model: ["4", 0] }
}
};
const comfy = libComfy.runWorkflow({
endpoint: "127.0.0.1:8188",
workflow: JSON.stringify(prompt),
timeout: 600
});
export const comfyRun = workflow(
output.output({ name: "result", value: comfy.output() })
);
The limitation is in the generated types. RunWorkflowInputs names the four
static properties (endpoint, api, workflow, timeout) and nothing else,
because the generator reads the node’s declared props and the
<comfyNodeId>:<field> handles are derived from a workflow it has never seen.
A dynamic key in a DSL literal is a type error:
error TS2353: Object literal may only specify known properties,
and '"6:text"' does not exist in type 'RunWorkflowInputs'.
So a DSL-authored comfy node submits its prompt with nothing injected. Two ways around it, in order of preference:
- Build the values into the prompt JSON. The node injects dynamic props
into
prompt[nodeId].inputs[field]anyway, so writing them into the object you stringify reaches the same place. This covers seeds, steps, CFG, and prompt text, which is most of what a script varies. - Author the graph in the editor when the workflow needs a NodeTool media
ref on a
Load*handle. That upload only happens for a connected ref, so there is no JSON you can write instead. Run the saved graph by id from the CLI or the API.
The API
A saved workflow containing a ComfyUI node runs like any other:
curl -X POST "http://localhost:7777/api/workflows/YOUR_WORKFLOW_ID/run" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"params": {"brief": "a red bicycle"}}'
The response is one JSON object with job_id, workflow_id, status,
outputs, error, message_count, and background. outputs is keyed by
output-node name, so the comfy slot reaches the caller under whatever you named
the nodetool.output.Output node wired to it.
params is keyed by input-node name, not by comfy handle. A workflow whose
6:text is fed by a literal takes no parameter at all. Put a
nodetool.input.StringInput in front of the handle and its name becomes the
key. The route does not stream, so per-node progress and the ComfyUI lifecycle
lines need the /ws WebSocket instead. Full shapes:
API Reference, WebSocket API.
Cost, time, and failure
Cost. No ComfyUI run writes a provider cost record, on any of the three
nodes, so nodetool costs shows nothing for one. A direct or worker run is
billed by whoever owns the GPU. A Comfy Cloud run is billed per GPU second by
plan, and Comfy’s v2 job response carries no cost field, so the node records the
job id and no charge amount. The run log line Comfy job <id> submitted is the
join key against Comfy’s own billing. See
Known limitations.
Time. All three nodes default timeout to 600 seconds and each bounds the
NodeTool side of the run. Raise it for video and upscale graphs. The message
names which side gave up: Timeout waiting for ComfyUI result on the direct
native path, ComfyUI workflow did not finish within <n>s on a v2 path, and
Comfy Cloud job did not finish within <n>s on Cloud, whose own cap is 30
minutes per job (60 on Pro) regardless of what you set here.
Cancellation does not reach ComfyUI the same way everywhere:
| Path | What a cancelled NodeTool run does |
|---|---|
RunWorkflow, api: native |
Posts /interrupt and closes the WebSocket, then fails with ComfyUI execution was canceled |
RunWorkflow, api: v2 |
Calls the job’s cancel endpoint, so the job stops server-side |
RunWorkflowOnCloud |
Calls the job’s cancel endpoint, so Comfy stops billing the run |
RunWorkflowOnWorker, v2 path |
Calls the job’s cancel endpoint |
RunWorkflowOnWorker, bridge path |
Makes no cancel call. The bridge connection closes when the call settles, and the prompt runs on inside the worker’s ComfyUI |
The bridge path is the one every shipped worker takes today, so treat a
long-running worker run as uncancellable and set timeout accordingly. The
error message table for everything else is
Troubleshooting.
Related
- ComfyUI: the three nodes, their properties, handles, outputs, and limitations
- ComfyUI Setup: getting a server the node can reach
- Creative Cookbook: the same fan-out, collect, and render shapes, without ComfyUI in the middle
- CLI Reference: every flag on
validate,debug,run, andworkflows run - Harness Reference: what each harness simulates and what it does not
- App Builder: the Design view Recipe 5 uses
- API Reference: endpoint matrix, auth, and the run payload