Build a graph
workflow(name) returns a GraphBuilder. g.add(spec, params) adds a typed node. g.toGraph() is Graph IR.
/**
* Code-first quickstart graph. Edit this file; do not hand-edit compiled JSON.
*/
import { compile, workflow, type Graph } from "@stepupgaming/comfy-workflows";
import { conditioning, image, latent, loaders, sampling } from "@stepupgaming/comfy-workflows/nodes";
export function build(): Graph {
const g = workflow("docs-t2i");
const ckpt = g.add(loaders.CheckpointLoaderSimple, {
ckpt_name: "v1-5-pruned-emaonly.safetensors",
});
const positive = g.add(conditioning.CLIPTextEncode, {
text: "a red cube on a table, still camera",
clip: ckpt.CLIP,
});
const negative = g.add(conditioning.CLIPTextEncode, {
text: "blurry, text, watermark",
clip: ckpt.CLIP,
});
const empty = g.add(latent.EmptyLatentImage, {
width: 512,
height: 512,
batch_size: 1,
});
const sampled = g.add(sampling.KSampler, {
model: ckpt.MODEL,
positive: positive.CONDITIONING,
negative: negative.CONDITIONING,
latent_image: empty.LATENT,
seed: 42n,
steps: 8,
cfg: 7,
sampler_name: "euler",
scheduler: "normal",
denoise: 1,
});
const decoded = g.add(latent.VAEDecode, {
samples: sampled.LATENT,
vae: ckpt.VAE,
});
g.add(image.SaveImage, {
images: decoded.IMAGE,
filename_prefix: "docs-t2i",
});
g.output(decoded.IMAGE, { name: "image" });
return g.toGraph();
}
export function compiledJson(): string {
const result = compile(build());
if (!result.ok) {
throw new Error(result.errors.map((e) => `${e.code}: ${e.message}`).join("\n"));
}
return result.json;
}Creating a graph
import { workflow } from "@stepupgaming/comfy-workflows";
const g = workflow("mine");name is metadata. It shows up in explainGraph / cwf explain.
Adding typed nodes
params is checked against the spec:
- Widget inputs:
string,number,bigint,boolean, combo strings - Connection inputs: an output handle of the declared socket type
- Everything: a
ParamReffromg.param(...)
A MODEL wired into a CLIP input is a type error.
You can import category namespaces or named specs:
import { loaders, sampling } from "@stepupgaming/comfy-workflows/nodes";
import { KSampler } from "@stepupgaming/comfy-workflows/nodes";Widget vs connection
The builder splits params for you. If the value is a node output handle, it becomes inputs[name] = { node, out }. Otherwise it becomes params[name].
Node ids and titles
const ks = g.add(sampling.KSampler, { /* ... */ }, { id: "sampler" });
g.setTitle(ks.id, "Main pass");Imported graphs pass { id } so Comfy errors map back to the original ids. Fresh graphs get n1, n2, … unless you set an id.
Graph outputs
g.output(decoded.IMAGE, { name: "image" });Without an explicit list, some recipes declare a sensible default (for example the decoded image). Runtime artifact fetch follows graph.outputs.
Modes
g.setMode(ks.id, "bypassed"); // active | bypassed | muted
g.setBypassMap(ks.id, { 0: "model" });Bypass lowering is conservative. No map → E_UNRESOLVED_BYPASS. Muted node with a consumer → E_MUTED_CONSUMED. Bypass.
Serialization
import { parseGraph, serializeGraph } from "@stepupgaming/comfy-workflows";
const text = serializeGraph(g.toGraph(), { pretty: true });
const again = parseGraph(text);Next: Connections · Parameters