Execution Flow¶
The complete sequence from clicking "Generate" to seeing the result.
Step-by-Step¶
1. Form Submission¶
The user fills the form on the runner page and clicks Generate (or the multi-generate stepper sends N sequential requests).
The frontend sends:
POST /api/run/{workflow_id}/execute
Content-Type: multipart/form-data
Fields: all form values as key-value pairs
File: input image (if applicable)
2. Image Upload (if applicable)¶
If the workflow has an image type input and the user selected an image, it was already uploaded to ComfyUI when selected (not at form submission time):
The backend receives the filename from ComfyUI's response.
3. Build Workflow¶
The backend calls _build_workflow():
Static workflows:
1. Load workflow.json
2. Scan all node inputs for "{{variable}}" patterns
3. Replace each with the corresponding form value, converting types (string→int, string→float, etc.)
Dynamic workflows:
1. Load manifest pipeline definition
2. For each block in order:
- Load the block JSON from blocks/
- Prefix all node IDs with {block}_{instance}_
- Resolve imports (wire to previous block's exports)
- Substitute {{variables}} with form values
3. Merge all nodes into one flat dict
4. Handle LoRA injection: if lora_picker_dynamic was used, insert a chain of LoraLoader nodes between the checkpoint and sampler
5. Handle resolution: extract width and height from resolution_picker and inject into EmptyLatentImage
6. Handle seed: resolve -1 to a random integer
4. Patch Outputs¶
Before sending to ComfyUI, the backend patches ALL output nodes:
SaveImage→ prefix becomescomfyui-studio/{jobid}/imageVHS_VideoCombine→ prefix becomescomfyui-studio/{jobid}/videoPreviewImage→ prefix becomescomfyui-studio/{jobid}/preview- Any
VHS_VideoCombinewithsave_output=false→ forced totrue, prefixintermediate
5. Send to ComfyUI¶
POST http://localhost:8188/prompt
{
"prompt": {assembled workflow dict},
"client_id": "unique-client-id"
}
ComfyUI responds with a prompt_id that identifies this execution.
6. Save Job Record¶
The backend creates a job record:
{
"prompt_id": "abc123...",
"workflow_id": "t2i-batch",
"workflow_name": "Text to Image (Batch)",
"status": "queued",
"queued_at": "2026-03-24T15:30:00",
"params": {form values},
"seeds": {resolved seeds},
"output_dir": "comfyui-studio/abc123...",
"input_image": "uploaded_image.png",
"output": null,
"error": null
}
This is saved to the SQLite database. The in-memory progress tracker is also initialized.
7. Start WebSocket Listener¶
A background thread connects to ComfyUI's WebSocket:
This thread captures ALL messages from ComfyUI for this execution:
| Message Type | Action |
|---|---|
execution_start |
Update status to "running", create .incomplete marker, record started_at |
execution_cached |
Track cached node IDs, adjust effective_total (exclude cached from progress) |
executing {node} |
Update progress: which node, nodes_done count. Exclude "instant" types (loaders, encoders) from progress percentage |
progress {value, max} |
Step-level progress within a node. Calculate ETA from rolling 10-step average. Calculate step rate (it/s) |
executed {node, output} |
Track node outputs (images, videos). Store in node_outputs for preview |
| Binary frame (JPEG/PNG) | Preview image. Detect by magic bytes: FF D8 = JPEG, 89 50 = PNG. Forward to connected WS clients |
execution_error |
Update status to "error", save error message with node title |
8. Progress Broadcast¶
The Studio WebSocket endpoint (/api/run/ws) runs a loop every 150ms that pushes the current progress state to ALL connected browser clients:
{
"status": "running",
"node_title": "KSampler",
"step": 15,
"total_steps": 20,
"nodes_done": 3,
"total_nodes": 5,
"effective_total": 4,
"percent": 75,
"eta_seconds": 12.5,
"step_rate": 1.6,
"node_outputs": {}
}
Binary preview frames are forwarded directly.
9. Completion¶
When ComfyUI finishes:
- The WS listener receives the final
executedmessage - Status updated to "completed"
finished_attimestamp recordeddurationcalculated (finished - started)- Best output selected (video preferred over image)
.incompletemarker removed from output directory- Job record saved to database
- Event emitted:
job.completed
10. Result Display¶
The frontend on the Queue page sees the progress reach 100% and the status change to "completed". The History page shows the completed job with:
- Thumbnail/preview of the output
- Duration
- Parameters used
- Seeds (for reproducibility)
- Links to output files
Progress Calculation¶
Effective Total¶
Not all nodes contribute meaningfully to progress. The system distinguishes:
- Processing nodes (KSampler, VAEDecode, VHS_VideoCombine, etc.) — counted in progress
- Instant nodes (CheckpointLoaderSimple, CLIPTextEncode, EmptyLatentImage, etc.) — excluded
effective_total = total_nodes - cached_nodes - instant_nodes
This gives a more accurate percentage. Without this adjustment, a 10-node workflow where 5 nodes are instant loaders would show 50% complete before any actual processing starts.
ETA Calculation¶
ETA is calculated from the step-level progress of the current sampling node:
- Track the last 10 step timestamps
- Calculate average time per step
- Remaining steps = total_steps - current_step
- ETA = remaining_steps × average_step_time
The step rate (iterations/second) is also calculated and displayed.
Error Handling¶
If ComfyUI sends an execution_error message:
- Status set to "error"
- Error message extracted (includes the node title that failed)
.incompletemarker removed- Job record saved with error
- Event emitted:
job.failed
If the WebSocket connection drops unexpectedly:
- The listener thread exits
- If the job was "running", it stays as "running" in the database
- On next backend startup, the startup task marks all "running"/"queued" jobs as "stalled"