This guide covers how to create, structure, and execute ComfyUI workflows in the Oshun platform.
Table of Contents#
- Workflow Overview
- Workflow Structure
- Creating Workflows
- Built-in Generation Types
- Custom Workflows
- Parameter Injection
- Workflow Validation
- Workflow Templates
- Best Practices
Workflow Overview#
ComfyUI workflows are node-based graphs that define image generation pipelines. Each workflow consists of interconnected nodes that process data sequentially or in parallel.
Architecture#
text
┌─────────────────────────────────────────────────────────────────────────────┐
│ ComfyUI Workflow Execution │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Input Processing Output │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Prompt │──────▶│ CLIP Encode │──┐ │ │ │
│ │ (Text) │ └──────────────┘ │ │ Save │ │
│ └──────────────┘ │ │ Image │ │
│ ▼ │ │ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────┐ │ │ │
│ │ Checkpoint │──────▶│ KSampler │─┤ VAE ├─▶│ │ │
│ │ Loader │ └──────────────┘ │ Decode │ │ │ │
│ └──────────────┘ ▲ └────────┘ └──────────────┘ │
│ │ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Empty │──────▶│ Latent │─┘ │
│ │ Latent │ │ Image │ │
│ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Workflow Components#
| Component | Description |
|---|---|
| Nodes | Processing units that perform specific operations |
| Edges | Connections between node outputs and inputs |
| Inputs | External values injected into the workflow |
| Outputs | Generated results (images, videos, data) |
Workflow Structure#
JSON Format#
ComfyUI workflows use a JSON format with numbered nodes:
json
{
"3": {
"class_type": "KSampler",
"inputs": {
"seed": 12345,
"steps": 20,
"cfg": 7.5,
"sampler_name": "euler_ancestral",
"scheduler": "normal",
"denoise": 1.0,
"model": ["4", 0],
"positive": ["6", 0],
"negative": ["7", 0],
"latent_image": ["5", 0]
}
},
"4": {
"class_type": "CheckpointLoaderSimple",
"inputs": {
"ckpt_name": "sd_xl_base_1.0.safetensors"
}
},
"5": {
"class_type": "EmptyLatentImage",
"inputs": {
"width": 1024,
"height": 1024,
"batch_size": 1
}
},
"6": {
"class_type": "CLIPTextEncode",
"inputs": {
"text": "A beautiful sunset over mountains",
"clip": ["4", 1]
}
},
"7": {
"class_type": "CLIPTextEncode",
"inputs": {
"text": "blurry, low quality",
"clip": ["4", 1]
}
},
"8": {
"class_type": "VAEDecode",
"inputs": {
"samples": ["3", 0],
"vae": ["4", 2]
}
},
"9": {
"class_type": "SaveImage",
"inputs": {
"filename_prefix": "ComfyUI",
"images": ["8", 0]
}
}
}
Node Reference Format#
Connections use the format ["node_id", output_index]:
typescript
// Connection from node 4, output slot 0
"model": ["4", 0]
// Connection from node 6, output slot 0
"positive": ["6", 0]
TypeScript Interface#
typescript
interface ComfyWorkflow {
nodes: Record<string, ComfyNode>;
metadata?: WorkflowMetadata;
}
interface ComfyNode {
class_type: string;
inputs: Record<string, ComfyNodeInput>;
_meta?: Record<string, unknown>;
}
type ComfyNodeInput =
| string
| number
| boolean
| [string, number] // Node reference [nodeId, outputIndex]
| ComfyNodeInput[];
interface WorkflowMetadata {
title?: string;
description?: string;
author?: string;
version?: string;
tags?: string[];
}
Creating Workflows#
Using ComfyUI Editor#
- Open ComfyUI in your browser
- Build your workflow using the node editor
- Test the workflow with sample inputs
- Export via "Save (API Format)" or the developer console
Programmatic Creation#
typescript
import { ComfyWorkflow, ComfyNode } from '@oshun/comfy-types';
// Build a simple txt2img workflow
function buildTxt2ImgWorkflow(params: {
prompt: string;
negativePrompt: string;
model: string;
width: number;
height: number;
steps: number;
cfg: number;
seed: number;
}): ComfyWorkflow {
return {
nodes: {
// Checkpoint loader
'1': {
class_type: 'CheckpointLoaderSimple',
inputs: {
ckpt_name: params.model,
},
},
// Empty latent image
'2': {
class_type: 'EmptyLatentImage',
inputs: {
width: params.width,
height: params.height,
batch_size: 1,
},
},
// Positive prompt encoding
'3': {
class_type: 'CLIPTextEncode',
inputs: {
text: params.prompt,
clip: ['1', 1], // CLIP from checkpoint
},
},
// Negative prompt encoding
'4': {
class_type: 'CLIPTextEncode',
inputs: {
text: params.negativePrompt,
clip: ['1', 1],
},
},
// Sampler
'5': {
class_type: 'KSampler',
inputs: {
seed: params.seed,
steps: params.steps,
cfg: params.cfg,
sampler_name: 'euler_ancestral',
scheduler: 'normal',
denoise: 1.0,
model: ['1', 0], // Model from checkpoint
positive: ['3', 0], // Positive conditioning
negative: ['4', 0], // Negative conditioning
latent_image: ['2', 0], // Empty latent
},
},
// VAE decode
'6': {
class_type: 'VAEDecode',
inputs: {
samples: ['5', 0], // Latents from sampler
vae: ['1', 2], // VAE from checkpoint
},
},
// Save image
'7': {
class_type: 'SaveImage',
inputs: {
filename_prefix: 'oshun',
images: ['6', 0],
},
},
},
metadata: {
title: 'Simple Txt2Img',
version: '1.0',
},
};
}
Workflow Builder Pattern#
typescript
class WorkflowBuilder {
private nodeId = 0;
private nodes: Record<string, ComfyNode> = {};
private nextId(): string {
return String(++this.nodeId);
}
loadCheckpoint(ckptName: string): {
model: string;
clip: string;
vae: string;
} {
const id = this.nextId();
this.nodes[id] = {
class_type: 'CheckpointLoaderSimple',
inputs: { ckpt_name: ckptName },
};
return {
model: `${id}:0`,
clip: `${id}:1`,
vae: `${id}:2`,
};
}
encodeText(text: string, clip: string): string {
const id = this.nextId();
const [nodeId, outputIdx] = clip.split(':');
this.nodes[id] = {
class_type: 'CLIPTextEncode',
inputs: {
text,
clip: [nodeId, parseInt(outputIdx)],
},
};
return `${id}:0`;
}
emptyLatent(width: number, height: number, batchSize = 1): string {
const id = this.nextId();
this.nodes[id] = {
class_type: 'EmptyLatentImage',
inputs: { width, height, batch_size: batchSize },
};
return `${id}:0`;
}
sample(params: {
model: string;
positive: string;
negative: string;
latent: string;
steps: number;
cfg: number;
seed: number;
sampler?: string;
scheduler?: string;
denoise?: number;
}): string {
const id = this.nextId();
const parseRef = (ref: string): [string, number] => {
const [nodeId, idx] = ref.split(':');
return [nodeId, parseInt(idx)];
};
this.nodes[id] = {
class_type: 'KSampler',
inputs: {
seed: params.seed,
steps: params.steps,
cfg: params.cfg,
sampler_name: params.sampler || 'euler_ancestral',
scheduler: params.scheduler || 'normal',
denoise: params.denoise ?? 1.0,
model: parseRef(params.model),
positive: parseRef(params.positive),
negative: parseRef(params.negative),
latent_image: parseRef(params.latent),
},
};
return `${id}:0`;
}
decode(samples: string, vae: string): string {
const id = this.nextId();
const parseRef = (ref: string): [string, number] => {
const [nodeId, idx] = ref.split(':');
return [nodeId, parseInt(idx)];
};
this.nodes[id] = {
class_type: 'VAEDecode',
inputs: {
samples: parseRef(samples),
vae: parseRef(vae),
},
};
return `${id}:0`;
}
saveImage(images: string, prefix = 'oshun'): string {
const id = this.nextId();
const [nodeId, outputIdx] = images.split(':');
this.nodes[id] = {
class_type: 'SaveImage',
inputs: {
filename_prefix: prefix,
images: [nodeId, parseInt(outputIdx)],
},
};
return `${id}:0`;
}
build(): ComfyWorkflow {
return { nodes: this.nodes };
}
}
// Usage
const builder = new WorkflowBuilder();
const { model, clip, vae } = builder.loadCheckpoint(
'sd_xl_base_1.0.safetensors'
);
const positive = builder.encodeText('A beautiful sunset', clip);
const negative = builder.encodeText('blurry, low quality', clip);
const latent = builder.emptyLatent(1024, 1024);
const samples = builder.sample({
model,
positive,
negative,
latent,
steps: 20,
cfg: 7.5,
seed: 12345,
});
const images = builder.decode(samples, vae);
builder.saveImage(images);
const workflow = builder.build();
Built-in Generation Types#
The Oshun platform provides built-in generation types that automatically construct optimized workflows.
Text-to-Image (txt2img)#
typescript
import { ComfyUIService } from '@oshun/comfyui-service';
const service = new ComfyUIService(config);
const result = await service.generateTxt2Img({
prompt: 'A majestic dragon flying over a castle at sunset',
negativePrompt: 'blurry, low quality, distorted',
model: 'sd_xl_base_1.0.safetensors',
width: 1024,
height: 1024,
steps: 30,
cfg: 7.5,
seed: -1, // Random seed
sampler: 'euler_ancestral',
scheduler: 'normal',
});
console.log('Generated image:', result.outputs[0].url);
Image-to-Image (img2img)#
typescript
const result = await service.generateImg2Img({
prompt: 'Transform into a watercolor painting',
negativePrompt: 'photo, realistic',
image: 'https://example.com/input.png', // or base64
model: 'sd_xl_base_1.0.safetensors',
strength: 0.75, // Denoising strength
width: 1024,
height: 1024,
steps: 30,
cfg: 7.5,
seed: -1,
});
Inpainting#
typescript
const result = await service.generateInpaint({
prompt: 'A red sports car',
negativePrompt: 'blurry',
image: 'https://example.com/scene.png',
mask: 'https://example.com/mask.png', // White = inpaint area
model: 'sd_xl_base_1.0_inpainting.safetensors',
width: 1024,
height: 1024,
steps: 30,
cfg: 7.5,
seed: -1,
});
Upscaling#
typescript
const result = await service.generateUpscale({
image: 'https://example.com/low_res.png',
upscaler: 'RealESRGAN_x4plus',
scale: 4,
});
Custom Workflows#
Submitting Custom Workflows#
typescript
import { RunComfyProvider } from '@oshun/comfy-provider';
const provider = new RunComfyProvider({
apiKey: process.env.RUNCOMFY_API_KEY,
});
// Load workflow from file or define inline
const workflow: ComfyWorkflow = {
nodes: {
// ... your custom workflow nodes
},
};
const result = await provider.submitJob({
workflow,
inputs: [
{ name: 'positive_prompt', value: 'A beautiful landscape' },
{ name: 'negative_prompt', value: 'ugly, blurry' },
{ name: 'seed', value: 12345 },
],
webhookUrl: 'https://api.myapp.com/webhooks/comfyui',
priority: 1,
timeoutMs: 300000,
});
console.log('Job ID:', result.jobId);
console.log('Status:', result.status);
Workflow with ControlNet#
typescript
const controlNetWorkflow: ComfyWorkflow = {
nodes: {
// Checkpoint loader
'1': {
class_type: 'CheckpointLoaderSimple',
inputs: { ckpt_name: 'sd_xl_base_1.0.safetensors' },
},
// ControlNet loader
'2': {
class_type: 'ControlNetLoader',
inputs: { control_net_name: 'controlnet-canny-sdxl-1.0.safetensors' },
},
// Load control image
'3': {
class_type: 'LoadImage',
inputs: { image: 'control_image.png' },
},
// Canny edge detection
'4': {
class_type: 'CannyEdgePreprocessor',
inputs: {
image: ['3', 0],
low_threshold: 100,
high_threshold: 200,
resolution: 1024,
},
},
// Positive prompt
'5': {
class_type: 'CLIPTextEncode',
inputs: { text: 'A futuristic city', clip: ['1', 1] },
},
// Negative prompt
'6': {
class_type: 'CLIPTextEncode',
inputs: { text: 'blurry, distorted', clip: ['1', 1] },
},
// Apply ControlNet
'7': {
class_type: 'ControlNetApply',
inputs: {
conditioning: ['5', 0],
control_net: ['2', 0],
image: ['4', 0],
strength: 0.8,
},
},
// Empty latent
'8': {
class_type: 'EmptyLatentImage',
inputs: { width: 1024, height: 1024, batch_size: 1 },
},
// Sampler
'9': {
class_type: 'KSampler',
inputs: {
seed: 12345,
steps: 30,
cfg: 7.5,
sampler_name: 'euler_ancestral',
scheduler: 'normal',
denoise: 1.0,
model: ['1', 0],
positive: ['7', 0], // ControlNet-enhanced conditioning
negative: ['6', 0],
latent_image: ['8', 0],
},
},
// VAE decode
'10': {
class_type: 'VAEDecode',
inputs: { samples: ['9', 0], vae: ['1', 2] },
},
// Save
'11': {
class_type: 'SaveImage',
inputs: { filename_prefix: 'controlnet', images: ['10', 0] },
},
},
};
Workflow with LoRA#
typescript
const loraWorkflow: ComfyWorkflow = {
nodes: {
// Checkpoint loader
'1': {
class_type: 'CheckpointLoaderSimple',
inputs: { ckpt_name: 'sd_xl_base_1.0.safetensors' },
},
// LoRA loader
'2': {
class_type: 'LoraLoader',
inputs: {
model: ['1', 0],
clip: ['1', 1],
lora_name: 'my_style_lora.safetensors',
strength_model: 0.8,
strength_clip: 0.8,
},
},
// Second LoRA (stacking)
'3': {
class_type: 'LoraLoader',
inputs: {
model: ['2', 0], // Model from first LoRA
clip: ['2', 1], // CLIP from first LoRA
lora_name: 'detail_enhancer.safetensors',
strength_model: 0.5,
strength_clip: 0.5,
},
},
// ... rest of workflow using ['3', 0] as model and ['3', 1] as clip
},
};
Parameter Injection#
Input Variables#
Define injectable parameters in your workflow:
typescript
const templateWorkflow: ComfyWorkflow = {
nodes: {
'1': {
class_type: 'CheckpointLoaderSimple',
inputs: { ckpt_name: '{{model}}' }, // Template variable
},
'2': {
class_type: 'CLIPTextEncode',
inputs: {
text: '{{positive_prompt}}',
clip: ['1', 1],
},
},
'3': {
class_type: 'CLIPTextEncode',
inputs: {
text: '{{negative_prompt}}',
clip: ['1', 1],
},
},
'4': {
class_type: 'EmptyLatentImage',
inputs: {
width: '{{width}}',
height: '{{height}}',
batch_size: 1,
},
},
'5': {
class_type: 'KSampler',
inputs: {
seed: '{{seed}}',
steps: '{{steps}}',
cfg: '{{cfg}}',
sampler_name: '{{sampler}}',
scheduler: 'normal',
denoise: 1.0,
model: ['1', 0],
positive: ['2', 0],
negative: ['3', 0],
latent_image: ['4', 0],
},
},
// ...
},
};
// Inject values
function injectParameters(
workflow: ComfyWorkflow,
params: Record<string, unknown>
): ComfyWorkflow {
const json = JSON.stringify(workflow);
let result = json;
for (const [key, value] of Object.entries(params)) {
const placeholder = `{{${key}}}`;
const stringValue =
typeof value === 'string' ? `"${value}"` : String(value);
// Handle both quoted and unquoted placeholders
result = result.replace(new RegExp(`"${placeholder}"`, 'g'), stringValue);
result = result.replace(new RegExp(placeholder, 'g'), String(value));
}
return JSON.parse(result);
}
// Usage
const finalWorkflow = injectParameters(templateWorkflow, {
model: 'sd_xl_base_1.0.safetensors',
positive_prompt: 'A beautiful sunset',
negative_prompt: 'blurry',
width: 1024,
height: 1024,
seed: 12345,
steps: 30,
cfg: 7.5,
sampler: 'euler_ancestral',
});
Dynamic Node Configuration#
typescript
interface WorkflowConfig {
useRefiner: boolean;
useUpscaler: boolean;
useControlNet: boolean;
controlNetType?: 'canny' | 'depth' | 'pose' | 'scribble';
}
function buildDynamicWorkflow(
baseParams: GenerationParams,
config: WorkflowConfig
): ComfyWorkflow {
const builder = new WorkflowBuilder();
// Base checkpoint
const { model, clip, vae } = builder.loadCheckpoint(baseParams.model);
// Optional ControlNet
let conditioning = builder.encodeText(baseParams.prompt, clip);
if (config.useControlNet && config.controlNetType) {
conditioning = builder.addControlNet(
conditioning,
config.controlNetType,
baseParams.controlImage
);
}
// Sample
let samples = builder.sample({
model,
positive: conditioning,
negative: builder.encodeText(baseParams.negativePrompt, clip),
latent: builder.emptyLatent(baseParams.width, baseParams.height),
steps: baseParams.steps,
cfg: baseParams.cfg,
seed: baseParams.seed,
});
// Optional refiner
if (config.useRefiner) {
const refiner = builder.loadCheckpoint('sd_xl_refiner_1.0.safetensors');
samples = builder.sample({
model: refiner.model,
positive: builder.encodeText(baseParams.prompt, refiner.clip),
negative: builder.encodeText(baseParams.negativePrompt, refiner.clip),
latent: samples,
steps: 10,
cfg: 7.5,
seed: baseParams.seed,
denoise: 0.3,
});
}
// Decode
let images = builder.decode(samples, vae);
// Optional upscaler
if (config.useUpscaler) {
images = builder.upscale(images, 'RealESRGAN_x4plus', 2);
}
builder.saveImage(images);
return builder.build();
}
Workflow Validation#
Schema Validation#
typescript
import { z } from 'zod';
const ComfyNodeInputSchema = z.union([
z.string(),
z.number(),
z.boolean(),
z.tuple([z.string(), z.number()]), // Node reference
z.array(z.lazy(() => ComfyNodeInputSchema)),
]);
const ComfyNodeSchema = z.object({
class_type: z.string().min(1),
inputs: z.record(ComfyNodeInputSchema),
_meta: z.record(z.unknown()).optional(),
});
const ComfyWorkflowSchema = z.object({
nodes: z.record(ComfyNodeSchema),
metadata: z
.object({
title: z.string().optional(),
description: z.string().optional(),
author: z.string().optional(),
version: z.string().optional(),
tags: z.array(z.string()).optional(),
})
.optional(),
});
function validateWorkflow(workflow: unknown): ComfyWorkflow {
return ComfyWorkflowSchema.parse(workflow);
}
Structural Validation#
typescript
interface ValidationResult {
valid: boolean;
errors: ValidationError[];
warnings: ValidationWarning[];
}
interface ValidationError {
nodeId: string;
field: string;
message: string;
}
interface ValidationWarning {
nodeId: string;
message: string;
}
function validateWorkflowStructure(workflow: ComfyWorkflow): ValidationResult {
const errors: ValidationError[] = [];
const warnings: ValidationWarning[] = [];
const nodeIds = new Set(Object.keys(workflow.nodes));
for (const [nodeId, node] of Object.entries(workflow.nodes)) {
// Check class_type is valid
if (!node.class_type) {
errors.push({
nodeId,
field: 'class_type',
message: 'Node missing class_type',
});
}
// Check input references
for (const [inputName, inputValue] of Object.entries(node.inputs)) {
if (Array.isArray(inputValue) && inputValue.length === 2) {
const [refNodeId, outputIdx] = inputValue;
if (typeof refNodeId === 'string' && typeof outputIdx === 'number') {
// This is a node reference
if (!nodeIds.has(refNodeId)) {
errors.push({
nodeId,
field: inputName,
message: `Reference to non-existent node: ${refNodeId}`,
});
}
}
}
}
// Warn about common issues
if (node.class_type === 'KSampler' && !node.inputs.seed) {
warnings.push({
nodeId,
message: 'KSampler missing seed - results will be non-deterministic',
});
}
}
// Check for output nodes
const hasOutput = Object.values(workflow.nodes).some(
(n) => n.class_type === 'SaveImage' || n.class_type === 'PreviewImage'
);
if (!hasOutput) {
warnings.push({
nodeId: '',
message: 'Workflow has no output node (SaveImage/PreviewImage)',
});
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
Cycle Detection#
typescript
function detectCycles(workflow: ComfyWorkflow): string[][] {
const cycles: string[][] = [];
const visited = new Set<string>();
const recursionStack = new Set<string>();
function getReferencedNodes(node: ComfyNode): string[] {
const refs: string[] = [];
for (const input of Object.values(node.inputs)) {
if (Array.isArray(input) && input.length === 2) {
const [refNodeId] = input;
if (typeof refNodeId === 'string') {
refs.push(refNodeId);
}
}
}
return refs;
}
function dfs(nodeId: string, path: string[]): void {
visited.add(nodeId);
recursionStack.add(nodeId);
path.push(nodeId);
const node = workflow.nodes[nodeId];
if (node) {
for (const refNodeId of getReferencedNodes(node)) {
if (!visited.has(refNodeId)) {
dfs(refNodeId, [...path]);
} else if (recursionStack.has(refNodeId)) {
// Found cycle
const cycleStart = path.indexOf(refNodeId);
cycles.push([...path.slice(cycleStart), refNodeId]);
}
}
}
recursionStack.delete(nodeId);
}
for (const nodeId of Object.keys(workflow.nodes)) {
if (!visited.has(nodeId)) {
dfs(nodeId, []);
}
}
return cycles;
}
Workflow Templates#
Template Repository#
typescript
interface WorkflowTemplate {
id: string;
name: string;
description: string;
category: 'txt2img' | 'img2img' | 'inpaint' | 'upscale' | 'video' | 'custom';
parameters: TemplateParameter[];
workflow: ComfyWorkflow;
requiredModels: string[];
requiredNodes: string[];
estimatedTime: number; // seconds
estimatedVram: number; // GB
}
interface TemplateParameter {
name: string;
type: 'string' | 'number' | 'boolean' | 'select' | 'image';
default?: unknown;
required: boolean;
description: string;
options?: string[]; // For select type
min?: number; // For number type
max?: number; // For number type
}
const WORKFLOW_TEMPLATES: WorkflowTemplate[] = [
{
id: 'sdxl-txt2img-basic',
name: 'SDXL Text-to-Image',
description: 'Basic SDXL text-to-image generation',
category: 'txt2img',
parameters: [
{
name: 'prompt',
type: 'string',
required: true,
description: 'Generation prompt',
},
{
name: 'negative_prompt',
type: 'string',
required: false,
default: '',
description: 'Negative prompt',
},
{
name: 'width',
type: 'number',
required: false,
default: 1024,
min: 512,
max: 2048,
description: 'Image width',
},
{
name: 'height',
type: 'number',
required: false,
default: 1024,
min: 512,
max: 2048,
description: 'Image height',
},
{
name: 'steps',
type: 'number',
required: false,
default: 30,
min: 1,
max: 150,
description: 'Sampling steps',
},
{
name: 'cfg',
type: 'number',
required: false,
default: 7.5,
min: 1,
max: 30,
description: 'CFG scale',
},
{
name: 'seed',
type: 'number',
required: false,
default: -1,
description: 'Random seed (-1 for random)',
},
],
workflow: {
/* ... */
},
requiredModels: ['sd_xl_base_1.0.safetensors'],
requiredNodes: [],
estimatedTime: 30,
estimatedVram: 8,
},
// ... more templates
];
function getTemplateById(id: string): WorkflowTemplate | undefined {
return WORKFLOW_TEMPLATES.find((t) => t.id === id);
}
function instantiateTemplate(
templateId: string,
params: Record<string, unknown>
): ComfyWorkflow {
const template = getTemplateById(templateId);
if (!template) {
throw new Error(`Template not found: ${templateId}`);
}
// Validate required parameters
for (const param of template.parameters) {
if (param.required && !(param.name in params)) {
throw new Error(`Missing required parameter: ${param.name}`);
}
}
// Apply defaults
const finalParams: Record<string, unknown> = {};
for (const param of template.parameters) {
finalParams[param.name] = params[param.name] ?? param.default;
}
return injectParameters(template.workflow, finalParams);
}
Best Practices#
1. Use Deterministic Seeds#
typescript
// For reproducibility, always set explicit seeds
const seed =
params.seed === -1 ? Math.floor(Math.random() * 2147483647) : params.seed;
// Store the seed with the result for reproducibility
const result = await provider.submitJob({
workflow,
metadata: { seed },
});
2. Optimize Node Order#
Place nodes in execution order to help visualization and debugging:
typescript
// Good: Sequential IDs match execution order
const workflow = {
nodes: {
'1': { class_type: 'CheckpointLoaderSimple', ... }, // Load first
'2': { class_type: 'CLIPTextEncode', ... }, // Then encode
'3': { class_type: 'EmptyLatentImage', ... }, // Create latent
'4': { class_type: 'KSampler', ... }, // Then sample
'5': { class_type: 'VAEDecode', ... }, // Decode
'6': { class_type: 'SaveImage', ... }, // Finally save
},
};
3. Validate Before Submission#
typescript
async function submitWorkflow(workflow: ComfyWorkflow): Promise<JobResult> {
// Validate structure
const validation = validateWorkflowStructure(workflow);
if (!validation.valid) {
throw new Error(`Invalid workflow: ${JSON.stringify(validation.errors)}`);
}
// Check for cycles
const cycles = detectCycles(workflow);
if (cycles.length > 0) {
throw new Error(`Workflow contains cycles: ${JSON.stringify(cycles)}`);
}
// Log warnings
for (const warning of validation.warnings) {
console.warn(`Workflow warning: ${warning.message}`);
}
return provider.submitJob({ workflow });
}
4. Handle Large Workflows#
typescript
// Split large workflows into stages
async function executeMultiStage(
stages: ComfyWorkflow[]
): Promise<JobOutput[]> {
const outputs: JobOutput[] = [];
for (const [index, workflow] of stages.entries()) {
console.log(`Executing stage ${index + 1}/${stages.length}`);
const result = await provider.submitJob({ workflow });
outputs.push(...result.outputs);
// Pass outputs to next stage if needed
if (index < stages.length - 1) {
// Upload intermediate results for next stage
}
}
return outputs;
}
5. Cache Workflow Templates#
typescript
const workflowCache = new Map<string, ComfyWorkflow>();
function getCachedWorkflow(templateId: string): ComfyWorkflow {
if (!workflowCache.has(templateId)) {
const template = getTemplateById(templateId);
if (template) {
workflowCache.set(templateId, structuredClone(template.workflow));
}
}
return structuredClone(workflowCache.get(templateId)!);
}