Cloud Job Output Download#
comfy download fetches output files from a completed ComfyUI job to the local filesystem. It resolves output URLs from one of three sources β piped stdin, on-disk state file, or a live API query β and streams each file to the configured output directory. The entry point is execute_download() in comfy_cli/command/transfer.py.
CLI Command#
Registered in cmdline.py:
comfy download [PROMPT_ID] [--out-dir DIR] [--where local|cloud] [--url-only]
| Option | Default | Description |
|---|---|---|
PROMPT_ID | (stdin) | Prompt ID to download. Omit to read from piped stdin. |
--out-dir / -o | ./outputs | Directory to save output files. |
--where | (auto) | Force routing to local or cloud. |
--url-only | False | Emit resolved URLs as JSON without downloading. |
The pipe pattern comfy --json run --wait | comfy download is the documented zero-argument usage . --url-only is useful for agents that need to pass URLs to other tools rather than downloading files.
Preflight Credential Validation#
Before any download attempt, the command validates cloud credentials via cloud_preflight_or_exit() in comfy_cli/where.py. This runs only when routing targets cloud .
cloud_preflight() checks credentials in this order:
- API key β
COMFY_CLOUD_API_KEYenv var or storedcomfy-cloud-api-keyprovider record. No expiry check; validity is confirmed by the server at request time . - OAuth session β proactively refreshed via
get_session(refresh=True)to avoid failures from a lapsed short-lived access token .
Failure codes:
cloud_not_configuredβ neither credential is present; hint:comfy cloud login.cloud_unauthorizedβ OAuth session present but expired .
A second guard fires inside execute_download() if the state-file lookup misses and an API query is needed: constructing Client(target, clear_session_on_auth_failure=False) raises Unauthenticated immediately if the cloud target has neither auth_token nor api_key . The flag clear_session_on_auth_failure=False prevents download's observer-role API calls from clearing the shared OAuth session on a transient failure .
Output URL Resolution (Priority Order)#
execute_download() resolves output URLs by trying three sources in order :
1. Piped stdin#
When PROMPT_ID is omitted and stdin is non-interactive, the function reads a JSON envelope from comfy --json run --wait. It extracts data.prompt_id and data.outputs . If ok=false in the envelope, the error is surfaced immediately with a hint to the upstream prompt ID .
2. On-disk state file#
jobs_state.read(prompt_id) reads <config-root>/jobs/<prompt_id>.json. If the file exists and state.outputs is populated, those URLs are used directly without any network call . The state file also carries state.record and state.item_map, which are used later to annotate output files with node/item provenance .
The JobState schema :
outputs: list of output URLsrecord: full node-keyed history record from the cloud APIitem_map: foreach-item β node IDs mapping written at submit timewhere:"local"or"cloud"β used to gate local-disk copy vs HTTP fetch
3. API query fallback#
If neither stdin nor state file yields URLs, Client.get_history(prompt_id) is called, then Client.extract_output_urls(record) flattens the node-keyed history record into a URL list .
extract_output_entries() iterates outputs[node_id] for media keys images, gifs, videos, audio, files and builds (filename, subfolder, type) tuples. Client.view_url() encodes these as /view?filename=β¦&subfolder=β¦&type=β¦ query parameters.
Download Execution#
For each resolved URL :
- Local job (
state.where == "local"): output URLs are bare on-disk paths orfile://URLs. The file is copied via_copy_local_output_capped()instead of an HTTP fetch. The local branch is gated onstate.where, never URL shape, to prevent SSRF . - Remote/cloud job:
_assert_download_url()rejects non-HTTP/HTTPS URLs. Auth headers (X-API-KeyorAuthorization: Bearer) are stripped on redirect by_DownloadRedirectHandlerso credentials never follow a 302 to a signed GCS URL.
Safety limits applied to every download :
- 10 GB per-file cap (
_MAX_DOWNLOAD_BYTES) - 30 s per-socket-operation timeout (
_DOWNLOAD_TIMEOUT_S) - Content-Length verification: truncated transfers raise
download_failedrather than silently saving a partial file
Files are written to a .part temp file via _open_part_file(), then atomically renamed into place so local_path never holds a partial file .
Output filename convention :
- If provenance (node/item) is known:
<item>_<nnn><ext>with a per-item counter - Otherwise:
<prompt_id[:8]>_<idx><ext> - Existing files are never overwritten β
_collision_safe_path()appends.1,.2, etc.
The default output directory resolves to <project_root>/outputs, falling back to the default_project_dir config key, then ./outputs .
Key Source Files#
| File | Role |
|---|---|
comfy_cli/command/transfer.py | execute_download() β full download orchestration |
comfy_cli/comfy_client.py | Client, get_history(), extract_output_urls(), Unauthenticated |
comfy_cli/jobs_state.py | JobState, read() β on-disk state file schema and reader |
comfy_cli/where.py | cloud_preflight(), cloud_preflight_or_exit() β credential preflight |
comfy_cli/cmdline.py | download Typer command registration |