Disciplines · Integrations

ComfyUI Workflow Documentation

ComfyUI workflows are node-based graphs that define image generation pipelines.

11sections1 minread

On this page

This guide covers how to create, structure, and execute ComfyUI workflows in the Oshun platform.

Table of Contents#


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#

  1. Open ComfyUI in your browser
  2. Build your workflow using the node editor
  3. Test the workflow with sample inputs
  4. 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)!);
}