Parameters and templates
A literal is compiled into the graph. A ParamRef is a hole filled later.
seed: 42n // literal
seed: paramRef("seed") // or g.param("seed", { type: "int" })Topology stays put. Values arrive at instantiate / cwf run --param.
import {
instantiateTemplate,
workflow,
type Graph,
} from "@stepupgaming/comfy-workflows";
import { conditioning, image, latent, loaders, sampling } from "@stepupgaming/comfy-workflows/nodes";
export function buildTemplate(): Graph {
const g = workflow("docs-t2i-template");
const checkpoint = g.param("checkpoint", {
type: "combo",
description: "Checkpoint filename on the Comfy server.",
});
const prompt = g.param("prompt", {
type: "string",
description: "Positive prompt.",
});
const seed = g.param("seed", {
type: "int",
default: 42n,
description: "Sampling seed. Use bigint for the full 64-bit range.",
});
const steps = g.param("steps", { type: "int", default: 20 });
const ckpt = g.add(loaders.CheckpointLoaderSimple, { ckpt_name: checkpoint });
const positive = g.add(conditioning.CLIPTextEncode, { text: prompt, clip: ckpt.CLIP });
const negative = g.add(conditioning.CLIPTextEncode, { text: "", 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,
steps,
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-template" });
g.output(decoded.IMAGE, { name: "image" });
return g.toGraph();
}
export function bind(params: { checkpoint: string; prompt: string; seed?: bigint; steps?: number }): Graph {
return instantiateTemplate(buildTemplate(), {
params: {
checkpoint: params.checkpoint,
prompt: params.prompt,
...(params.seed !== undefined ? { seed: params.seed } : {}),
...(params.steps !== undefined ? { steps: params.steps } : {}),
},
});
}g.param
const seed = g.param("seed", {
type: "int", // int | float | string | boolean | combo
default: 42n,
description: "Sampling seed",
// options: [...] // combo
});Duplicate names throw. The returned ParamRef is { $param: "seed" } and can sit in any widget slot.
paramRef("seed") builds the same placeholder without declaring metadata. Recipes use it; declare the param on the builder (or let the recipe declare it) so instantiate knows the type/default.
Binding
import { instantiateTemplate } from "@stepupgaming/comfy-workflows";
const graph = instantiateTemplate(tpl, {
params: { prompt: "a lighthouse", seed: 42n },
// inputs: { image: someSlotRef },
});Unbound param with no default → E_UNBOUND_PARAM. Unbound port → E_UNBOUND_PORT. Instantiation renumbers nodes n1..nN in topological order so the same template + bindings compile identically.
CLI:
cwf run workflow.ts --url http://127.0.0.1:8188 --param prompt=hello --param seed=42--param is repeatable (-p).
Integer / bigint
Use bigint (42n) for seeds. number is accepted but cannot hold values above 2^53 exactly. Lossless integers.
Files and paths
Local files that must be uploaded are AssetRef, not string paths in a published default. Machine-local paths fail cwf pack (E_PACK_LOCAL_PATH). Checkpoint names on the server are portable parameters. Absolute C:\Users\... paths are not. Assets.
Discovery
Package manifests list parameters. cwf inspect prints required vs optional. cwf suggest proposes expose candidates without mutating.
Topology vs parameter
If changing a value changes which nodes exist, it is topology. Put it in TypeScript.
If it only changes a node input, it is a runtime parameter.
That rule keeps a Python/Rust binder stupid in a good way: replace {$param}, do not grow graphs. No second compiler.