comfyui-node 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.OLD.md +1395 -0
- package/README.md +111 -1228
- package/dist/.tsbuildinfo +1 -1
- package/dist/call-wrapper.d.ts +124 -123
- package/dist/call-wrapper.d.ts.map +1 -1
- package/dist/call-wrapper.js +567 -555
- package/dist/call-wrapper.js.map +1 -1
- package/dist/client.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pool/WorkflowPool.d.ts +58 -2
- package/dist/pool/WorkflowPool.d.ts.map +1 -1
- package/dist/pool/WorkflowPool.js +455 -434
- package/dist/pool/WorkflowPool.js.map +1 -1
- package/dist/pool/client/ClientManager.d.ts +36 -1
- package/dist/pool/client/ClientManager.d.ts.map +1 -1
- package/dist/pool/client/ClientManager.js +64 -1
- package/dist/pool/client/ClientManager.js.map +1 -1
- package/dist/pool/index.d.ts +1 -0
- package/dist/pool/index.d.ts.map +1 -1
- package/dist/pool/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,1323 +6,206 @@
|
|
|
6
6
|

|
|
7
7
|

|
|
8
8
|
|
|
9
|
-
TypeScript SDK for interacting with the [ComfyUI](https://github.com/comfyanonymous/ComfyUI) API – focused on workflow construction, prompt execution orchestration, multi
|
|
10
|
-
|
|
11
|
-
> 1.0 is a complete redesign around modular feature namespaces (`api.ext.*`) and stronger typing. All legacy instance methods have been removed – see Migration section if upgrading.
|
|
12
|
-
|
|
13
|
-
## Contents
|
|
14
|
-
|
|
15
|
-
- [Features](#features)
|
|
16
|
-
- [Installation](#installation)
|
|
17
|
-
- [Cheat Sheet](#cheat-sheet)
|
|
18
|
-
- [Recent Enhancements (Ergonomics & Typing)](#recent-enhancements-ergonomics--typing)
|
|
19
|
-
- [High‑Level Workflow API (Experimental) – Quick Intro](#highlevel-workflow-api-experimental--quick-intro)
|
|
20
|
-
- [Choosing: Workflow vs PromptBuilder](#choosing-workflow-vs-promptbuilder)
|
|
21
|
-
- [Result Object Anatomy](#result-object-anatomy)
|
|
22
|
-
- [Multi-Instance Pool](#multi-instance-pool)
|
|
23
|
-
- [Authentication](#authentication)
|
|
24
|
-
- [Custom WebSocket](#custom-websocket)
|
|
25
|
-
- [Modular Features (`api.ext`)](#modular-features-apiext)
|
|
26
|
-
- [Events](#events)
|
|
27
|
-
- [Preview Metadata](#preview-metadata)
|
|
28
|
-
- [API Nodes (Comfy.org paid)](#api-nodes-comfyorg-paid)
|
|
29
|
-
- [Image Inputs: Attach Files (DX)](#image-inputs-attach-files-dx)
|
|
30
|
-
- [1.0 Migration](#10-migration)
|
|
31
|
-
- [Reference Overview](#reference-overview)
|
|
32
|
-
- [Examples](#examples)
|
|
33
|
-
- [Errors & Diagnostics](#errors--diagnostics)
|
|
34
|
-
- [Troubleshooting](#troubleshooting)
|
|
35
|
-
- [Published Smoke Test](#published-smoke-test)
|
|
36
|
-
- [Contributing](#contributing)
|
|
37
|
-
- [License](#license)
|
|
9
|
+
TypeScript SDK for interacting with the [ComfyUI](https://github.com/comfyanonymous/ComfyUI) API – focused on workflow construction, prompt execution orchestration, multi-instance scheduling and extension integration.
|
|
38
10
|
|
|
39
11
|
## Features
|
|
40
12
|
|
|
41
|
-
- Fully typed TypeScript surface
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
- WebSocket events
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
- JSON round‑trip support for builder state persistence
|
|
53
|
-
- High‑level `Workflow` abstraction (rapid parameter tweaking of existing JSON graphs)
|
|
54
|
-
- Input sugar helpers: `wf.input(...)`, `wf.batchInputs(...)`
|
|
55
|
-
- Soft autocomplete mode for sampler / scheduler (`Workflow.fromAugmented`)
|
|
56
|
-
- Progressive typed outputs inferred from `wf.output(...)` declarations
|
|
57
|
-
- Per‑node output shape heuristics (e.g. `SaveImage*`, `KSampler`)
|
|
58
|
-
- Automatic random seed substitution for `seed: -1` with `_autoSeeds` metadata
|
|
13
|
+
- Fully typed TypeScript surface with progressive output typing
|
|
14
|
+
- High-level `Workflow` API – tweak existing JSON workflows with minimal boilerplate
|
|
15
|
+
- Low-level `PromptBuilder` – programmatic graph construction with validation
|
|
16
|
+
- WebSocket events – progress, preview, output, completion with reconnection
|
|
17
|
+
- Multi-instance pooling – `WorkflowPool` with smart failover & health checks (v1.4.1+)
|
|
18
|
+
- Modular features – `api.ext.*` namespaces (queue, history, system, file, etc.)
|
|
19
|
+
- Authentication – basic, bearer token, custom headers
|
|
20
|
+
- Image attachments – upload files directly with workflow submissions
|
|
21
|
+
- Preview metadata – rich preview frames with metadata support
|
|
22
|
+
- Auto seed substitution – `seed: -1` randomized automatically
|
|
23
|
+
- API node support – compatible with custom/paid API nodes (Comfy.org)
|
|
59
24
|
|
|
60
25
|
## Installation
|
|
61
26
|
|
|
62
|
-
Requires Node.js >= 22
|
|
27
|
+
Requires Node.js >= 22. Works with Bun.
|
|
63
28
|
|
|
64
29
|
```bash
|
|
65
30
|
npm install comfyui-node
|
|
66
|
-
# or
|
|
67
|
-
pnpm add comfyui-node
|
|
68
|
-
# or
|
|
69
|
-
bun add comfyui-node
|
|
70
31
|
```
|
|
71
32
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
Minimal ESM usage example:
|
|
33
|
+
## Quick Start
|
|
75
34
|
|
|
76
35
|
```ts
|
|
77
36
|
import { ComfyApi, Workflow } from 'comfyui-node';
|
|
78
37
|
import BaseWorkflow from './example-txt2img-workflow.json';
|
|
79
38
|
|
|
80
|
-
async function main() {
|
|
81
|
-
const api = await new ComfyApi('http://127.0.0.1:8188').ready();
|
|
82
|
-
const wf = Workflow.from(BaseWorkflow)
|
|
83
|
-
.set('6.inputs.text', 'Hello ComfyUI SDK')
|
|
84
|
-
.output('images:9');
|
|
85
|
-
const job = await api.run(wf, { autoDestroy: true });
|
|
86
|
-
const result = await job.done();
|
|
87
|
-
for (const img of (result.images?.images || [])) {
|
|
88
|
-
console.log('image path:', api.ext.file.getPathImage(img));
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
main();
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
## Cheat Sheet
|
|
95
|
-
|
|
96
|
-
Fast reference for common operations. See deeper sections for narrative explanations.
|
|
97
|
-
|
|
98
|
-
### Workflow (High-Level)
|
|
99
|
-
|
|
100
|
-
```ts
|
|
101
|
-
import { ComfyApi, Workflow } from 'comfyui-node';
|
|
102
39
|
const api = await new ComfyApi('http://127.0.0.1:8188').ready();
|
|
103
|
-
const wf = Workflow.from(json)
|
|
104
|
-
.set('3.inputs.steps', 20) // dotted path set
|
|
105
|
-
.input('SAMPLER','cfg', 4) // input helper
|
|
106
|
-
.batchInputs('SAMPLER', { steps: 15, cfg: 3 })
|
|
107
|
-
.output('images:9'); // alias:nodeId
|
|
108
|
-
const job = await api.run(wf); // acceptance barrier
|
|
109
|
-
job.on('progress_pct', p => console.log(p,'%'));
|
|
110
|
-
const result = await job.done();
|
|
111
|
-
for (const img of (result.images?.images||[])) console.log(api.ext.file.getPathImage(img));
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
### PromptBuilder (Lower-Level)
|
|
115
|
-
|
|
116
|
-
```ts
|
|
117
|
-
import { PromptBuilder } from 'comfyui-node';
|
|
118
|
-
const builder = new PromptBuilder(base,[ 'positive','seed' ],[ 'images' ])
|
|
119
|
-
.setInputNode('positive','6.inputs.text')
|
|
120
|
-
.setInputNode('seed','3.inputs.seed')
|
|
121
|
-
.setOutputNode('images','9')
|
|
122
|
-
.input('positive','A misty forest')
|
|
123
|
-
.input('seed', 1234)
|
|
124
|
-
.validateOutputMappings();
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### Running (Alternate APIs)
|
|
128
|
-
|
|
129
|
-
```ts
|
|
130
|
-
await api.run(wf); // high-level (Workflow)
|
|
131
|
-
await api.runWorkflow(wf); // alias
|
|
132
|
-
new CallWrapper(api, builder)
|
|
133
|
-
.onFinished(o => console.log(o.images?.images?.length))
|
|
134
|
-
.run(); // builder execution
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
### Declaring Outputs
|
|
138
|
-
|
|
139
|
-
```ts
|
|
140
|
-
wf.output('alias:NodeId');
|
|
141
|
-
wf.output('alias','NodeId');
|
|
142
|
-
wf.output('NodeId'); // key = id
|
|
143
|
-
// none declared -> auto collect SaveImage nodes
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
### Events (WorkflowJob)
|
|
147
|
-
|
|
148
|
-
```txt
|
|
149
|
-
pending -> start -> progress / progress_pct / preview -> output* -> finished (or failed)
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
| Event | Notes |
|
|
153
|
-
| ----- | ----- |
|
|
154
|
-
| pending | accepted into queue |
|
|
155
|
-
| start | first execution step began |
|
|
156
|
-
| progress_pct | integer 0-100 (deduped) |
|
|
157
|
-
| preview | live frame (Blob) |
|
|
158
|
-
| output | a declared / auto-detected node produced data |
|
|
159
|
-
| finished | all requested nodes resolved |
|
|
160
|
-
| failed | execution error / interruption |
|
|
161
|
-
|
|
162
|
-
### Seed Handling
|
|
163
|
-
|
|
164
|
-
```ts
|
|
165
|
-
// -1 sentinel => randomized & reported under _autoSeeds
|
|
166
|
-
wf.batchInputs('SAMPLER', { seed: -1 });
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
### Type Extraction
|
|
170
|
-
|
|
171
|
-
```ts
|
|
172
|
-
type Result = ReturnType<typeof wf.typedResult>;
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
### Pool Quick Start
|
|
176
|
-
|
|
177
|
-
```ts
|
|
178
|
-
const pool = new ComfyPool([
|
|
179
|
-
new ComfyApi('http://localhost:8188'),
|
|
180
|
-
new ComfyApi('http://localhost:8189')
|
|
181
|
-
]);
|
|
182
|
-
const job2 = await pool.clients[0].run(wf, { pool });
|
|
183
|
-
await job2.done();
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
### Selecting Workflow vs PromptBuilder
|
|
187
|
-
|
|
188
|
-
Use Workflow for 90% of: tweak existing JSON, few parameter edits, rapid prototyping. Use PromptBuilder when you must programmatically assemble / rewire node graphs or need validation utilities pre-submit.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
### High‑Level Workflow API (Experimental) – Quick Intro
|
|
193
|
-
|
|
194
|
-
Skip manual `PromptBuilder` wiring with `Workflow` when you just want to tweak an existing graph JSON and run it. A full step‑by‑step tutorial is below; here is the 10‑second overview:
|
|
195
|
-
|
|
196
|
-
- Load JSON – `Workflow.from(json)`
|
|
197
|
-
- Mutate values – `.set('nodeId.inputs.field', value)`
|
|
198
|
-
- Declare outputs – `.output('alias:nodeId')` (or just `.output('nodeId')`; falls back to auto‑detecting `SaveImage` nodes)
|
|
199
|
-
- Execute – `await api.run(wf)` (or `api.runWorkflow(wf)` alias) returning a `WorkflowJob` (Promise‑like)
|
|
200
|
-
- Subscribe to events – `progress`, `progress_pct`, `preview`, `output`, `finished`, `failed`
|
|
201
|
-
- Await final object – either `await job` or `await job.done()`
|
|
202
|
-
|
|
203
|
-
See the dedicated tutorial section for a narrated example and option details.
|
|
204
|
-
|
|
205
|
-
### Recent Enhancements (Ergonomics & Typing)
|
|
206
|
-
|
|
207
|
-
The `Workflow` surface has gained several quality‑of‑life helpers and **progressive typing** features. All are additive (no breaking changes) and optional—fall back to raw `set()` / `output()` styles whenever you prefer.
|
|
208
|
-
|
|
209
|
-
| Feature | Purpose | Example | Type Effect |
|
|
210
|
-
| ------- | ------- | ------- | ----------- |
|
|
211
|
-
| `wf.input(nodeId, inputName, value)` | Concise single input mutation (vs dotted path) | `wf.input('SAMPLER','steps',30)` | none (runtime sugar) |
|
|
212
|
-
| `wf.batchInputs(nodeId, { ... })` | Set multiple inputs on one node | `wf.batchInputs('SAMPLER',{ steps:30, cfg:5 })` | none |
|
|
213
|
-
| `wf.batchInputs({ NODEA:{...} })` | Multi‑node batch mutation | `wf.batchInputs({ SAMPLER:{ cfg:6 } })` | none |
|
|
214
|
-
| `Workflow.fromAugmented(json)` | Soft autocomplete on sampler / scheduler but still accepts future values | `Workflow.fromAugmented(base)` | narrows fields to union \| (string & {}) |
|
|
215
|
-
| Typed output inference | `.output('alias:ID')` accumulates object keys | `wf.output('images:SAVE_IMAGE')` | widens result shape with `images` key |
|
|
216
|
-
| Per‑node output shape hints | Heuristic shapes for `SaveImage*`, `KSampler` | `result.images.images` | structural hints for nested fields |
|
|
217
|
-
| Multiple output syntaxes | Choose preferred style | `'alias:NodeId'` / `('alias','NodeId')` / `'NodeId'` | identical effect |
|
|
218
|
-
| `wf.typedResult()` | Get IDE type of final result | `type R = ReturnType<typeof wf.typedResult>` | captures accumulated generic |
|
|
219
|
-
| Auto seed substitution | `seed: -1` randomized before submit | `wf.input('SAMPLER','seed',-1)` | adds `_autoSeeds` map key |
|
|
220
|
-
| Acceptance barrier run | `await api.run(wf)` returns job handle pre-completion | `const job=await api.run(wf)` | result type unchanged |
|
|
221
|
-
|
|
222
|
-
> All typing is structural—no runtime validation. Unknown / future sampler names or new node classes continue to work.
|
|
223
|
-
|
|
224
|
-
#### Input Helpers
|
|
225
|
-
|
|
226
|
-
```ts
|
|
227
|
-
const wf = Workflow.fromAugmented(baseJson)
|
|
228
|
-
.input('LOADER','ckpt_name','model.safetensors')
|
|
229
|
-
.batchInputs('SAMPLER', {
|
|
230
|
-
steps: 30,
|
|
231
|
-
cfg: 4,
|
|
232
|
-
sampler_name: 'euler_ancestral', // autocomplete + accepts future strings
|
|
233
|
-
scheduler: 'karras', // autocomplete + forward compatible
|
|
234
|
-
seed: -1 // -1 -> auto randomized before submit
|
|
235
|
-
})
|
|
236
|
-
.batchInputs({
|
|
237
|
-
CLIP_TEXT_ENCODE_POSITIVE: { text: 'A moody cinematic landscape' },
|
|
238
|
-
LATENT_IMAGE: { width: 896, height: 1152 }
|
|
239
|
-
});
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
### Output Declaration & Typing
|
|
243
|
-
|
|
244
|
-
Each `output()` call accumulates inferred keys:
|
|
245
|
-
|
|
246
|
-
```ts
|
|
247
|
-
const wf2 = Workflow.fromAugmented(baseJson)
|
|
248
|
-
.output('gallery:SAVE_IMAGE') // key 'gallery'
|
|
249
|
-
.output('KSamplerNode') // key 'KSamplerNode'
|
|
250
|
-
.output('thumb','THUMBNAIL_NODE'); // key 'thumb'
|
|
251
|
-
|
|
252
|
-
// Type exploration (IDE only):
|
|
253
|
-
type Wf2Result = ReturnType<typeof wf2.typedResult>;
|
|
254
|
-
// Wf2Result ~ {
|
|
255
|
-
// gallery: { images?: any[] }; // SaveImage heuristic
|
|
256
|
-
// KSamplerNode: { samples?: any }; // KSampler heuristic
|
|
257
|
-
// thumb: any; // THUMBNAIL_NODE class not mapped yet
|
|
258
|
-
// _promptId?: string; _nodes?: string[]; _aliases?: Record<string,string>; _autoSeeds?: Record<string,number>;
|
|
259
|
-
// }
|
|
260
|
-
|
|
261
|
-
const job = await api.run(wf2);
|
|
262
|
-
const final = await job.done();
|
|
263
|
-
final.gallery.images?.forEach(img => console.log(api.ext.file.getPathImage(img)));
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
Supported output forms (all equivalent semantically; choose your style):
|
|
267
|
-
|
|
268
|
-
```ts
|
|
269
|
-
wf.output('alias:NodeId');
|
|
270
|
-
wf.output('alias','NodeId');
|
|
271
|
-
wf.output('NodeId'); // raw key = node id
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
If you declare *no* outputs the SDK still auto‑collects all `SaveImage` nodes.
|
|
275
|
-
|
|
276
|
-
#### Per‑Node Output Shapes (Heuristics)
|
|
277
|
-
|
|
278
|
-
Currently recognized:
|
|
279
|
-
|
|
280
|
-
| class_type match | Inferred shape fragment |
|
|
281
|
-
| ---------------- | ----------------------- |
|
|
282
|
-
| `SaveImage`, `SaveImageAdvanced` | `{ images?: any[] }` |
|
|
283
|
-
| `KSampler` | `{ samples?: any }` |
|
|
284
|
-
|
|
285
|
-
All others are typed as `any` (you still get alias key inference). This table will expand; explicit contributions welcome.
|
|
286
|
-
|
|
287
|
-
#### Combining With Result Metadata
|
|
288
|
-
|
|
289
|
-
The object from `job.done()` (and `runAndWait`) is always the intersection:
|
|
290
|
-
|
|
291
|
-
```ts
|
|
292
|
-
// final result shape (conceptual)
|
|
293
|
-
{ ...yourDeclaredOutputs, _promptId?: string, _nodes?: string[], _aliases?: Record<string,string>, _autoSeeds?: Record<string,number> }
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
#### When to Use `Workflow.fromAugmented`
|
|
297
|
-
|
|
298
|
-
Use it when you want IDE suggestions for sampler / scheduler *without* losing forward compatibility. The widened types are `TSamplerName | (string & {})` and `TSchedulerName | (string & {})` internally—any new upstream values are valid.
|
|
299
|
-
|
|
300
|
-
#### Extracting a Stable Result Type
|
|
301
|
-
|
|
302
|
-
If you want to export a type for downstream modules:
|
|
303
|
-
|
|
304
|
-
```ts
|
|
305
|
-
export type MyGenerationResult = ReturnType<typeof wf.typedResult>;
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
This stays accurate as long as all `output()` calls run before the type is captured.
|
|
309
|
-
|
|
310
|
-
#### Limitations & Future Work
|
|
311
|
-
|
|
312
|
-
- Output shapes are heuristic; not all node classes annotated yet.
|
|
313
|
-
- Dynamic node creation using non‑strict `input()` cannot update the generic shape (TypeScript limitation). You can re‑wrap with `Workflow.fromAugmented` after structural edits if needed.
|
|
314
|
-
- Potential future API: `wf.withOutputShapes({ MyCustomNode: { customField: string } })` for user overrides.
|
|
315
|
-
|
|
316
|
-
---
|
|
317
|
-
|
|
318
|
-
## Choosing: Workflow vs PromptBuilder
|
|
319
|
-
|
|
320
|
-
| Criterion | Prefer Workflow | Prefer PromptBuilder |
|
|
321
|
-
| --------- | --------------- | -------------------- |
|
|
322
|
-
| Starting point | You already have a working JSON graph | You need to assemble nodes programmatically |
|
|
323
|
-
| Change pattern | Tweak a handful of numeric/text inputs | Add/remove/re‑wire nodes dynamically |
|
|
324
|
-
| Output declaration | Simple image node aliases | Complex multi‑node mapping / conditional outputs |
|
|
325
|
-
| Validation needs | Light (auto collect `SaveImage`) | Strong: explicit mapping + cycle checks |
|
|
326
|
-
| Type ergonomics | Progressive result typing via `.output()` + heuristics | Fully explicit generic parameters on construction |
|
|
327
|
-
| Autocomplete | Sampler / scheduler (augmented mode) | Input & output alias keys / builder fluency |
|
|
328
|
-
| Serialization | Not needed / reuse same base JSON | Need to persist & replay builder state |
|
|
329
|
-
| Scheduling | Direct `api.run(wf)` | Usually wrapped in `CallWrapper` (or converted later) |
|
|
330
|
-
| Learning curve | Minimal (few fluent methods) | Slightly higher (need to map inputs / outputs) |
|
|
331
|
-
| Migration path | Can drop down later to builder if requirements grow | Can export to JSON & wrap with `Workflow.from(...)` for simpler tweaking |
|
|
332
|
-
|
|
333
|
-
Rule of thumb: start with `Workflow`. Move to `PromptBuilder` when you feel friction needing structural graph edits or stronger pre‑submit validation.
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
Pool variant (experimental):
|
|
337
|
-
|
|
338
|
-
```ts
|
|
339
|
-
import { ComfyApi, ComfyPool, Workflow } from 'comfyui-node';
|
|
340
|
-
import BaseWorkflow from './example-txt2img-workflow.json';
|
|
341
|
-
|
|
342
|
-
const pool = new ComfyPool([
|
|
343
|
-
new ComfyApi('http://localhost:8188'),
|
|
344
|
-
new ComfyApi('http://localhost:8189')
|
|
345
|
-
]);
|
|
346
40
|
|
|
347
41
|
const wf = Workflow.from(BaseWorkflow)
|
|
348
|
-
.set('6.inputs.text', 'A
|
|
349
|
-
.output('9');
|
|
350
|
-
|
|
351
|
-
// Run using one specific API (pool provided for scheduling context)
|
|
352
|
-
const api2 = pool.clients[0];
|
|
353
|
-
const job2 = await api2.run(wf, { pool });
|
|
354
|
-
await job2.done();
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
Notes:
|
|
358
|
-
|
|
359
|
-
- Experimental surface: event names / helpers may refine before a stable minor release.
|
|
42
|
+
.set('6.inputs.text', 'A dramatic cinematic landscape')
|
|
43
|
+
.output('images:9');
|
|
360
44
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
`PromptBuilder` now includes optional robustness helpers you can invoke before submission:
|
|
364
|
-
|
|
365
|
-
```ts
|
|
366
|
-
builder
|
|
367
|
-
.validateOutputMappings() // Ensures every declared output key maps to an existing node id
|
|
368
|
-
.validateNoImmediateCycles(); // Guards against a node directly referencing itself in its input tuple
|
|
369
|
-
|
|
370
|
-
// Serialize to persist / send over IPC
|
|
371
|
-
const saved = builder.toJSON();
|
|
372
|
-
// Later restore (types must line up with original generic parameters when casting)
|
|
373
|
-
const restored = PromptBuilder.fromJSON(saved);
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
If validation fails an `Error` is thrown with a concise list of offending mappings, e.g.
|
|
45
|
+
const job = await api.run(wf, { autoDestroy: true });
|
|
46
|
+
job.on('progress_pct', p => console.log(`${p}%`));
|
|
377
47
|
|
|
378
|
-
|
|
379
|
-
|
|
48
|
+
const result = await job.done();
|
|
49
|
+
for (const img of (result.images?.images || [])) {
|
|
50
|
+
console.log(api.ext.file.getPathImage(img));
|
|
51
|
+
}
|
|
380
52
|
```
|
|
381
53
|
|
|
382
|
-
|
|
54
|
+
## Documentation
|
|
383
55
|
|
|
384
|
-
###
|
|
56
|
+
### Getting Started
|
|
385
57
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
.setInputNode("positive","6.inputs.text")
|
|
390
|
-
.setInputNode("seed","3.inputs.seed")
|
|
391
|
-
.setOutputNode("images","9")
|
|
392
|
-
.input("positive","Hello world")
|
|
393
|
-
.input("seed", seed())
|
|
394
|
-
.validateOutputMappings()
|
|
395
|
-
.validateNoImmediateCycles();
|
|
396
|
-
}
|
|
397
|
-
```
|
|
58
|
+
- **[Getting Started Guide](./docs/getting-started.md)** – Installation, quick start, core concepts, cheat sheet
|
|
59
|
+
- **[Workflow Guide](./docs/workflow-guide.md)** – Complete high-level Workflow API tutorial with progressive typing
|
|
60
|
+
- **[PromptBuilder Guide](./docs/prompt-builder.md)** – Lower-level graph construction, validation, serialization
|
|
398
61
|
|
|
399
|
-
|
|
62
|
+
### Multi-Instance Pooling
|
|
400
63
|
|
|
401
|
-
|
|
64
|
+
- **[WorkflowPool Documentation](./docs/workflow-pool.md)** – Production-ready pooling with health checks (v1.4.1+)
|
|
65
|
+
- **[Connection Stability Guide](./docs/websocket-idle-issue.md)** – WebSocket health check implementation details
|
|
402
66
|
|
|
403
|
-
|
|
404
|
-
2. Heuristic shape hints (currently only augmenting `SaveImage*` & `KSampler` nodes with friendly nested fields)
|
|
405
|
-
3. Metadata fields: `_promptId`, `_nodes`, `_aliases`, `_autoSeeds`
|
|
67
|
+
### Advanced Features
|
|
406
68
|
|
|
407
|
-
|
|
69
|
+
- **[Advanced Usage](./docs/advanced-usage.md)** – Authentication, events, preview metadata, API nodes, image attachments
|
|
70
|
+
- **[API Features](./docs/api-features.md)** – Modular `api.ext.*` namespaces (queue, file, system, etc.)
|
|
408
71
|
|
|
409
|
-
|
|
410
|
-
type WorkflowResult = {
|
|
411
|
-
// Your keys:
|
|
412
|
-
[aliasOrNodeId: string]: any; // each node's output blob (heuristically narrowed)
|
|
413
|
-
// Metadata:
|
|
414
|
-
_promptId?: string;
|
|
415
|
-
_nodes?: string[]; // collected node ids
|
|
416
|
-
_aliases?: Record<string,string>; // nodeId -> alias
|
|
417
|
-
_autoSeeds?: Record<string,number>; // nodeId -> randomized seed (when -1 sentinel used)
|
|
418
|
-
};
|
|
419
|
-
```
|
|
72
|
+
### Help & Migration
|
|
420
73
|
|
|
421
|
-
|
|
74
|
+
- **[Troubleshooting](./docs/troubleshooting.md)** – Common issues, error types, testing, diagnostics
|
|
75
|
+
- **[Migration Guide](./docs/migration-guide.md)** – Upgrading from <1.0 to 1.0+ with complete API mappings
|
|
422
76
|
|
|
423
|
-
|
|
424
|
-
const wf = Workflow.fromAugmented(json)
|
|
425
|
-
.output('gallery:SAVE_IMAGE')
|
|
426
|
-
.output('sampler:KSampler');
|
|
427
|
-
type R = ReturnType<typeof wf.typedResult>; // => { gallery: { images?: any[] }; sampler: { samples?: any }; _promptId?: ... }
|
|
428
|
-
```
|
|
77
|
+
## Key Concepts
|
|
429
78
|
|
|
430
|
-
|
|
79
|
+
### Workflow vs PromptBuilder
|
|
431
80
|
|
|
432
|
-
|
|
81
|
+
**Use `Workflow`** for tweaking existing JSON workflows:
|
|
433
82
|
|
|
434
83
|
```ts
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
for (const img of (res.gallery?.images || [])) {
|
|
440
|
-
console.log(api.ext.file.getPathImage(img));
|
|
441
|
-
}
|
|
84
|
+
const wf = Workflow.from(baseJson)
|
|
85
|
+
.set('3.inputs.steps', 20)
|
|
86
|
+
.input('SAMPLER', 'cfg', 4)
|
|
87
|
+
.output('images:9');
|
|
442
88
|
```
|
|
443
89
|
|
|
444
|
-
|
|
90
|
+
**Use `PromptBuilder`** for programmatic graph construction:
|
|
445
91
|
|
|
446
92
|
```ts
|
|
447
|
-
|
|
93
|
+
const builder = new PromptBuilder(base, ['positive', 'seed'], ['images'])
|
|
94
|
+
.setInputNode('positive', '6.inputs.text')
|
|
95
|
+
.validateOutputMappings();
|
|
448
96
|
```
|
|
449
97
|
|
|
450
|
-
|
|
98
|
+
See [comparison guide](./docs/workflow-guide.md#choosing-workflow-vs-promptbuilder) for details.
|
|
451
99
|
|
|
100
|
+
### WorkflowPool (v1.4.1+)
|
|
452
101
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
The SDK ships two pooling layers:
|
|
456
|
-
|
|
457
|
-
- **`WorkflowPool` (new, recommended)** – Manages its own queue (pluggable adapters), emits per-job events with consistent job ids, and handles smart failover / retry without depending on the ComfyUI server queue depth. Ideal for multi-tenant services or when integrating with Redis/BullMQ/RabbitMQ backends.
|
|
458
|
-
- **`ComfyPool` (legacy)** – Weighted, in-memory scheduler that delegates most coordination to the ComfyUI queue. Useful for lightweight scripts or when you need backwards compatibility with earlier SDK versions.
|
|
459
|
-
|
|
460
|
-
### WorkflowPool Snapshot
|
|
102
|
+
Production-ready multi-instance scheduling with automatic health checks:
|
|
461
103
|
|
|
462
104
|
```ts
|
|
463
|
-
import {
|
|
464
|
-
import WorkflowJson from "./example-txt2img-workflow.json";
|
|
105
|
+
import { WorkflowPool, MemoryQueueAdapter } from "comfyui-node";
|
|
465
106
|
|
|
466
|
-
const
|
|
107
|
+
const pool = new WorkflowPool([
|
|
467
108
|
new ComfyApi("http://localhost:8188"),
|
|
468
109
|
new ComfyApi("http://localhost:8189")
|
|
469
|
-
]
|
|
470
|
-
|
|
471
|
-
const pool = new WorkflowPool(clients, {
|
|
472
|
-
queueAdapter: new MemoryQueueAdapter()
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
pool.on("job:progress", (ev) => {
|
|
476
|
-
console.log(`job ${ev.detail.jobId} -> ${ev.detail.progress.value}/${ev.detail.progress.max}`);
|
|
110
|
+
], {
|
|
111
|
+
healthCheckIntervalMs: 30000 // keeps connections alive
|
|
477
112
|
});
|
|
478
113
|
|
|
479
|
-
pool.on("
|
|
480
|
-
console.warn(`client ${ev.detail.clientId} cooling off for workflow ${ev.detail.workflowHash.slice(0, 8)}`);
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
const jobId = await pool.enqueue(WorkflowJson, {
|
|
484
|
-
metadata: { tenant: "alpha" },
|
|
485
|
-
includeOutputs: ["9"],
|
|
486
|
-
priority: 10
|
|
487
|
-
});
|
|
114
|
+
pool.on("job:progress", ev => console.log(ev.detail.jobId, ev.detail.progress));
|
|
488
115
|
|
|
489
|
-
|
|
116
|
+
const jobId = await pool.enqueue(workflow, { priority: 10 });
|
|
490
117
|
```
|
|
491
118
|
|
|
492
|
-
See
|
|
493
|
-
|
|
494
|
-
### Legacy Pool (`ComfyPool`)
|
|
495
|
-
|
|
496
|
-
The legacy `ComfyPool` is a simpler, in-memory scheduler that relies on the server's queue depth for load balancing.
|
|
497
|
-
|
|
498
|
-
#### ComfyPool Modes
|
|
499
|
-
|
|
500
|
-
| Mode | Enum | Behavior | When to use |
|
|
501
|
-
| ---- | ---- | -------- | ----------- |
|
|
502
|
-
| Pick zero queue | `EQueueMode.PICK_ZERO` (default) | Choose any online client whose reported `queue_remaining` is 0 (prefers idle machines). Locks a client until it emits an execution event. | Co-existence with the ComfyUI web UI where queue spikes are common. |
|
|
503
|
-
| Lowest queue | `EQueueMode.PICK_LOWEST` | Choose the online client with the smallest `queue_remaining` (may still be busy). | High throughput batch ingestion; keeps all nodes saturated. |
|
|
504
|
-
| Round-robin | `EQueueMode.PICK_ROUTINE` | Simple rotation through available online clients irrespective of queue depth. | Latency balancing; predictable distribution. |
|
|
505
|
-
|
|
506
|
-
---
|
|
507
|
-
|
|
508
|
-
## High‑Level Workflow Tutorial (New Users of This SDK)
|
|
509
|
-
|
|
510
|
-
Audience: You already understand ComfyUI graphs & node JSON, but are new to this TypeScript SDK.
|
|
511
|
-
|
|
512
|
-
Goals after this section you can: (a) clone a base workflow, (b) modify its parameters, (c) name your desired outputs, (d) track progress & previews, and (e) retrieve final image paths – with minimal boilerplate.
|
|
513
|
-
|
|
514
|
-
### 1. Prepare a Base Workflow JSON
|
|
515
|
-
|
|
516
|
-
Export or copy a working ComfyUI txt2img graph (e.g. the one in `test/example-txt2img-workflow.json`). Ensure you know the node ID of the final `SaveImage` (here we assume `9`).
|
|
119
|
+
**New in v1.4.1:** Automatic health checks prevent idle connection timeouts. See [WorkflowPool docs](./docs/workflow-pool.md).
|
|
517
120
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
`api.ready()` handles connection & feature probing. It is idempotent (can be safely called multiple times). You can override the host using `COMFY_HOST`.
|
|
521
|
-
|
|
522
|
-
### 3. Mutate Parameters & Declare Outputs
|
|
523
|
-
|
|
524
|
-
Use `.set('<nodeId>.inputs.<field>', value)` to change values. Call `.output('alias:nodeId')` to collect that node's result under a friendly key (`alias`). If you omit alias (`.output('9')`) the key will be the node ID. If you omit all outputs the SDK tries to collect every `SaveImage` node automatically.
|
|
525
|
-
|
|
526
|
-
Auto seed: If any node has an input field literally named `seed` with value `-1`, the SDK will replace it with a random 32‑bit integer before submission and expose the mapping in the final result under `_autoSeeds` (object keyed by node id). This lets you keep templates with `-1` sentinel for “random every run”.
|
|
527
|
-
|
|
528
|
-
### 4. Run & Observe Progress
|
|
529
|
-
|
|
530
|
-
`api.run(workflow, { autoDestroy: true })` executes and (optionally) closes underlying sockets once finished/failed so the process can exit without manual cleanup. The returned `WorkflowJob` is an EventEmitter‑like object AND a Promise: `await job` works just like `await job.done()`.
|
|
531
|
-
|
|
532
|
-
### 5. Extract Image Paths
|
|
533
|
-
|
|
534
|
-
Final structure includes your alias keys plus `_promptId`, `_nodes` and `_aliases` metadata. Use `api.ext.file.getPathImage(imageInfo)` to build a fetchable URL.
|
|
535
|
-
|
|
536
|
-
### Complete Example
|
|
537
|
-
|
|
538
|
-
```ts
|
|
539
|
-
import { ComfyApi, Workflow } from 'comfyui-node';
|
|
540
|
-
import BaseWorkflow from './example-txt2img-workflow.json';
|
|
121
|
+
## What's New in v1.4.1
|
|
541
122
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
.set('4.inputs.ckpt_name', process.env.COMFY_MODEL || 'SDXL/realvisxlV40_v40LightningBakedvae.safetensors')
|
|
547
|
-
.set('6.inputs.text', 'A dramatic cinematic landscape, volumetric light')
|
|
548
|
-
.set('7.inputs.text', 'text, watermark')
|
|
549
|
-
.set('3.inputs.seed', Math.floor(Math.random() * 10_000_000))
|
|
550
|
-
.set('3.inputs.steps', 8)
|
|
551
|
-
.set('3.inputs.cfg', 2)
|
|
552
|
-
.set('3.inputs.sampler_name', 'dpmpp_sde')
|
|
553
|
-
.set('3.inputs.scheduler', 'sgm_uniform')
|
|
554
|
-
.set('5.inputs.width', 1024)
|
|
555
|
-
.set('5.inputs.height', 1024)
|
|
556
|
-
.output('images:9'); // alias 'images' -> node 9
|
|
557
|
-
|
|
558
|
-
const job = await api.runWorkflow(wf, { autoDestroy: true });
|
|
559
|
-
|
|
560
|
-
job
|
|
561
|
-
.on('pending', id => console.log('[queue]', id))
|
|
562
|
-
.on('start', id => console.log('[start]', id))
|
|
563
|
-
.on('progress_pct', pct => process.stdout.write(`\rprogress ${pct}% `))
|
|
564
|
-
.on('preview', blob => console.log('\npreview frame bytes=', blob.size))
|
|
565
|
-
.on('failed', err => console.error('\nerror', err));
|
|
566
|
-
|
|
567
|
-
const result = await job; // or await job.done();
|
|
568
|
-
console.log('\nPrompt ID:', result._promptId);
|
|
569
|
-
for (const img of (result.images?.images || [])) {
|
|
570
|
-
console.log('image path:', api.ext.file.getPathImage(img));
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
main().catch(e => { console.error(e); process.exit(1); });
|
|
575
|
-
```
|
|
576
|
-
|
|
577
|
-
### Key Options Recap
|
|
578
|
-
|
|
579
|
-
| Option | Where | Purpose |
|
|
580
|
-
| ------ | ----- | ------- |
|
|
581
|
-
| `autoDestroy` | `api.run(...)` | Automatically `destroy()` the client on finish/fail |
|
|
582
|
-
| `includeOutputs` | `api.run(wf,{ includeOutputs:['9'] })` | Force extra node IDs (in addition to `.output(...)`) |
|
|
583
|
-
| `pool` | (advanced) | Execute through a `ComfyPool` for multi‑instance scheduling |
|
|
584
|
-
|
|
585
|
-
### Event Cheat Sheet (WorkflowJob)
|
|
586
|
-
|
|
587
|
-
| Event | Payload | Description |
|
|
588
|
-
| ----- | ------- | ----------- |
|
|
589
|
-
| `pending` | promptId | Enqueued, waiting to start |
|
|
590
|
-
| `start` | promptId | Execution began |
|
|
591
|
-
| `progress` | raw `{ value,max }` | Low‑level progress data |
|
|
592
|
-
| `progress_pct` | number (0‑100) | Deduped integer percentage (fires on change) |
|
|
593
|
-
| `preview` | `Blob` | Live image preview frame |
|
|
594
|
-
| `output` | nodeId | Partial node output arrived |
|
|
595
|
-
| `finished` | final object | All requested outputs resolved |
|
|
596
|
-
| `failed` | `Error` | Execution failed / interrupted |
|
|
597
|
-
|
|
598
|
-
### Execution Flow & Await Semantics
|
|
599
|
-
|
|
600
|
-
`await api.run(wf)` resolves AFTER the job has been accepted (queued) and returns a `WorkflowJob` handle you can attach events to. You then explicitly `await job.done()` for final outputs.
|
|
601
|
-
|
|
602
|
-
```ts
|
|
603
|
-
const job = await api.run(wf); // acceptance barrier reached -> you have prompt id via 'pending' event
|
|
604
|
-
job
|
|
605
|
-
.on('progress_pct', pct => console.log('progress', pct))
|
|
606
|
-
.on('preview', blob => console.log('preview frame', blob.size));
|
|
607
|
-
|
|
608
|
-
const outputs = await job.done(); // final mapped outputs + metadata
|
|
609
|
-
```
|
|
610
|
-
|
|
611
|
-
This two‑stage await keeps early feedback (events available immediately after acceptance) while still letting you write linear code for final result consumption.
|
|
612
|
-
|
|
613
|
-
Auto‑generated metadata keys:
|
|
614
|
-
|
|
615
|
-
| Key | Meaning |
|
|
616
|
-
| --- | ------- |
|
|
617
|
-
| `_promptId` | Server prompt id assigned |
|
|
618
|
-
| `_nodes` | Array of collected node ids |
|
|
619
|
-
| `_aliases` | Mapping nodeId -> alias (where provided) |
|
|
620
|
-
| `_autoSeeds` | Mapping nodeId -> randomized seed (only when you used -1 sentinel) |
|
|
621
|
-
|
|
622
|
-
---
|
|
623
|
-
|
|
624
|
-
### Job Weighting
|
|
625
|
-
|
|
626
|
-
Jobs are inserted into an internal priority queue ordered by ascending weight. Lower weight runs earlier. By default the weight is set to the queue length at insertion (FIFO). You can override:
|
|
627
|
-
|
|
628
|
-
```ts
|
|
629
|
-
await Promise.all([
|
|
630
|
-
pool.run(doSomethingHeavy, 10), // runs later
|
|
631
|
-
pool.run(doSomethingQuick, 1), // runs first
|
|
632
|
-
pool.run(anotherTask, 5)
|
|
633
|
-
]);
|
|
634
|
-
```
|
|
635
|
-
|
|
636
|
-
### Include / Exclude Filters
|
|
637
|
-
|
|
638
|
-
Target or avoid specific client IDs:
|
|
639
|
-
|
|
640
|
-
```ts
|
|
641
|
-
await pool.run(taskA, undefined, { includeIds: ["gpu-a"] }); // only gpu-a
|
|
642
|
-
await pool.run(taskB, undefined, { excludeIds: ["gpu-b"] }); // any except gpu-b
|
|
643
|
-
```
|
|
644
|
-
|
|
645
|
-
### Failover & Retries
|
|
646
|
-
|
|
647
|
-
`run()` attempts transparent failover when a job throws. It excludes the failing client and retries another (up to `maxRetries`).
|
|
648
|
-
|
|
649
|
-
```ts
|
|
650
|
-
await pool.run(doGenerate, undefined, undefined, { maxRetries: 3, retryDelay: 1500 });
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
Disable failover:
|
|
654
|
-
|
|
655
|
-
```ts
|
|
656
|
-
await pool.run(doGenerate, undefined, undefined, { enableFailover: false });
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
### Pool Events
|
|
660
|
-
|
|
661
|
-
`ComfyPool` is an `EventTarget` emitting high‑level orchestration signals:
|
|
662
|
-
|
|
663
|
-
| Event | Detail Payload | When |
|
|
664
|
-
| ----- | -------------- | ---- |
|
|
665
|
-
| `init` | – | All clients added & initial processing pass done |
|
|
666
|
-
| `added` / `removed` | `{ client, clientIdx }` | Client lifecycle changes |
|
|
667
|
-
| `ready` | `{ client, clientIdx }` | Individual client fully initialized |
|
|
668
|
-
| `executing` / `executed` | `{ client, clientIdx }` | A job starts / finishes on a client |
|
|
669
|
-
| `execution_error` | `{ client, clientIdx, error, willRetry, attempt, maxRetries }` | A job threw; may retry |
|
|
670
|
-
| `execution_interrupted` | `{ client, clientIdx }` | Underlying API emitted interruption |
|
|
671
|
-
| `connected` / `disconnected` / `reconnected` | `{ client, clientIdx }` | WebSocket state relayed from `ComfyApi` |
|
|
672
|
-
| `terminal` | `{ clientIdx, line }` | Terminal log pass‑through |
|
|
673
|
-
| `system_monitor` | `{ clientIdx, data }` | Crystools monitor snapshot (when supported) |
|
|
674
|
-
| `add_job` | `{ jobIdx, weight }` | Job inserted into internal queue |
|
|
675
|
-
| `change_mode` | `{ mode }` | Queue selection mode altered |
|
|
676
|
-
| `have_job` | `{ client, remain }` | A client reports pending queue > 0 |
|
|
677
|
-
| `idle` | `{ client }` | A previously busy client reports queue 0 |
|
|
678
|
-
|
|
679
|
-
### Cleaning Up
|
|
680
|
-
|
|
681
|
-
Always invoke `destroy()` when finished to clear intervals, event listeners & underlying client connections:
|
|
682
|
-
|
|
683
|
-
```ts
|
|
684
|
-
pool.destroy();
|
|
685
|
-
```
|
|
686
|
-
|
|
687
|
-
### Combined Orchestration Example (Auth + Pool + Validation + Retry)
|
|
688
|
-
|
|
689
|
-
```ts
|
|
690
|
-
import { ComfyApi, ComfyPool, EQueueMode, PromptBuilder, CallWrapper, seed } from "comfyui-node";
|
|
123
|
+
- **Idle connection stability** – Automatic health checks keep WebSocket connections alive
|
|
124
|
+
- **Increased default timeout** – WebSocket inactivity timeout raised from 10s to 60s
|
|
125
|
+
- **Configurable health checks** – `healthCheckIntervalMs` option (default 30s, set to 0 to disable)
|
|
126
|
+
- **Better DX** – Comprehensive JSDoc comments and exported types for all pool options
|
|
691
127
|
|
|
692
|
-
|
|
693
|
-
new ComfyApi(process.env.C1!,"c1", { credentials: { type: "bearer_token", token: process.env.C1_TOKEN! } }),
|
|
694
|
-
new ComfyApi(process.env.C2!,"c2")
|
|
695
|
-
], { mode: EQueueMode.PICK_LOWEST });
|
|
128
|
+
See [CHANGELOG.md](./CHANGELOG.md) for complete release notes.
|
|
696
129
|
|
|
697
|
-
|
|
698
|
-
const wf = /* load / clone a base workflow JSON */ {} as any;
|
|
699
|
-
const builder = new PromptBuilder(wf,["positive","seed"],["images"])
|
|
700
|
-
.setInputNode("positive","6.inputs.text")
|
|
701
|
-
.setInputNode("seed","3.inputs.seed")
|
|
702
|
-
.setOutputNode("images","9")
|
|
703
|
-
.input("positive", text)
|
|
704
|
-
.input("seed", seed())
|
|
705
|
-
.validateOutputMappings();
|
|
706
|
-
|
|
707
|
-
return await new Promise<string[]>((resolve, reject) => {
|
|
708
|
-
new CallWrapper(api, builder)
|
|
709
|
-
.onFinished(d => resolve((d.images?.images||[]).map((img:any)=> api.ext.file.getPathImage(img))))
|
|
710
|
-
.onFailed(err => reject(err))
|
|
711
|
-
.run();
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// Weighted submission with retry semantics
|
|
716
|
-
const tasks = ["cat portrait","cyberpunk city","forest at dawn"].map(txt => (api: ComfyApi) => generate(api, txt));
|
|
717
|
-
const results = await Promise.all(tasks.map((fn,i)=> pool.run(fn, i))); // lower weight = earlier
|
|
718
|
-
console.log(results.flat());
|
|
719
|
-
pool.destroy();
|
|
720
|
-
```
|
|
721
|
-
|
|
722
|
-
### Choosing a Mode
|
|
723
|
-
|
|
724
|
-
| Goal | Suggested Mode |
|
|
725
|
-
| ---- | -------------- |
|
|
726
|
-
| Minimize latency spikes | `PICK_ZERO` |
|
|
727
|
-
| Maximize throughput | `PICK_LOWEST` |
|
|
728
|
-
| Deterministic striping | `PICK_ROUTINE` |
|
|
729
|
-
|
|
730
|
-
You can change dynamically:
|
|
731
|
-
|
|
732
|
-
```ts
|
|
733
|
-
pool.changeMode(EQueueMode.PICK_LOWEST);
|
|
734
|
-
```
|
|
735
|
-
|
|
736
|
-
### Observability Tips
|
|
737
|
-
|
|
738
|
-
Listen for `execution_error` with `willRetry=true` to surface transient node failures; attach Prometheus / metrics counters externally from these events if desired.
|
|
739
|
-
|
|
740
|
-
### Relation to `CallWrapper`
|
|
130
|
+
## Examples
|
|
741
131
|
|
|
742
|
-
|
|
132
|
+
Check the `scripts/` directory for comprehensive examples:
|
|
743
133
|
|
|
744
|
-
|
|
134
|
+
- **Basic workflows:** `workflow-tutorial-basic.ts`, `test-simple-txt2img.ts`
|
|
135
|
+
- **Image editing:** `qwen-image-edit-demo.ts`, `qwen-image-edit-queue.ts`
|
|
136
|
+
- **Pooling:** `workflow-pool-demo.ts`, `workflow-pool-debug.ts`
|
|
137
|
+
- **Node bypass:** `demo-node-bypass.ts`, `demo-workflow-bypass.ts`
|
|
138
|
+
- **API nodes:** `api-node-image-edit.ts` (Comfy.org paid nodes)
|
|
139
|
+
- **Image loading:** `image-loading-demo.ts`
|
|
745
140
|
|
|
746
|
-
-
|
|
747
|
-
- Adaptive weight assignment based on rolling execution duration
|
|
748
|
-
- Pluggable selection strategies via user callback
|
|
141
|
+
Live demo: `demos/recursive-edit/` – recursive image editing server + web client.
|
|
749
142
|
|
|
750
|
-
|
|
143
|
+
## API Reference
|
|
751
144
|
|
|
752
|
-
|
|
145
|
+
### ComfyApi Client
|
|
753
146
|
|
|
754
147
|
```ts
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
## Custom WebSocket
|
|
763
|
-
|
|
764
|
-
```ts
|
|
765
|
-
import { ComfyApi, WebSocketInterface } from "comfyui-node";
|
|
766
|
-
import CustomWebSocket from "your-custom-ws";
|
|
148
|
+
const api = new ComfyApi('http://127.0.0.1:8188', 'optional-id', {
|
|
149
|
+
credentials: { type: 'basic', username: 'user', password: 'pass' },
|
|
150
|
+
wsTimeout: 60000,
|
|
151
|
+
comfyOrgApiKey: process.env.COMFY_ORG_API_KEY,
|
|
152
|
+
debug: true
|
|
153
|
+
});
|
|
767
154
|
|
|
768
|
-
|
|
155
|
+
await api.ready(); // Connection + feature probing
|
|
769
156
|
```
|
|
770
157
|
|
|
771
|
-
|
|
158
|
+
### Modular Features (`api.ext`)
|
|
772
159
|
|
|
773
160
|
```ts
|
|
774
|
-
await api.waitForReady();
|
|
775
161
|
await api.ext.queue.queuePrompt(null, workflow);
|
|
162
|
+
await api.ext.queue.interrupt();
|
|
776
163
|
const stats = await api.ext.system.getSystemStats();
|
|
777
164
|
const checkpoints = await api.ext.node.getCheckpoints();
|
|
778
|
-
|
|
779
|
-
const
|
|
780
|
-
```
|
|
781
|
-
|
|
782
|
-
| Namespace | Responsibility |
|
|
783
|
-
| --------- | -------------- |
|
|
784
|
-
| `queue` | Prompt submission, append & interrupt |
|
|
785
|
-
| `history` | Execution history retrieval |
|
|
786
|
-
| `system` | System stats & memory free |
|
|
787
|
-
| `node` | Node defs + sampler / checkpoint / lora helpers |
|
|
788
|
-
| `user` | User & settings CRUD |
|
|
789
|
-
| `file` | Uploads, image helpers, user data file ops |
|
|
790
|
-
| `model` | Experimental model browsing & previews |
|
|
791
|
-
| `terminal` | Terminal logs & subscription toggle |
|
|
792
|
-
| `misc` | Extensions list, embeddings (new + fallback) |
|
|
793
|
-
| `manager` | ComfyUI Manager extension integration |
|
|
794
|
-
| `monitor` | Crystools monitor events & snapshot |
|
|
795
|
-
| `featureFlags` | Server capabilities (`/features`) |
|
|
796
|
-
|
|
797
|
-
## Events
|
|
798
|
-
|
|
799
|
-
Both `ComfyApi` and `ComfyPool` expose strongly typed event maps. Import the key unions or event maps for generic helpers:
|
|
800
|
-
|
|
801
|
-
```ts
|
|
802
|
-
import { ComfyApi, ComfyApiEventKey, TComfyAPIEventMap } from 'comfyui-node';
|
|
803
|
-
|
|
804
|
-
const api = new ComfyApi('http://localhost:8188');
|
|
805
|
-
api.on('progress', (ev) => {
|
|
806
|
-
console.log(ev.detail.value, '/', ev.detail.max);
|
|
807
|
-
});
|
|
808
|
-
|
|
809
|
-
function handleApiEvent<K extends ComfyApiEventKey>(k: K, e: TComfyAPIEventMap[K]) {
|
|
810
|
-
if (k === 'executed') {
|
|
811
|
-
console.log('Node executed:', e.detail.node);
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
```
|
|
815
|
-
|
|
816
|
-
Pool usage:
|
|
817
|
-
|
|
818
|
-
```ts
|
|
819
|
-
import { ComfyPool, ComfyPoolEventKey } from 'comfyui-node';
|
|
820
|
-
|
|
821
|
-
pool.on('execution_error', (ev) => {
|
|
822
|
-
if (ev.detail.willRetry) console.warn('Transient failure, retrying...');
|
|
823
|
-
});
|
|
824
|
-
```
|
|
825
|
-
|
|
826
|
-
---
|
|
827
|
-
|
|
828
|
-
## Preview Metadata
|
|
829
|
-
|
|
830
|
-
When the server advertises the `supports_preview_metadata` feature flag, binary preview frames are sent using a richer protocol (`PREVIEW_IMAGE_WITH_METADATA`). The SDK decodes these frames and exposes both legacy and richer events.
|
|
831
|
-
|
|
832
|
-
What you get:
|
|
833
|
-
|
|
834
|
-
- Low-level API events on `ComfyApi`:
|
|
835
|
-
- `b_preview` – existing event with `Blob` image only (kept for backward compatibility)
|
|
836
|
-
- `b_preview_meta` – new event with `{ blob: Blob; metadata: any }`
|
|
837
|
-
|
|
838
|
-
- High-level `WorkflowJob` events:
|
|
839
|
-
- `preview` – existing event with `Blob`
|
|
840
|
-
- `preview_meta` – new event with `{ blob, metadata }`
|
|
841
|
-
|
|
842
|
-
Server protocol (per ComfyUI `protocol.py`):
|
|
843
|
-
|
|
844
|
-
- Binary event IDs:
|
|
845
|
-
- `1` = `PREVIEW_IMAGE` (legacy)
|
|
846
|
-
- `4` = `PREVIEW_IMAGE_WITH_METADATA`
|
|
847
|
-
- For type `4`, payload format after the 4-byte type header:
|
|
848
|
-
- 4 bytes: big-endian uint32 `metadata_length`
|
|
849
|
-
- N bytes: UTF-8 JSON metadata
|
|
850
|
-
- remaining: image bytes (PNG or JPEG)
|
|
851
|
-
|
|
852
|
-
The SDK reads `metadata.image_type` to set the Blob MIME type.
|
|
853
|
-
|
|
854
|
-
Example – low-level API usage:
|
|
855
|
-
|
|
856
|
-
```ts
|
|
857
|
-
api.on('b_preview_meta', (ev) => {
|
|
858
|
-
const { blob, metadata } = ev.detail;
|
|
859
|
-
console.log('[b_preview_meta]', metadata, 'bytes=', blob.size);
|
|
860
|
-
});
|
|
861
|
-
```
|
|
862
|
-
|
|
863
|
-
Example – high-level Workflow API usage:
|
|
864
|
-
|
|
865
|
-
```ts
|
|
866
|
-
const job = await api.run(wf, { autoDestroy: true });
|
|
867
|
-
|
|
868
|
-
job
|
|
869
|
-
.on('preview', (blob) => console.log('preview bytes=', blob.size))
|
|
870
|
-
.on('preview_meta', ({ blob, metadata }) => {
|
|
871
|
-
console.log('mime:', metadata?.image_type, 'size=', blob.size);
|
|
872
|
-
// other metadata fields depend on the server implementation
|
|
873
|
-
});
|
|
874
|
-
```
|
|
875
|
-
|
|
876
|
-
Backwards compatibility:
|
|
877
|
-
|
|
878
|
-
- If the server only emits legacy frames, you will still receive `preview` / `b_preview` events as before.
|
|
879
|
-
- When metadata frames are present, both are emitted: `b_preview` and `b_preview_meta` (and at the high level, `preview` and `preview_meta`).
|
|
880
|
-
|
|
881
|
-
Troubleshooting:
|
|
882
|
-
|
|
883
|
-
- Ensure your ComfyUI build supports `PREVIEW_IMAGE_WITH_METADATA` and that the feature flag is enabled. The SDK announces support via WebSocket on connect.
|
|
884
|
-
|
|
885
|
-
---
|
|
886
|
-
|
|
887
|
-
## API Nodes (Comfy.org paid)
|
|
888
|
-
|
|
889
|
-
Some workflows use paid API nodes (for example, Luma/Photon) that communicate progress and results via additional binary WebSocket frames. This SDK supports those nodes by:
|
|
890
|
-
|
|
891
|
-
- Allowing you to pass your Comfy.org API key to the server with each job
|
|
892
|
-
- Emitting low-level events for binary/text frames so you can surface progress and result URLs
|
|
893
|
-
|
|
894
|
-
### Enabling API-node runs
|
|
895
|
-
|
|
896
|
-
Provide your key through the `comfyOrgApiKey` client option (recommended to source it from an environment variable):
|
|
897
|
-
|
|
898
|
-
```ts
|
|
899
|
-
import { ComfyApi, Workflow } from 'comfyui-node';
|
|
900
|
-
import LumaPhoton from './your-luma-photon-workflow.json';
|
|
901
|
-
|
|
902
|
-
const api = await new ComfyApi(
|
|
903
|
-
process.env.COMFY_HOST || 'http://127.0.0.1:8188',
|
|
904
|
-
undefined,
|
|
905
|
-
{
|
|
906
|
-
comfyOrgApiKey: process.env.COMFY_ORG_API_KEY,
|
|
907
|
-
wsTimeout: 30000, // API nodes may take longer; increase if needed
|
|
908
|
-
debug: true // optional: structured socket + polling logs
|
|
909
|
-
}
|
|
910
|
-
).ready();
|
|
911
|
-
|
|
912
|
-
// Minimal example: set prompt/seed, declare output, observe events
|
|
913
|
-
const wf = Workflow.fromAugmented(LumaPhoton)
|
|
914
|
-
.input('LUMA', 'prompt', 'Old photograph of the Guanabara Bay in Rio de Janeiro, aerial view')
|
|
915
|
-
.input('LUMA', 'seed', -1) // -1 => randomized; see _autoSeeds in result
|
|
916
|
-
.output('final_images', '2'); // alias, nodeId (auto-corrects if swapped)
|
|
917
|
-
|
|
918
|
-
// Low-level API-node events (binary channel text + raw preview bytes)
|
|
919
|
-
api.on('b_text', (ev) => {
|
|
920
|
-
const text = (ev as any).detail as string;
|
|
921
|
-
if (typeof text === 'string') console.log('[api-node text]', text.slice(0, 200));
|
|
922
|
-
});
|
|
923
|
-
api.on('b_text_meta', (ev) => {
|
|
924
|
-
// { channel: number, text: string }
|
|
925
|
-
console.log('[api-node text meta]', (ev as any).detail);
|
|
926
|
-
});
|
|
927
|
-
api.on('b_preview_raw', (ev) => {
|
|
928
|
-
const bytes = (ev as any).detail as Uint8Array;
|
|
929
|
-
console.log('[api-node preview raw bytes]', bytes?.byteLength);
|
|
930
|
-
});
|
|
931
|
-
|
|
932
|
-
const job = await api.run(wf, { autoDestroy: true });
|
|
933
|
-
|
|
934
|
-
job
|
|
935
|
-
.on('start', (id) => console.log('[start]', id))
|
|
936
|
-
.on('progress_pct', (p) => process.stdout.write(`\rprogress ${p}% `))
|
|
937
|
-
.on('preview', (blob) => console.log('\npreview bytes=', blob.size))
|
|
938
|
-
.on('failed', (e) => console.error('\nfailed', e));
|
|
939
|
-
|
|
940
|
-
const result = await job.done();
|
|
941
|
-
console.log('\nPrompt ID:', result._promptId);
|
|
942
|
-
for (const img of (result.final_images?.images || [])) {
|
|
943
|
-
console.log('image path:', api.ext.file.getPathImage(img));
|
|
944
|
-
}
|
|
945
|
-
```
|
|
946
|
-
|
|
947
|
-
Notes:
|
|
948
|
-
|
|
949
|
-
- API-node text frames often include human-readable progress and a final “Result URL:” line. The SDK exposes the raw text via `b_text` and `{ channel, text }` via `b_text_meta` so you can parse or display them as desired.
|
|
950
|
-
- For long-running API calls, increase `wsTimeout` and consider enabling `debug` or setting `COMFY_DEBUG=1` to troubleshoot reconnection/polling.
|
|
951
|
-
- Output declaration accepts any of: `'alias:NodeId'`, `('alias','NodeId')`, or `'NodeId'`. If you accidentally swap the alias/id parameters, the SDK will auto-correct and warn.
|
|
952
|
-
|
|
953
|
-
Security tip: Never print your API key. The built-in debug logger redacts common key/authorization fields automatically.
|
|
954
|
-
|
|
955
|
-
---
|
|
956
|
-
|
|
957
|
-
## Image Inputs: Attach Files (DX)
|
|
958
|
-
|
|
959
|
-
When a workflow references images (e.g., `LoadImage.image = "IMAGE_A.png"` or folder loaders such as `LoadImageSetFromFolderNode`), you can attach local buffers directly to the `Workflow` and let the SDK handle uploads before execution.
|
|
960
|
-
|
|
961
|
-
Helpers:
|
|
962
|
-
|
|
963
|
-
- `wf.attachImage(nodeId, inputName, data, fileName, opts?)`
|
|
964
|
-
- Uploads `data` (Blob/Buffer/ArrayBuffer/Uint8Array) and sets the node input to `fileName` automatically.
|
|
965
|
-
- Options: `{ subfolder?: string; override?: boolean }`.
|
|
966
|
-
- `wf.attachFolderFiles(subfolder, files[], opts?)`
|
|
967
|
-
- Upload multiple files into a server subfolder; ideal for folder‑based loaders.
|
|
968
|
-
|
|
969
|
-
Example (see `scripts/image-loading-demo.ts`):
|
|
970
|
-
|
|
971
|
-
```ts
|
|
972
|
-
import { ComfyApi, Workflow } from 'comfyui-node';
|
|
973
|
-
import Graph from './ImageLoading.json';
|
|
974
|
-
import * as fs from 'node:fs/promises';
|
|
975
|
-
import * as path from 'node:path';
|
|
976
|
-
|
|
977
|
-
const api = await new ComfyApi(process.env.COMFY_HOST || 'http://127.0.0.1:8188').ready();
|
|
978
|
-
const wf = Workflow.from(Graph);
|
|
979
|
-
|
|
980
|
-
// Attach two individual images for LoadImage nodes 2 and 4
|
|
981
|
-
const dir = path.resolve(process.cwd(), 'scripts', 'example_images');
|
|
982
|
-
const a = await fs.readFile(path.join(dir, 'IMAGE_A.png'));
|
|
983
|
-
const b = await fs.readFile(path.join(dir, 'IMAGE_B.png'));
|
|
984
|
-
wf.attachImage('2', 'image', a, 'IMAGE_A.png', { override: true })
|
|
985
|
-
.attachImage('4', 'image', b, 'IMAGE_B.png', { override: true });
|
|
986
|
-
|
|
987
|
-
// Attach an entire folder for node 5 (LoadImageSetFromFolderNode)
|
|
988
|
-
const files = (await fs.readdir(dir))
|
|
989
|
-
.filter(f => /\.(png|jpe?g|webp)$/i.test(f))
|
|
990
|
-
.map(async f => ({ fileName: f, data: await fs.readFile(path.join(dir, f)) }));
|
|
991
|
-
wf.attachFolderFiles('EXAMPLE_IMAGES', await Promise.all(files), { override: true });
|
|
992
|
-
wf.set('5.inputs.folder', 'EXAMPLE_IMAGES');
|
|
993
|
-
|
|
994
|
-
// Collect a simple output target for demonstration
|
|
995
|
-
wf.output('1');
|
|
996
|
-
|
|
997
|
-
const job = await api.run(wf, { autoDestroy: true });
|
|
998
|
-
job.on('progress_pct', p => process.stdout.write(`\rprogress ${p}% `));
|
|
999
|
-
await job.done();
|
|
1000
|
-
```
|
|
1001
|
-
|
|
1002
|
-
Notes:
|
|
1003
|
-
|
|
1004
|
-
- The inputs are updated to point at the uploaded filenames; subfolders are handled server‑side.
|
|
1005
|
-
- Use `override: true` to replace existing files with the same name if needed.
|
|
1006
|
-
|
|
1007
|
-
---
|
|
1008
|
-
|
|
1009
|
-
## 1.0 Migration
|
|
1010
|
-
|
|
1011
|
-
All legacy `ComfyApi` instance methods listed below were **removed in 1.0.0** after a deprecation window in 0.2.x. Migrate to the `api.ext.*` namespaces. If you're upgrading from <1.0, replace calls as shown. No runtime warnings remain (they were stripped with the removals).
|
|
1012
|
-
|
|
1013
|
-
| Deprecated | Replacement |
|
|
1014
|
-
| ---------- | ----------- |
|
|
1015
|
-
| `queuePrompt(...)` | `api.ext.queue.queuePrompt(...)` |
|
|
1016
|
-
| `appendPrompt(...)` | `api.ext.queue.appendPrompt(...)` |
|
|
1017
|
-
| `getHistories(...)` | `api.ext.history.getHistories(...)` |
|
|
1018
|
-
| `getHistory(id)` | `api.ext.history.getHistory(id)` |
|
|
1019
|
-
| `getSystemStats()` | `api.ext.system.getSystemStats()` |
|
|
1020
|
-
| `getCheckpoints()` | `api.ext.node.getCheckpoints()` |
|
|
1021
|
-
| `getLoras()` | `api.ext.node.getLoras()` |
|
|
1022
|
-
| `getSamplerInfo()` | `api.ext.node.getSamplerInfo()` |
|
|
1023
|
-
| `getNodeDefs(name?)` | `api.ext.node.getNodeDefs(name?)` |
|
|
1024
|
-
| `getExtensions()` | `api.ext.misc.getExtensions()` |
|
|
1025
|
-
| `getEmbeddings()` | `api.ext.misc.getEmbeddings()` |
|
|
1026
|
-
| `uploadImage(...)` | `api.ext.file.uploadImage(...)` |
|
|
1027
|
-
| `uploadMask(...)` | `api.ext.file.uploadMask(...)` |
|
|
1028
|
-
| `getPathImage(info)` | `api.ext.file.getPathImage(info)` |
|
|
1029
|
-
| `getImage(info)` | `api.ext.file.getImage(info)` |
|
|
1030
|
-
| `getUserData(file)` | `api.ext.file.getUserData(file)` |
|
|
1031
|
-
| `storeUserData(...)` | `api.ext.file.storeUserData(...)` |
|
|
1032
|
-
| `deleteUserData(file)` | `api.ext.file.deleteUserData(file)` |
|
|
1033
|
-
| `moveUserData(...)` | `api.ext.file.moveUserData(...)` |
|
|
1034
|
-
| `listUserData(...)` | `api.ext.file.listUserData(...)` |
|
|
1035
|
-
| `getUserConfig()` | `api.ext.user.getUserConfig()` |
|
|
1036
|
-
| `createUser(name)` | `api.ext.user.createUser(name)` |
|
|
1037
|
-
| `getSettings()` | `api.ext.user.getSettings()` |
|
|
1038
|
-
| `getSetting(id)` | `api.ext.user.getSetting(id)` |
|
|
1039
|
-
| `storeSettings(map)` | `api.ext.user.storeSettings(map)` |
|
|
1040
|
-
| `storeSetting(id,val)` | `api.ext.user.storeSetting(id,val)` |
|
|
1041
|
-
| `getTerminalLogs()` | `api.ext.terminal.getTerminalLogs()` |
|
|
1042
|
-
| `setTerminalSubscription()` | `api.ext.terminal.setTerminalSubscription()` |
|
|
1043
|
-
| `interrupt()` | `api.ext.queue.interrupt()` |
|
|
1044
|
-
|
|
1045
|
-
Quick grep-based migration (bash):
|
|
1046
|
-
|
|
1047
|
-
```bash
|
|
1048
|
-
grep -R "api\.getSystemStats" -n src | cut -d: -f1 | xargs sed -i '' 's/api\.getSystemStats()/api.ext.system.getSystemStats()/g'
|
|
1049
|
-
```
|
|
1050
|
-
|
|
1051
|
-
PowerShell example:
|
|
1052
|
-
|
|
1053
|
-
```powershell
|
|
1054
|
-
Get-ChildItem -Recurse -Include *.ts | ForEach-Object {
|
|
1055
|
-
(Get-Content $_.FullName) -replace 'api.getSystemStats\(\)', 'api.ext.system.getSystemStats()' | Set-Content $_.FullName
|
|
1056
|
-
}
|
|
1057
|
-
```
|
|
1058
|
-
|
|
1059
|
-
(Adjust the pattern per method; or use a codemod tool if you have many occurrences.)
|
|
1060
|
-
|
|
1061
|
-
Diff example:
|
|
1062
|
-
|
|
1063
|
-
Example migration:
|
|
1064
|
-
|
|
1065
|
-
```diff
|
|
1066
|
-
- const stats = await api.getSystemStats();
|
|
1067
|
-
+ const stats = await api.ext.system.getSystemStats();
|
|
1068
|
-
- await api.uploadImage(buf, 'a.png');
|
|
1069
|
-
+ await api.ext.file.uploadImage(buf, 'a.png');
|
|
1070
|
-
```
|
|
1071
|
-
|
|
1072
|
-
## Reference Overview
|
|
1073
|
-
|
|
1074
|
-
Core (non‑deprecated) `ComfyApi` methods: `init`, `waitForReady`, event registration (`on`/`off`/`removeAllListeners`), `fetchApi`, `pollStatus`, `ping`, `reconnectWs`, `destroy`, and modular surface via `ext`.
|
|
1075
|
-
|
|
1076
|
-
Supporting classes:
|
|
1077
|
-
|
|
1078
|
-
- `PromptBuilder` – graph construction & value injection
|
|
1079
|
-
- `CallWrapper` – prompt execution lifecycle helpers
|
|
1080
|
-
- `ComfyPool` – multi‑instance scheduler
|
|
1081
|
-
|
|
1082
|
-
Enums & Types: `EQueueMode`, sampler / scheduler unions, `OSType`, plus exported response types found under `types/*`.
|
|
1083
|
-
|
|
1084
|
-
## Monitoring: System vs Job Progress
|
|
1085
|
-
|
|
1086
|
-
"Monitoring" in this SDK refers to two unrelated event domains:
|
|
1087
|
-
|
|
1088
|
-
| Type | Source | Requires Extension | Events | Usage |
|
|
1089
|
-
| ---- | ------ | ------------------ | ------ | ----- |
|
|
1090
|
-
| System Monitoring | Crystools extension | Yes (ComfyUI-Crystools) | `system_monitor` (pool) + feature internals | Host CPU/GPU/RAM telemetry |
|
|
1091
|
-
| Job Progress | Core ComfyUI | No | `executing`, `progress`, `executed`, `execution_success`, `execution_error`, `execution_interrupted`, `b_preview` | Per‑job progress %, live image previews |
|
|
1092
|
-
|
|
1093
|
-
System monitoring is toggled via env flags in the smoke script (`COMFY_MONITOR`, `COMFY_MONITOR_STRICT`, `COMFY_MONITOR_FORCE`) and is surfaced under `api.ext.monitor`.
|
|
1094
|
-
|
|
1095
|
-
Job progress monitoring is always active: subscribe directly (`api.on("progress", ...)`) or use higher‑level helpers:
|
|
1096
|
-
|
|
1097
|
-
```ts
|
|
1098
|
-
new CallWrapper(api, builder)
|
|
1099
|
-
.onProgress(p => console.log(p.value, '/', p.max))
|
|
1100
|
-
.onPreview(blob => /* show transient image */)
|
|
1101
|
-
.onFinished(out => /* final outputs */)
|
|
1102
|
-
.run();
|
|
165
|
+
await api.ext.file.uploadImage(buffer, 'image.png');
|
|
166
|
+
const history = await api.ext.history.getHistory('prompt-id');
|
|
1103
167
|
```
|
|
1104
168
|
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
If you only need generation progress & previews you do NOT need the Crystools extension.
|
|
1108
|
-
|
|
1109
|
-
## Examples
|
|
169
|
+
See [API Features docs](./docs/api-features.md) for complete namespace reference.
|
|
1110
170
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
## Errors & Diagnostics
|
|
1114
|
-
|
|
1115
|
-
The SDK raises specialized subclasses of `Error` to improve debuggability during workflow submission and execution:
|
|
1116
|
-
|
|
1117
|
-
| Error | When | Key Extras |
|
|
1118
|
-
| ----- | ---- | ---------- |
|
|
1119
|
-
| `EnqueueFailedError` | HTTP `/prompt` (append/queue) failed | `status`, `statusText`, `url`, `method`, `bodyJSON`, `bodyTextSnippet`, `reason` |
|
|
1120
|
-
| `ExecutionFailedError` | Execution finished but not all mapped outputs arrived | missing outputs context |
|
|
1121
|
-
| `ExecutionInterruptedError` | Server emitted an interruption mid run | cause carries interruption detail |
|
|
1122
|
-
| `MissingNodeError` | A declared bypass or output node is absent | `cause` (optional) |
|
|
1123
|
-
| `WentMissingError` | Job disappeared from queue and no cached output | – |
|
|
1124
|
-
| `FailedCacheError` | Cached output retrieval failed | – |
|
|
1125
|
-
| `CustomEventError` | Server emitted execution error event | event payload in `cause` |
|
|
1126
|
-
| `DisconnectedError` | WebSocket disconnected mid‑execution | – |
|
|
1127
|
-
|
|
1128
|
-
### Error Codes
|
|
1129
|
-
|
|
1130
|
-
Every custom error exposes a stable `code` (enum) to enable branch logic without string matching message text:
|
|
171
|
+
### Events
|
|
1131
172
|
|
|
1132
173
|
```ts
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
if ((e as any).code === ErrorCode.ENQUEUE_FAILED) {
|
|
1137
|
-
// inspect structured diagnostics
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
```
|
|
1141
|
-
|
|
1142
|
-
### EnqueueFailedError Details
|
|
1143
|
-
|
|
1144
|
-
When the server rejects a workflow submission the SDK now attempts to surface the underlying cause:
|
|
174
|
+
api.on('progress', ev => console.log(ev.detail.value, '/', ev.detail.max));
|
|
175
|
+
api.on('b_preview', ev => console.log('Preview:', ev.detail.size));
|
|
176
|
+
api.on('executed', ev => console.log('Node:', ev.detail.node));
|
|
1145
177
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
} catch (e) {
|
|
1150
|
-
if (e instanceof EnqueueFailedError) {
|
|
1151
|
-
console.error('Status:', e.status, e.statusText);
|
|
1152
|
-
console.error('Reason:', e.reason);
|
|
1153
|
-
console.error('Body JSON:', e.bodyJSON);
|
|
1154
|
-
console.error('Snippet:', e.bodyTextSnippet);
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
178
|
+
job.on('progress_pct', pct => console.log(`${pct}%`));
|
|
179
|
+
job.on('preview', blob => console.log('Preview:', blob.size));
|
|
180
|
+
job.on('failed', err => console.error(err));
|
|
1157
181
|
```
|
|
1158
182
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
If the response body is not JSON, `bodyTextSnippet` contains the first 500 characters of the returned text, which is also copied into `reason`.
|
|
1162
|
-
|
|
1163
|
-
These enriched diagnostics are only attached for the enqueue phase; downstream execution issues still rely on event‑level errors.
|
|
1164
|
-
|
|
1165
|
-
### Execution Failure vs Interruption
|
|
1166
|
-
|
|
1167
|
-
- `ExecutionFailedError`: The workflow ran but one or more declared output nodes never produced data (often due to an upstream node error not surfaced as a global event). Revisit your output mappings or inspect per‑node errors.
|
|
1168
|
-
- `ExecutionInterruptedError`: The server (or user action) actively interrupted execution; retrying may succeed if the interruption cause was transient.
|
|
1169
|
-
|
|
1170
|
-
### Persisting & Replaying Builder State
|
|
1171
|
-
|
|
1172
|
-
You can store builder state in a database / job queue:
|
|
1173
|
-
|
|
1174
|
-
```ts
|
|
1175
|
-
const snapshot = builder.toJSON();
|
|
1176
|
-
// later
|
|
1177
|
-
const restored = PromptBuilder.fromJSON(snapshot)
|
|
1178
|
-
.validateOutputMappings();
|
|
1179
|
-
```
|
|
1180
|
-
|
|
1181
|
-
This is useful for deferred execution, cross‑process scheduling, or audit logging of the exact prompt graph sent to the server.
|
|
1182
|
-
|
|
1183
|
-
## Testing & Coverage
|
|
1184
|
-
|
|
1185
|
-
This repository uses Bun's built-in test runner. Common scripts:
|
|
1186
|
-
|
|
1187
|
-
```bash
|
|
1188
|
-
bun test # unit + lightweight integration tests
|
|
1189
|
-
bun run test:real # real server tests (COMFY_REAL=1)
|
|
1190
|
-
bun run test:full # comprehensive real server tests (COMFY_REAL=1 COMFY_FULL=1)
|
|
1191
|
-
bun run coverage # text coverage summary (lines/functions per file)
|
|
1192
|
-
bun run coverage:lcov # generate coverage/lcov.info (for badges or external services)
|
|
1193
|
-
bun run coverage:enforce # generate LCOV then enforce thresholds
|
|
1194
|
-
```
|
|
1195
|
-
|
|
1196
|
-
Environment flags:
|
|
1197
|
-
|
|
1198
|
-
- `COMFY_REAL=1` enables `test/real.integration.spec.ts` (expects a running ComfyUI at `http://localhost:8188` unless overridden via `COMFY_HOST`).
|
|
1199
|
-
- `COMFY_FULL=1` additionally enables the extended `test/real.full.integration.spec.ts` suite.
|
|
1200
|
-
- `COMFY_HOST=http://host:port` to point at a non-default instance.
|
|
1201
|
-
|
|
1202
|
-
Coverage thresholds are enforced by `scripts/coverage-check.ts` (baseline intentionally modest to allow incremental improvement):
|
|
1203
|
-
|
|
1204
|
-
Default thresholds:
|
|
1205
|
-
|
|
1206
|
-
- Lines: `>= 25%`
|
|
1207
|
-
- Functions: `>= 60%`
|
|
1208
|
-
|
|
1209
|
-
Override thresholds ad hoc (CI example):
|
|
183
|
+
## Testing
|
|
1210
184
|
|
|
1211
185
|
```bash
|
|
1212
|
-
|
|
186
|
+
bun test # Unit + integration tests
|
|
187
|
+
bun run test:real # Real server tests (COMFY_REAL=1)
|
|
188
|
+
bun run test:full # Comprehensive tests (COMFY_FULL=1)
|
|
189
|
+
bun run coverage # Coverage report
|
|
1213
190
|
```
|
|
1214
191
|
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
```powershell
|
|
1218
|
-
$env:COVERAGE_MIN_LINES=30; $env:COVERAGE_MIN_FUNCTIONS=65; bun run coverage:enforce
|
|
1219
|
-
```
|
|
1220
|
-
|
|
1221
|
-
### Improving Coverage
|
|
1222
|
-
|
|
1223
|
-
Current low-coverage areas (see `bun test --coverage` output):
|
|
1224
|
-
|
|
1225
|
-
- `src/client.ts` – large surface; break out helpers & add unit tests for fetch error branches and WebSocket reconnect logic.
|
|
1226
|
-
- `src/call-wrapper.ts` – test error paths (enqueue failure, execution interruption, missing outputs) with mocked `fetch` & event streams.
|
|
1227
|
-
- Feature modules with toleration logic (`monitoring`, `manager`, `terminal`) – add mocks to simulate absent endpoints & successful responses.
|
|
192
|
+
See [Troubleshooting docs](./docs/troubleshooting.md#testing--coverage) for details.
|
|
1228
193
|
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
1. Extract pure helper functions from monolithic classes (e.g., parsing, polling backoff) into modules you can unit test in isolation.
|
|
1232
|
-
2. Add fine-grained tests for error branches (simulate non-200 responses & malformed JSON bodies) to raise line coverage quickly.
|
|
1233
|
-
3. Introduce deterministic mock WebSocket that replays scripted events (connection drop, progress, output) to cover reconnect & event translation.
|
|
1234
|
-
4. Gradually raise `COVERAGE_MIN_LINES` by 5% after each meaningful set of additions.
|
|
1235
|
-
|
|
1236
|
-
Skipping heavy real-image generation: full suite internally tolerates missing models & will skip or soften assertions rather than fail—use it sparingly in CI (nightly job) if runtime is a concern.
|
|
1237
|
-
|
|
1238
|
-
If contributing, please run at least:
|
|
1239
|
-
|
|
1240
|
-
```bash
|
|
1241
|
-
bun test && bun run coverage
|
|
1242
|
-
```
|
|
1243
|
-
|
|
1244
|
-
before opening a PR, and prefer adding tests alongside new feature code.
|
|
1245
|
-
|
|
1246
|
-
## Troubleshooting
|
|
1247
|
-
|
|
1248
|
-
| Symptom | Likely Cause | Fix |
|
|
1249
|
-
| ------- | ------------ | ---- |
|
|
1250
|
-
| `progress_pct` never fires | Only listening to raw `progress` (or run finished instantly) | Subscribe to `progress_pct`; ensure workflow isn't trivially cached / instant |
|
|
1251
|
-
| Empty `images` array | Wrong node id in `.output()` or no `SaveImage` nodes detected | Verify node id in base JSON; omit outputs to let auto-detect run |
|
|
1252
|
-
| `_autoSeeds` missing | No `seed: -1` inputs present | Set seed field explicitly to `-1` on nodes requiring randomization |
|
|
1253
|
-
| Autocomplete missing for sampler | Used `Workflow.from(...)` not `fromAugmented` | Switch to `Workflow.fromAugmented(json)` |
|
|
1254
|
-
| Type not updating after new `.output()` | Captured type alias before adding the call | Recompute `type R = ReturnType<typeof wf.typedResult>` after the last output declaration |
|
|
1255
|
-
| Execution error but no missing outputs | Underlying node error surfaced via `execution_error` event | Listen to `failed` + inspect error / server logs |
|
|
1256
|
-
| Job hangs waiting for output | Declared non-existent node id | Run with fewer outputs or validate JSON; inspect `_nodes` metadata |
|
|
1257
|
-
| Random seed not changing between runs | Provided explicit numeric seed | Use `-1` sentinel or generate a random seed before `.set()` |
|
|
1258
|
-
| Preview frames never appear | Workflow lacks preview-capable nodes (e.g. KSampler) | Confirm server emits `b_preview` events for your graph |
|
|
1259
|
-
| Pool never selects idle client | Mode set to `PICK_LOWEST` with constant queue depth | Switch to `PICK_ZERO` for latency focus |
|
|
1260
|
-
| High-level run returns immediately | Accessed `await api.run(wf)` only (acceptance barrier) | Await `job.done()` or events to completion |
|
|
1261
|
-
|
|
1262
|
-
Diagnostic tips:
|
|
1263
|
-
|
|
1264
|
-
- Enable verbose progress: set `COMFY_PROGRESS_VERBOSE=1` before running the smoke script.
|
|
1265
|
-
- For enqueue failures inspect `EnqueueFailedError` fields (`status`, `reason`, `bodyTextSnippet`).
|
|
1266
|
-
- Use `_aliases` metadata to confirm alias -> node id mapping at runtime.
|
|
1267
|
-
- Log `_autoSeeds` to verify sentinel replacement behavior in batch runs.
|
|
1268
|
-
- If types feel stale, close & reopen the file – TypeScript sometimes caches deep conditional expansions.
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
## Published Smoke Test
|
|
1272
|
-
|
|
1273
|
-
The script `scripts/published-e2e.ts` offers a zero‑config verification of the published npm artifact with **Bun auto‑install**. It dynamically imports `comfyui-node`, builds a small txt2img workflow (optionally an upscale branch), waits for completion and prints output image URLs.
|
|
1274
|
-
|
|
1275
|
-
### Quick Run (Auto‑Install)
|
|
1276
|
-
|
|
1277
|
-
```bash
|
|
1278
|
-
mkdir comfyui-node-smoke
|
|
1279
|
-
cd comfyui-node-smoke
|
|
1280
|
-
curl -o published-e2e.ts https://raw.githubusercontent.com/igorls/comfyui-node/main/scripts/published-e2e.ts
|
|
1281
|
-
COMFY_HOST=http://localhost:8188 bun run published-e2e.ts
|
|
1282
|
-
```
|
|
1283
|
-
|
|
1284
|
-
### Optional Explicit Install
|
|
1285
|
-
|
|
1286
|
-
```bash
|
|
1287
|
-
mkdir comfyui-node-smoke
|
|
1288
|
-
cd comfyui-node-smoke
|
|
1289
|
-
bun add comfyui-node
|
|
1290
|
-
curl -o published-e2e.ts https://raw.githubusercontent.com/igorls/comfyui-node/main/scripts/published-e2e.ts
|
|
1291
|
-
COMFY_HOST=http://localhost:8188 bun run published-e2e.ts
|
|
1292
|
-
```
|
|
1293
|
-
|
|
1294
|
-
### Environment Variables
|
|
1295
|
-
|
|
1296
|
-
| Var | Default | Purpose |
|
|
1297
|
-
| --- | ------- | ------- |
|
|
1298
|
-
| `COMFY_HOST` | `http://127.0.0.1:8188` | Base ComfyUI server |
|
|
1299
|
-
| `COMFY_MODEL` | `SDXL/sd_xl_base_1.0.safetensors` | Checkpoint file name (must exist) |
|
|
1300
|
-
| `COMFY_POSITIVE_PROMPT` | scenic base prompt | Positive text |
|
|
1301
|
-
| `COMFY_NEGATIVE_PROMPT` | `text, watermark` | Negative text |
|
|
1302
|
-
| `COMFY_SEED` | random | Deterministic seed override |
|
|
1303
|
-
| `COMFY_STEPS` | `8` | Sampling steps |
|
|
1304
|
-
| `COMFY_CFG` | `2` | CFG scale |
|
|
1305
|
-
| `COMFY_SAMPLER` | `dpmpp_sde` | Sampler name |
|
|
1306
|
-
| `COMFY_SCHEDULER` | `sgm_uniform` | Scheduler name |
|
|
1307
|
-
| `COMFY_TIMEOUT_MS` | `120000` | Overall timeout (ms) |
|
|
1308
|
-
| `COMFY_UPSCALE` | unset | If set, adds RealESRGAN upscale branch |
|
|
1309
|
-
| `COMFY_MONITOR` | unset | If set, attempt to enable Crystools system monitor & log first event |
|
|
1310
|
-
| `COMFY_MONITOR_STRICT` | unset | With monitor enabled, fail (exit 5) if no events received |
|
|
1311
|
-
|
|
1312
|
-
Exit codes: 0 success, 1 import failure, 2 timeout, 3 enqueue failure, 4 other error, 5 monitor strict failure.
|
|
1313
|
-
|
|
1314
|
-
### Rationale
|
|
1315
|
-
|
|
1316
|
-
Ensures the published `dist` is coherent and functional in a clean consumer environment; can later be wired into CI behind an opt‑in flag (e.g. `E2E_PUBLISHED=1`).
|
|
194
|
+
## Contributing
|
|
1317
195
|
|
|
1318
|
-
|
|
196
|
+
Issues and PRs welcome! Please:
|
|
1319
197
|
|
|
1320
|
-
|
|
198
|
+
- Include tests for new features
|
|
199
|
+
- Follow existing code style
|
|
200
|
+
- Keep feature surfaces minimal & cohesive
|
|
201
|
+
- Run `bun test && bun run coverage` before submitting
|
|
1321
202
|
|
|
1322
|
-
##
|
|
203
|
+
## License
|
|
1323
204
|
|
|
1324
|
-
|
|
205
|
+
MIT – see [LICENSE](./LICENSE)
|
|
1325
206
|
|
|
1326
|
-
##
|
|
207
|
+
## Links
|
|
1327
208
|
|
|
1328
|
-
|
|
209
|
+
- **npm:** [comfyui-node](https://www.npmjs.com/package/comfyui-node)
|
|
210
|
+
- **GitHub:** [igorls/comfyui-node](https://github.com/igorls/comfyui-node)
|
|
211
|
+
- **ComfyUI:** [comfyanonymous/ComfyUI](https://github.com/comfyanonymous/ComfyUI)
|