Python SDK for running ComfyUI workflows via the Comfy API v2 (self-hosted, Comfy Cloud, serverless).
Project description
comfy-sdk (Python)
Python SDK for running ComfyUI workflows via the Comfy API v2. The same code runs against a self-hosted ComfyUI, Comfy Cloud, or a serverless deployment — only the base URL and an optional API key change.
from comfy_sdk import Comfy
client = Comfy("http://127.0.0.1:8189") # self-hosted, no key
# client = Comfy("https://api.comfy.org", api_key="ck_...") # Comfy Cloud
wf = client.workflows.from_file("workflow_api.json")
# Lazy asset handle: hashed locally with blake3, deduped against the server's
# fast-path (mint over existing bytes), or streamed-uploaded on a miss — then
# substituted into the graph as a core/ASSET reference.
asset = client.assets.from_file("photo.png")
wf.set_input("10", "image", asset)
job = client.run(wf) # submit, then poll to a terminal state
job.get_outputs("13")[0].to_file("out.png")
Requirements and install
Requires Python 3.10+. Dependencies: httpx, blake3, pydantic (v2).
pip install comfy-sdk
Releases are published to PyPI from a GitHub Release (tag vX.Y.Z) by
.github/workflows/publish.yml, using
PyPI's Trusted Publishing (OIDC) — no API token is stored in this repo.
To install from source instead (for local development, or to track an unreleased commit):
git clone https://github.com/Comfy-Org/ComfyPythonSDK
cd ComfyPythonSDK
pip install -e .
# with everything needed to lint/type-check/test locally:
pip install -e ".[dev]"
Preview.to_pil() (decoding an in-progress SSE preview frame to a PIL.Image)
needs the optional pil extra: pip install -e ".[pil]".
Authentication — one client, per-surface key
| Surface | Example base URL | api_key |
|---|---|---|
| Self-hosted ComfyUI (behind the API proxy) | http://127.0.0.1:8189 |
Omit — no key is sent, even implicitly |
| Comfy Cloud | https://api.comfy.org |
Required |
| Serverless deployment | https://<deployment>.comfy.org |
Required |
client = Comfy("http://127.0.0.1:8189") # self-hosted
client = Comfy("https://api.comfy.org", api_key="ck_...") # Comfy Cloud / serverless
AsyncComfy takes the same two arguments. A key is only ever attached to
requests aimed at the configured base_url's own origin — a server-returned
follow-up link (job.urls.self/cancel/events, or a redirected asset
download) pointing anywhere else never receives it.
Partner (API) node auth
Workflows that use partner/API nodes (Gemini, etc.) need a Comfy API key to
authenticate them. Pass it per submit with api_key=. This is not the same
as the api_key you construct Comfy with: the constructor key authenticates
you to the server, while this one authenticates the partner nodes inside the
workflow (it is often the same comfyui-… key):
job = client.run(wf, api_key="comfyui-...")
# or drive it yourself:
job = client.submit(wf, api_key="comfyui-...")
The SDK sends it once as extra_data.api_key_comfy_org alongside the workflow —
one key authenticates every partner node in the graph. It is never logged or
persisted by the SDK. Omit api_key and no extra_data is sent at all.
Assets and core/ASSET
client.assets.from_file(...) / from_bytes(...) / from_stream(...) /
from_url(...) return a lazy asset handle immediately — no network call
yet. Embed it directly into the workflow graph:
asset = client.assets.from_file("photo.png")
wf.set_input("10", "image", asset)
On first use (submitting the workflow, or an explicit asset.commit()), the
SDK:
- hashes the bytes locally with blake3;
- probes the server's dedup fast-path — a
HEADexistence check by hash, then a cheapfrom-hashmint if the server already has those bytes; - only streams a full multipart upload on a miss.
At submit time, every asset handle found anywhere in the graph is replaced by
a core/ASSET reference object ({"__type": "core/ASSET", "info": {"id": ..., "hash": ..., "file_path": ...}}), which the server resolves back to the
uploaded asset when it runs the workflow.
Live progress
job = client.submit(wf)
for event in job.events(): # SSE; live, auto-reconnecting (no replay)
match event:
case Progress() as p: print(f"{p.value:.0%} {p.message}")
case Preview() as pv: show(pv.to_pil())
case OutputReady() as o: o.output.to_file(f"partial/{o.output.name}")
case StatusChange(status="succeeded"): break
result = job.result() # raises JobFailed with node details on failure
job.events() reconnects automatically if the stream drops, but never
replays a frame you've already seen (the stream carries no cursor). That's
why polling stays authoritative: job.wait() / job.result() (and
client.run(), which is submit() + result()) always fall back to
GET /jobs/{id} to decide when a job is really done — use events() for
live UI feedback, and wait()/result()/run() for the definitive answer.
job.status is the current status string; job.outputs is the full list of
output handles regardless of which node produced them (job.get_outputs(node_id)
filters to one node, as in the quickstart above).
Sync and async
Comfy and AsyncComfy expose the identical surface — swap the import and
add await / async for:
from comfy_sdk import AsyncComfy
async def main() -> None:
async with AsyncComfy("http://127.0.0.1:8189") as client:
wf = client.workflows.from_file("workflow_api.json")
job = await client.run(wf)
await job.outputs[0].to_file("out.png")
Typed errors
comfy_sdk translates the API's error envelope into a small set of
exceptions, all importable from the top-level package and all subclasses of
ComfyError:
Unauthorized,Forbidden,NotFound— auth and lookup failures.InvalidWorkflow,WorkflowFormatUi— the graph itself was rejected;WorkflowFormatUispecifically means a UI-export (nodes/links/last_node_id) was submitted instead of the API-format graph — the SDK catches this locally before it ever reaches the server.MissingAsset— acore/ASSETreference could not be resolved.HashMismatch,BlobNotFound— asset upload/dedup failures.IdempotencyKeyReuse— theIdempotency-Keywas reused.submit()(andrun()) attach a fresh key to every call, so an accidental exact resend never runs the workflow twice. Keys are single-use — reject-on-duplicate, there is no replay — so if you pass your ownidempotency_key=and reuse it, the second call raises this. After an ambiguous failure (e.g. a timeout where you don't know if the job was created), poll or list your jobs rather than resubmitting with the same key.InsufficientCredits— the account can't afford the job.QueueFull— backpressure; carries.retry_afterseconds.client.submitalready retries this automatically for a bounded budget before giving up and raising it.JobFailed— a job reached a non-succeededterminal state;.errorcarries node-level detail when the platform provided one.
from comfy_sdk import JobFailed, QueueFull, Unauthorized
try:
result = client.run(wf)
except JobFailed as e:
print(e.error)
except Unauthorized:
print("check your api_key")
Architecture — two layers
-
comfy_low— generated protocol bindings. Pydantic v2 models generated fromspec/openapi.yaml(src/comfy_low/models/_generated.py, committed; regenerate withscripts/gen_models.sh, CI fails on drift) plus a thin hand-writtenhttpxtransport (sync + async), one function peroperationId, with the mandatory escape hatches: raw response access, unbuffered/streaming bodies, all headers, and per-request timeout/abort. Boring and replaceable. -
comfy_sdk— the idiomatic layer integrators import. This is where the value lives: blake3 content-addressed dedup-upload,core/ASSETsubstitution, idempotent submit, live SSE with reconnect, poll-authoritativerun(), range-aware downloads, and typed exceptions mapping the error envelope.
spec/openapi.yaml is a one-way vendored copy of the canonical Comfy API v2
contract — do not hand-edit it (see spec/README.md). It's synced
periodically from that canonical contract, stripped of anything tagged
internal, and pinned by spec/VERSION.
Related projects
Part of the same SDK family: a TypeScript client with the equivalent surface
for JS/Node integrators, and the local API proxy that fronts a self-hosted
ComfyUI instance with this same v2 contract (comfy-api-proxy in the
servers list of spec/openapi.yaml).
Development
pip install -e ".[dev]"
ruff check .
ruff format --check .
mypy src
pytest -v
Regenerating and checking the vendored protocol layer (a separate CI job):
pip install -e ".[codegen]"
python scripts/gen_models.sh # regenerate comfy_low models from spec/openapi.yaml
python scripts/check_drift.py # same check CI runs; fails if committed models drifted
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file comfy_sdk-0.1.1.tar.gz.
File metadata
- Download URL: comfy_sdk-0.1.1.tar.gz
- Upload date:
- Size: 135.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8b3398a3e94db5b1641e9fc110c9b15e26f11b6366eee2cfae44c16c8f3011ff
|
|
| MD5 |
5dd5bcc84c8c6591b016666fc6aa79ac
|
|
| BLAKE2b-256 |
d9c163ff2ff9ee6fa7974bb05408b65f62159b7e91ce7ba979929e70737e2606
|
Provenance
The following attestation bundles were made for comfy_sdk-0.1.1.tar.gz:
Publisher:
publish.yml on Comfy-Org/ComfyPythonSDK
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
comfy_sdk-0.1.1.tar.gz -
Subject digest:
8b3398a3e94db5b1641e9fc110c9b15e26f11b6366eee2cfae44c16c8f3011ff - Sigstore transparency entry: 2215062994
- Sigstore integration time:
-
Permalink:
Comfy-Org/ComfyPythonSDK@39dcce0cd89043f1bd71e862241d282eb8501384 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/Comfy-Org
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@39dcce0cd89043f1bd71e862241d282eb8501384 -
Trigger Event:
release
-
Statement type:
File details
Details for the file comfy_sdk-0.1.1-py3-none-any.whl.
File metadata
- Download URL: comfy_sdk-0.1.1-py3-none-any.whl
- Upload date:
- Size: 34.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b39525471904ab74a033b12db38fd8d153a6e1882009a2635f4e28777015bc70
|
|
| MD5 |
44de7b708627a6abbbba705185fcb801
|
|
| BLAKE2b-256 |
ca14a544f5d6790f1bf81af174ddc888e2b1275ddbb170d5ba04670b5f2a30d5
|
Provenance
The following attestation bundles were made for comfy_sdk-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on Comfy-Org/ComfyPythonSDK
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
comfy_sdk-0.1.1-py3-none-any.whl -
Subject digest:
b39525471904ab74a033b12db38fd8d153a6e1882009a2635f4e28777015bc70 - Sigstore transparency entry: 2215063027
- Sigstore integration time:
-
Permalink:
Comfy-Org/ComfyPythonSDK@39dcce0cd89043f1bd71e862241d282eb8501384 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/Comfy-Org
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@39dcce0cd89043f1bd71e862241d282eb8501384 -
Trigger Event:
release
-
Statement type: