- Notifications
You must be signed in to change notification settings - Fork1
A battle-tested, TypeScript-first client for interacting with ComfyUI.
License
zandko/comfyui-sdk
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
A battle-tested, TypeScript-first client for interacting with ComfyUI. Designed for large-scale Node.js applications,comfyui-sdk offers:
🔄Type-safe Workflow ExecutionFully typed payloads and results, ensuring end-to-end type safety when invoking ComfyUI workflows.
📁Streamlined File ManagementUpload, overwrite, and organize files on the ComfyUI server. Automatic format inference, custom subfolders, and overwrite flags.
🏊High-throughput Connection PoolingDistribute requests across multiple ComfyUI instances with configurable concurrency limits and built-in load balancing.
📝Flexible, Configurable LoggingBuilt-in logging framework with adjustable log levels (NONE, ERROR, WARN, INFO, DEBUG).
⚡Robust Retry & PollingExponential backoff, configurable retry caps, and efficient polling loops to handle long-running workflows.
🔐Built-in AuthenticationSimple API-key support for secure communication.
📦Advanced Pipeline SupportDefine reusable, type-safe input/output mappings for complex pipelines; integrate processors for artifact transformation.
🌐Cloud Storage IntegrationsNative AWS S3 and Tencent COS uploaders, complete with signing and URL generation.
🎯Session & Resource ManagementAutomatic session pooling, resource cleanup, and artifact pipelines that handle uploads, transformations, and metadata collection.
🔧Modular Artifact ProcessorsBuild custom processors with fine-grained
shouldRunlogic and type-safe state propagation viaPipelineBus.
Install via your preferred package manager:
npm install comfyui-sdk
yarn add comfyui-sdk
pnpm add comfyui-sdk
import{ComfyUIClient,LogLevel}from'comfyui-sdk'constclient=newComfyUIClient({baseUrl:'http://localhost:8188',// ComfyUI server URLapiKey:'your-api-key-here',// Optional API keytimeout:90_000,// Request timeout (ms)logging:true,// Enable logginglogLevel:LogLevel.INFO// Adjust log verbosity})
// A minimal workflow: encode text and load a checkpointconstsimpleWorkflow={1:{inputs:{text:'A serene mountain landscape'},class_type:'CLIPTextEncode'},2:{inputs:{ckpt_name:'sd_base_1.0.safetensors'},class_type:'CheckpointLoaderSimple'}}asyncfunctionrunSimple(){try{constartifacts=awaitclient.run(simpleWorkflow)console.log('Artifacts:',artifacts)}catch(err){console.error('Workflow execution failed:',err)}}runSimple()
importfsfrom'node:fs'// Read a local image into a Bufferconstbuffer=fs.readFileSync('input.jpg')asyncfunctionuploadExample(){constresult=awaitclient.uploadFile(buffer,{filename:'input.jpg',// Custom name on serversubfolder:'images',// Place under `uploaded/images/`override:true// Overwrite if file exists})console.log('Uploaded as:',result.name)}uploadExample()
The primary class to interface with ComfyUI. All network requests, logging, and pooling are managed here.
interfaceClientOptions{baseUrl:string// E.g., 'http://localhost:8188'apiKey?:string// Optional API keytimeout?:number// Request timeout in milliseconds (default: 90_000)poll?:{interval?:number// Polling interval in ms (default: 4_000)backoffBase?:number// Exponential backoff base in ms (default: 2_000)backoffCap?:number// Max backoff per retry in ms (default: 15_000)}logging?:boolean// Enable or disable logging (default: false)logLevel?:LogLevel// One of NONE, ERROR, WARN, INFO, DEBUG (default: INFO)}
Execute a ComfyUI workflow. Returns an array ofArtifact objects (binary, text, or JSON). Optionally, specify a type-safe mapping usingdefineConfig.
Parameters
workflow: An object whose keys are node IDs (strings or numbers) and whose values defineclass_typeandinputs.options(optional):interfaceRunOptions<TNode>{node?:TNode// A typed pipeline/node definition (from defineConfig)inputs?:Record<string,unknown>// Values for required/optional pipeline inputs}
Returns
Promise<Artifact[]>- If using
defineConfig, you’ll receive a strongly typed result object (with named outputs).
- If using
Example: Basic Execution
constartifacts=awaitclient.run({1:{class_type:'CLIPTextEncode',inputs:{text:'hello world'}}})console.log(artifacts)
Example: Type-safe Pipeline
import{defineConfig}from'comfyui-sdk'consttextToImageConfig=defineConfig({inputs:[{from:'prompt',to:'1.inputs.text',required:true},{from:'seed',to:'2.inputs.seed',defaultValue:42}]asconst,outputs:[{from:'5',to:'image'}]asconst})// Workflow must match the node mapping aboveconstworkflow={1:{class_type:'CLIPTextEncode',inputs:{text:'A cosmic vista'}},2:{class_type:'RandomSeed',inputs:{seed:123}},// ... other nodes up to node 5 that produce an image}const{ image}=awaitclient.run(workflow,{node:textToImageConfig,inputs:{prompt:'A vibrant galaxy',seed:2025}})console.log('Generated image artifact:',image)
Upload a binary to ComfyUI’s file repository. Automatically handles file naming, subfolders, and overwrites.
Parameters
file: A Node.jsBufferor a browserBlob.options:interfaceUploadOptions{override?:boolean// Overwrite if a file with the same name already exists (default: false)subfolder?:string// Subfolder inside `uploaded/`; auto-creates directories (default: 'uploaded')filename?:string// Custom filename; if omitted, a UUID-based name is generated}
Returns
Promise<UploadOutput>interfaceUploadOutput{name:string// Full path on server, e.g., 'uploaded/images/foo.jpg'manifest:{contentType:string// MIME type (e.g., 'image/jpeg')filename:string// The final filename on serversize:number// File size in bytes// ... other manifest fields}}
Example
importfsfrom'node:fs'constimageBuffer=fs.readFileSync('avatar.png')constresult=awaitclient.uploadFile(imageBuffer,{subfolder:'avatars',filename:'user123.png',override:true})console.log('File URL/name:',result.name)
Fetch the full execution history for a given prompt UUID.
Parameters
promptId: The string identifier returned by a previous workflow execution.
Returns
Promise<Histories>- An object mapping prompt IDs to status details, timestamps, logs, and node-level progress.
Example
consthistory=awaitclient.getHistory('c2f9a8d2-1b3e-4e7c-9a11-abcdef123456')console.log('Status:',history['c2f9a8d2-1b3e-4e7c-9a11-abcdef123456'].status)
A pool ofComfyUIClient instances for load balancing across multiple ComfyUI servers.
Parameters
interfaceInstanceConfig{baseUrl:string// ComfyUI server URL, e.g. 'https://comfyui-1.example.com'apiKey?:string// Optional API key for that instancemaxConcurrency:number// Maximum concurrent workflows on this instancetimeout?:number// Override default timeout (ms)}interfacePoolOptions{logging?:boolean// Enable logging for pool activitieslogLevel?:LogLevel// Log level for pooled clients}
Example
constpool=newComfyUIPool([{baseUrl:'http://server1:8188',maxConcurrency:2},{baseUrl:'http://server2:8188',maxConcurrency:3},{baseUrl:'http://server3:8188',maxConcurrency:1}],{logging:true,logLevel:LogLevel.INFO})
Acquire a leased client from the pool.
Returns
Promise<ClientLease>interfaceClientLease{client:ComfyUIClientrelease:()=>void// Must be called when done, to return the client to the pool}
Example
asyncfunctionuseLease(){constlease=awaitpool.lease()try{constresults=awaitlease.client.run(workflow)console.log('Results:',results)}finally{lease.release()}}
Start a session that ties together a leased client + artifact pipeline. Allows you to run multiple workflows in sequence, automatically cleaning up resources and running the pipeline on each artifact.
Parameters
pipeline: AnArtifactPipelinecontaining one or moreArtifactProcessors.
Returns
ComfyUISession | nullif no clients are currently available.Example
import{ArtifactPipeline,CosUploader}from'comfyui-sdk'constpipeline=newArtifactPipeline([newCosUploader({secretId:process.env.COS_SECRET_ID!,secretKey:process.env.COS_SECRET_KEY!,bucket:'my-bucket',region:'ap-shanghai',prefix:'comfyui-exports/',domain:'cdn.example.com'})])constsession=pool.createSession(pipeline)if(!session)thrownewError('No available ComfyUI clients')try{constartifacts=awaitsession.run(workflow)console.log('Session artifacts:',artifacts)}finally{session.close()}
Convenience wrapper: automatically acquires a session, runs your callback, then closes the session.
Example
import{ArtifactPipeline,CosUploader}from'comfyui-sdk'constpipeline=newArtifactPipeline([/* processors */])constresult=awaitpool.withSession(async(session)=>{returnsession.run(workflow,{node:myConfig,inputs:{prompt:'Auto'}})},pipeline)console.log('Result from withSession:',result)
TheArtifactPipeline enables you to chain multipleArtifactProcessor instances. Each processor can:
- Inspect and transform artifacts
- Upload results to cloud storage
- Emit metadata or new artifacts
- Use fine-grained
shouldRunlogic to skip irrelevant artifacts - Propagate state through a shared
PipelineBus
import{ArtifactPipeline,CosUploader,defineConfig}from'comfyui-sdk'// 1. Define custom or built-in processorsconstuploader=newCosUploader({secretId:'AKID...',secretKey:'SECRET...',bucket:'comfyui-bucket',region:'ap-guangzhou',prefix:'outputs/',signExpires:0,domain:'cdn.example.com'})// 2. Instantiate the pipeline with an ordered list of processorsconstpipeline=newArtifactPipeline([uploader,// ... you can add more custom processors here])
CosUploader (Tencent Cloud Object Storage)Uploads binary artifacts to COS and attaches
artifact.pipeline.cosUploader.url.S3Uploader (AWS S3)Equivalent functionality for AWS: configure
bucket,region, optionalprefix,ACL, etc.(Custom Processors)Extend
ArtifactProcessorto build your own. See “📜 Custom Processors” below.
ProcessorOutput<M>InterfaceEach processor now returns:interfaceProcessorOutput<M=unknown>{output:M// Metadata or intermediate datanext?:Pick<Artifact,'kind'|'payload'|'manifest'>// Optional transformed artifact}
This allows you to both store processing dataand replace the artifact payload/manifest in one step.
Artifact TransformationUse the
nextproperty to pass a newArtifactshape downstream:classWebPConverterextendsArtifactProcessor{readonlyname='webpConverter'asyncrun(artifact:Artifact):Promise<ProcessorOutput<{originalFormat:string,newFormat:string}>>{if(artifact.kind!=='binary'){return{output:{originalFormat:'n/a',newFormat:'n/a'}}}// Imagine convertToWebP returns a BufferconstconvertedBuffer=awaitthis.convertToWebP(artifact.payloadasBuffer)return{output:{originalFormat:artifact.manifest.contentType,newFormat:'image/webp'},next:{kind:'binary',payload:convertedBuffer,manifest:{ ...artifact.manifest,filename:artifact.manifest.filename.replace(/\.[^.]+$/,'.webp'),contentType:'image/webp'}}}}privateasyncconvertToWebP(buffer:Buffer):Promise<Buffer>{// Conversion logic...returnbuffer}}
shouldRunHooksEach processor can overrideshouldRun(artifact: Artifact): Promise<boolean>to decide if it should process that artifact:classLargeFileProcessorextendsArtifactProcessor{readonlyname='largeFileProcessor'asyncshouldRun(artifact:Artifact):Promise<boolean>{returnartifact.kind==='binary'&&(artifact.payloadasBuffer).byteLength>1_000_000}asyncrun(artifact:Artifact):Promise<ProcessorOutput<Record<string,unknown>>>{// Process large binaries only...return{output:{processed:true}}}}
Pipeline State ManagementProcessors can share state via the
artifact.pipelineobject (typed by extending thePipelineBusinterface).// Extend PipelineBus in a declaration file or at top of your codedeclare module'comfyui-sdk/types'{interfacePipelineBus{customProcessor?:CustomProcessorOutput}}classCustomProcessorextendsArtifactProcessor{readonlyname='customProcessor'asyncrun(artifact:Artifact):Promise<ProcessorOutput<CustomProcessorOutput>>{conststart=Date.now()// ...process artifact.payloadconstduration=Date.now()-startreturn{output:{ duration,size:(artifact.payloadasBuffer).byteLength}}}}
All key types are exported for maximum TypeScript support:
// Artifact can be a binary (Buffer/Blob), text, or JSON resulttypeArtifact=|{kind:'binary',payload:ArrayBuffer|Uint8Array|Buffer|Blob,manifest:BinaryManifest,pipeline?:PipelineState}|{kind:'text',payload:string,manifest:TextManifest,pipeline?:PipelineState}|{kind:'json',payload:unknown,manifest:JsonManifest,pipeline?:PipelineState}interfaceBinaryManifest{filename:stringcontentType:stringsize:number// Additional fields: promptId, nodeId, etc.}interfaceTextManifest{filename:stringcontentType:stringsize:number// Additional metadata}interfaceJsonManifest{filename:stringcontentType:string// Additional metadata}interfacePipelineBus{}typePipelineState=Record<string,unknown>&Partial<PipelineBus>interfaceProcessorOutput<M=unknown>{output:Mnext?:Pick<Artifact,'kind'|'payload'|'manifest'>}abstractclassArtifactProcessor{abstractreadonlyname:stringasyncshouldRun(artifact:Artifact):Promise<boolean>abstractrun(artifact:Artifact):Promise<ProcessorOutput>}interfaceRunOptions<TNode>{node?:TNodeinputs?:Record<string,unknown>}interfaceClientOptions{/* ... as above ... */}interfaceUploadOptions{/* ... as above ... */}interfaceUploadOutput{/* ... as above ... */}interfaceComfyUISession{run:<TNode>(workflow:WorkflowPayload,options?:RunOptions<TNode>)=>Promise<Artifact[]>uploadFile:(data:Buffer|Blob,options?:UploadOptions)=>Promise<UploadOutput>getHistory:(promptId:string)=>Promise<Histories>close:()=>void}classArtifactPipeline{constructor(processors:ArtifactProcessor[])run(artifacts:Artifact[]|Record<string,Artifact>):Promise<Artifact[]|Record<string,Artifact>>}
import{ComfyUIClient,LogLevel}from'comfyui-sdk'constdevClient=newComfyUIClient({baseUrl:'http://localhost:8188',logging:true,logLevel:LogLevel.DEBUG,timeout:60_000,poll:{interval:1_000}})
import{ComfyUIPool,LogLevel}from'comfyui-sdk'constprodPool=newComfyUIPool([{baseUrl:'https://comfyui-1.example.com',apiKey:process.env.COMFYUI_API_KEY,maxConcurrency:5,timeout:180_000},{baseUrl:'https://comfyui-2.example.com',apiKey:process.env.COMFYUI_API_KEY,maxConcurrency:3,timeout:180_000}],{logging:true,logLevel:LogLevel.INFO})
import{ArtifactPipeline,ComfyUIClient,ComfyUIPool,CosUploader,defineConfig,LogLevel}from'comfyui-sdk'// 1. Configure pool of ComfyUI serversconstpool=newComfyUIPool([{baseUrl:'http://localhost:8188',maxConcurrency:2}],{logging:true,logLevel:LogLevel.INFO})// 2. Build an artifact pipeline: convert to WebP → upload to COS → collect metadataclassWebPConverterextendsArtifactProcessor{readonlyname='webpConverter'asyncrun(artifact:Artifact){if(artifact.kind!=='binary')return{output:{}}constconverted=artifact.payloadasBuffer// pretend conversionreturn{output:{original:artifact.manifest.contentType,converted:'image/webp'},next:{kind:'binary',payload:converted,manifest:{ ...artifact.manifest,contentType:'image/webp',filename:artifact.manifest.filename.replace(/\.\w+$/,'.webp')}}}}}// Tencent COS uploaderconstcosUploader=newCosUploader({secretId:process.env.COS_SECRET_ID!,secretKey:process.env.COS_SECRET_KEY!,bucket:'comfyui-bucket',region:'ap-guangzhou',prefix:'outputs/',signExpires:0,domain:'cdn.example.com'})// Metadata collectorclassMetadataCollectorextendsArtifactProcessor{readonlyname='metadataCollector'asyncrun(artifact:Artifact){constuploadInfo=artifact.pipeline?.cosUploaderconstwebpInfo=artifact.pipeline?.webpConverterconstmetadata={timestamp:newDate().toISOString(),filename:artifact.manifest.filename,size:artifact.kind==='binary' ?(artifact.payloadasBuffer).byteLength :0,uploadUrl:uploadInfo?.url||null,conversion:webpInfo||null}return{output:metadata,next:{kind:'json',payload:metadata,manifest:{from:artifact.manifest.from,promptId:artifact.manifest.promptId,filename:`metadata_${artifact.manifest.filename}.json`,contentType:'application/json'}}}}}// Assemble the pipelineconstpipeline=newArtifactPipeline([newWebPConverter(),cosUploader,newMetadataCollector()])// 3. Define a type-safe text-to-image pipelineconsttextToImageConfig=defineConfig({inputs:[{from:'prompt',to:'6.inputs.text',required:true},{from:'negative_prompt',to:'7.inputs.text',defaultValue:''},{from:'width',to:'5.inputs.width',defaultValue:1024},{from:'height',to:'5.inputs.height',defaultValue:1024},{from:'steps',to:'3.inputs.steps',defaultValue:20},{from:'cfg',to:'3.inputs.cfg',defaultValue:7},{from:'seed',to:'3.inputs.seed',defaultValue:42}]asconst,outputs:[{from:'9',to:'image'},{from:'12',to:'metadata'}]asconst}asconst)// 4. The SDXL text-to-image workflowconstsdxlWorkflow={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:'sunset over ocean',clip:['4',1]}},7:{class_type:'CLIPTextEncode',inputs:{text:'',clip:['4',1]}},3:{class_type:'KSampler',inputs:{seed:42,steps:20,cfg:7,sampler_name:'euler',scheduler:'normal',denoise:1,model:['4',0],positive:['6',0],negative:['7',0],latent_image:['5',0]}},8:{class_type:'VAEDecode',inputs:{samples:['3',0],vae:['4',2]}},9:{class_type:'SaveImage',inputs:{filename_prefix:'ComfyUI',images:['8',0]}},12:{class_type:'SaveJSON',inputs:{data:['8',0],filename:'metadata.json'}}}// 5. Execute within a session so artifacts flow through the pipelineasyncfunctiongenerateImage(){constsession=pool.createSession(pipeline)if(!session)thrownewError('No available clients')try{const{ image, metadata}=awaitsession.run(sdxlWorkflow,{node:textToImageConfig,inputs:{prompt:'A majestic dragon soaring above clouds',width:1920,height:1080,steps:30}})console.log('Final Image Artifact:',image)console.log('Metadata JSON:',metadata)}catch(err){console.error('Generation error:',err)}finally{session.close()}}generateImage()
Extend the baseArtifactProcessor to implement bespoke processing logic:
import{ArtifactProcessor,ProcessorOutput}from'comfyui-sdk'interfaceCustomProcessorOutput{processed:booleanoriginalSize:numberprocessedSize:numbertimestamp:numberdurationMs:number}classCustomImageProcessorextendsArtifactProcessor{readonlyname='customImageProcessor'asyncshouldRun(artifact:Artifact):Promise<boolean>{returnartifact.kind==='binary'}asyncrun(artifact:Artifact):Promise<ProcessorOutput<CustomProcessorOutput>>{conststartTime=Date.now()constbuffer=artifact.payloadasBuffer// Placeholder for real image processingconstprocessedBuffer=awaitthis.processImage(buffer)return{output:{processed:true,originalSize:buffer.byteLength,processedSize:processedBuffer.byteLength,timestamp:Date.now(),durationMs:Date.now()-startTime},next:{kind:'binary',payload:processedBuffer,manifest:{ ...artifact.manifest,filename:`processed_${artifact.manifest.filename}`,contentType:'image/png'}}}}privateasyncprocessImage(buffer:Buffer):Promise<Buffer>{// Insert actual processing here (e.g., resizing, filtering)returnbuffer}}// To enable full type safety on pipeline state:declare module'comfyui-sdk/types'{interfacePipelineBus{customImageProcessor:CustomProcessorOutput}}// Usageconstpipeline=newArtifactPipeline([newCustomImageProcessor(),newCosUploader({/* ...config */})])
We welcome contributions! To get started:
- Fork the repository
- Install dependencies:
npm install - Lint & type-check:
npm run lint && npm run build - Run tests:
npm test - Submit a pull request with clear descriptions of the changes.
Please review ourCONTRIBUTING.md before opening issues or PRs.
This project is licensed under theMIT License. See theLICENSE file for full details.
- ComfyUI (Core):https://github.com/comfyanonymous/ComfyUI
- comfyui-sdk:https://github.com/zandko/comfyui-sdk
- Issue Tracker:https://github.com/zandko/comfyui-sdk/issues
- npm Package:https://www.npmjs.com/package/comfyui-sdk
Crafted with care by ZaneNode.js 18+ | TypeScript 4.5+
About
A battle-tested, TypeScript-first client for interacting with ComfyUI.
Topics
Resources
License
Contributing
Uh oh!
There was an error while loading.Please reload this page.