A TypeScript library specialized for editing and validating ComfyUI workflow JSON.
Unlike direct JSON editing, this library ensures node reference integrity and prevents invalid connections. Ideal for cases where you want to safely modify JSON, such as workflow manipulation by AI agents.
git clone https://github.com/Yeq6X/comfyui-graph-utils.gitimport { ComfyWorkflow } from './comfyui-graph-utils';Sample scripts are available in the examples/ directory. You can run them directly with npx tsx.
# Generate an SDXL workflow and output to a JSON file
npx tsx examples/sdxl-workflow.ts > sdxl-workflow.json
# Output to console (with workflow info)
npx tsx examples/sdxl-workflow.ts
import { ComfyWorkflow } from 'comfyui-graph-utils';
const workflow = new ComfyWorkflow();
// Add nodes (IDs are auto-generated)
const vaeId = workflow.addNode('VAELoader', {
vae_name: 'hunyuan_video_vae_bf16.safetensors'
});
const samplerId = workflow.addNode('KSampler', {
steps: 20,
cfg: 7.5,
seed: 12345
});
// Connect nodes
workflow.addEdge(vaeId, 0, samplerId, 'model');
// Export as JSON
const json = workflow.toJson();// Load from object
const workflow = ComfyWorkflow.fromJson(existingWorkflowJson);
// Load from JSON string
const workflow2 = ComfyWorkflow.fromJson('{"1": {"inputs": {}, "class_type": "TestNode"}}');Creates an empty workflow.
const workflow = new ComfyWorkflow();Loads a workflow from existing JSON.
const workflow = ComfyWorkflow.fromJson(existingJson);| Parameter | Type | Description |
|---|---|---|
json |
ComfyWorkflowJson | string |
Workflow JSON (object or string) |
Adds a node and returns the generated ID.
// Basic usage
const nodeId = workflow.addNode('VAELoader', { vae_name: 'model.safetensors' });
// With custom ID
const nodeId2 = workflow.addNode('KSampler', { steps: 20 }, { id: 'my_sampler' });
// With metadata
const nodeId3 = workflow.addNode('LoadImage', {}, {
meta: { title: 'Start Image' }
});| Parameter | Type | Description |
|---|---|---|
classType |
string |
Node class type (e.g., VAELoader, KSampler) |
inputs |
object |
Input values (optional) |
options.id |
string |
Custom node ID (auto-generated if omitted) |
options.meta |
{ title?: string } |
Metadata (optional) |
Removes a node. Related edges are automatically removed.
workflow.removeNode('12');Gets a node. Returns undefined if not found.
const node = workflow.getNode('12');
if (node) {
console.log(node.class_type); // 'VAELoader'
console.log(node.inputs); // { vae_name: '...' }
}Gets all nodes.
const nodes = workflow.getNodes();
// { '12': { inputs: {...}, class_type: 'VAELoader' }, ... }Gets the number of nodes.
const count = workflow.getNodeCount(); // 15Searches nodes by class type.
const vaeLoaders = workflow.findNodesByType('VAELoader');
// [{ id: '12', node: { inputs: {...}, class_type: 'VAELoader' } }]
const samplers = workflow.findNodesByType('KSampler');
samplers.forEach(({ id, node }) => {
console.log(`Node ${id}: steps=${node.inputs.steps}`);
});Adds a connection between nodes.
// Connect VAELoader output port 0 to VAEEncode's vae input
workflow.addEdge('12', 0, '20', 'vae');
// Connect CLIP output port 0 to KSampler's positive input
workflow.addEdge('6', 0, '3', 'positive');| Parameter | Type | Description |
|---|---|---|
sourceNodeId |
string |
Source node ID |
sourcePort |
number |
Source output port number (0-based) |
targetNodeId |
string |
Target node ID |
targetInputName |
string |
Target input name |
Removes an edge.
workflow.removeEdge('20', 'vae');Gets all edges.
const edges = workflow.getEdges();
// [
// { sourceNodeId: '12', sourcePort: 0, targetNodeId: '20', targetInputName: 'vae' },
// { sourceNodeId: '18', sourcePort: 0, targetNodeId: '17', targetInputName: 'clip_vision' },
// ...
// ]Gets output edges from a specific node.
const edges = workflow.getEdgesFrom('12');Gets input edges to a specific node.
const edges = workflow.getEdgesTo('20');Checks if a connection exists between two nodes.
if (workflow.hasConnection('12', '20')) {
console.log('Connection exists from node 12 to 20');
}Sets an input value.
workflow.setInput('3', 'steps', 30);
workflow.setInput('3', 'cfg', 7.5);
workflow.setInput('3', 'seed', 999999);Gets an input value.
const steps = workflow.getInput('3', 'steps'); // 30
const connection = workflow.getInput('17', 'clip_vision'); // ['18', 0]Gets all inputs for a node.
const inputs = workflow.getInputs('3');
// { steps: 30, cfg: 7.5, seed: 999999, model: ['12', 0], ... }Updates multiple inputs at once.
workflow.updateInputs('3', {
steps: 25,
cfg: 8.0,
seed: 123456
});Removes an input.
workflow.clearInput('3', 'seed');Exports the workflow as a JSON object.
const json = workflow.toJson();Exports the workflow as a JSON string.
// Compact format
const compact = workflow.toJsonString();
// Pretty-printed (indent 2)
const pretty = workflow.toJsonString(2);A tuple representing a node connection.
type NodeConnection = [string, number]; // [nodeId, portNumber]Input value type.
type InputValue = string | number | boolean | NodeConnection | null;Node type.
interface ComfyNode {
inputs: { [key: string]: InputValue };
class_type: string;
_meta?: { title?: string };
}Edge type.
interface Edge {
sourceNodeId: string;
sourcePort: number;
targetNodeId: string;
targetInputName: string;
}Checks if a value is a NodeConnection.
import { isNodeConnection } from 'comfyui-graph-utils';
const value = node.inputs.model;
if (isNodeConnection(value)) {
const [nodeId, port] = value;
console.log(`Connection: node ${nodeId}, port ${port}`);
} else {
console.log(`Direct value: ${value}`);
}Checks if a value is a ComfyNode.
Checks if a value is a ComfyWorkflowJson.
Validates the entire workflow.
import { validateWorkflow } from 'comfyui-graph-utils';
const result = validateWorkflow(json);
if (!result.valid) {
console.error('Errors:', result.errors);
}
if (result.warnings.length > 0) {
console.warn('Warnings:', result.warnings);
}Validates only the workflow structure.
Validates workflow connections.
- Detects references to non-existent nodes as errors
- Detects isolated nodes as warnings
const workflow = ComfyWorkflow.fromJson(existingWorkflow);
// Change steps for all KSamplers
const samplers = workflow.findNodesByType('KSampler');
samplers.forEach(({ id }) => {
workflow.setInput(id, 'steps', 30);
});
// Randomize seeds for all KSamplers
samplers.forEach(({ id }) => {
workflow.setInput(id, 'seed', Math.floor(Math.random() * 1000000));
});
const updatedJson = workflow.toJson();const workflow = new ComfyWorkflow();
// Model loader
const checkpointId = workflow.addNode('CheckpointLoaderSimple', {
ckpt_name: 'v1-5-pruned.safetensors'
});
// CLIP text encoding
const positiveId = workflow.addNode('CLIPTextEncode', {
text: 'a beautiful landscape'
});
const negativeId = workflow.addNode('CLIPTextEncode', {
text: 'ugly, blurry'
});
// Empty latent
const latentId = workflow.addNode('EmptyLatentImage', {
width: 512,
height: 512,
batch_size: 1
});
// KSampler
const samplerId = workflow.addNode('KSampler', {
seed: 12345,
steps: 20,
cfg: 7.5,
sampler_name: 'euler',
scheduler: 'normal',
denoise: 1.0
});
// VAE decode
const decodeId = workflow.addNode('VAEDecode', {});
// Save image
const saveId = workflow.addNode('SaveImage', {
filename_prefix: 'output'
});
// Connections
workflow.addEdge(checkpointId, 0, samplerId, 'model');
workflow.addEdge(checkpointId, 1, positiveId, 'clip');
workflow.addEdge(checkpointId, 1, negativeId, 'clip');
workflow.addEdge(checkpointId, 2, decodeId, 'vae');
workflow.addEdge(positiveId, 0, samplerId, 'positive');
workflow.addEdge(negativeId, 0, samplerId, 'negative');
workflow.addEdge(latentId, 0, samplerId, 'latent_image');
workflow.addEdge(samplerId, 0, decodeId, 'samples');
workflow.addEdge(decodeId, 0, saveId, 'images');
const json = workflow.toJson();const workflow = ComfyWorkflow.fromJson(existingWorkflow);
// Find old VAELoader
const oldVae = workflow.findNodesByType('VAELoader')[0];
if (oldVae) {
// Add new VAELoader
const newVaeId = workflow.addNode('VAELoader', {
vae_name: 'new_vae_model.safetensors'
});
// Rewire edges from old node to new node
const edgesFromOld = workflow.getEdgesFrom(oldVae.id);
edgesFromOld.forEach(edge => {
workflow.removeEdge(edge.targetNodeId, edge.targetInputName);
workflow.addEdge(newVaeId, edge.sourcePort, edge.targetNodeId, edge.targetInputName);
});
// Remove old node
workflow.removeNode(oldVae.id);
}# Watch mode
npm run test
# Single run
npm run test:run