If you run ComfyUI as a headless backend — queuing jobs from a Python script, a CI pipeline, or a production product — you already know the worst part: you never know when a job actually finished. You either poll /history in a loop (wasteful), leave a browser tab open (wasteful), or find out hours later that a 20-minute video render died at second three.
What you actually want is a webhook: ComfyUI pushes an HTTP POST to your endpoint the instant a workflow completes or fails. This article walks through the three realistic ways to get there, what they cost, and which one fits your setup. Everything here is drawn from the official ComfyUI codebase and the public repositories of the nodes named below — I’ll link each source so you can verify it independently rather than take my word for it.
The problem: ComfyUI has no built-in webhook, only WebSockets
ComfyUI does not ship a first-party webhook. What it does ship is a WebSocket endpoint at /ws?clientId=... that streams events as a job runs. The event you care about is one of:
executing— fires when a node starts, withprompt_idandnode.execution_cached— a node returned from cache.execution_error— a node failed; carries the exception message.executed— a node produced output (this is whereimagesshow up for SaveImage nodes).progress— step/iteration progress within a node.
The canonical pattern is to connect a WebSocket client, watch for executing with node === null (which signals the whole prompt finished), and then fetch results from /history/{prompt_id}. This is exactly what the official ComfyUI repository demonstrates in its script_examples folder, and what many production harnesses build on.
The catch: WebSockets are a long-lived persistent connection. That’s awkward in serverless/async contexts, and it means your process has to stay alive and connected the whole time. A webhook inverts the relationship — ComfyUI calls you, so you don’t have to hold a connection open.
Related: if you’re already using WebSockets to detect completion, make sure you’re reading the right signal. We covered the common mistake of polling queue_remaining instead of watching executing in this earlier post.
Option 1: Webhook custom nodes (the 2-minute fix)
The fastest way to get a webhook is to bolt a custom node onto the end of your workflow graph. The node fires when its input is reached, which — because execution is topological — means “after everything upstream finished.” Two solid options:
ComfyUI-WebhookNotifier (iSuneast/ComfyUI-WebhookNotifier) ships two nodes:
Webhook Notifier— connects to anIMAGEoutput as its trigger.Webhook Notifier (Any)— accepts any input type (IMAGE,STRING,LATENT, video filenames, etc.), which is what you want if your final node emits something other than an image.
Install is a standard custom_node clone into custom_nodes/ plus a requirements.txt install, then restart. It sends a POST with custom JSON after execution.
ComfyUI-SendToDiscord (gmorks/ComfyUI-SendToDiscord) is purpose-built for Discord: you create a webhook under Server Settings → Integrations → Webhooks, paste the https://discord.com/api/webhooks/... URL into the node (there’s a dedicated “Set Discord Webhook” setup node in some variants), and the node posts the finished image straight into a channel.
Pros: drop-in, no code on your side, works in the GUI or headless. Cons: it only fires for workflows where you remembered to add the node, and it fires on graph reach, not on a global “job failed” event — a node error usually means the notifier node never executes, so a hard failure can go silent. That’s the single biggest trap with node-based webhooks and worth repeating: if the workflow errors before the notifier node, you get no notification.
Option 2: Roll your own webhook from the WebSocket stream
For anything beyond casual alerts, you want a small daemon that always knows job state — success or failure — and posts to your webhook accordingly. The structure is three parts:
- Subscribe to
/ws?clientId=my-daemon. - Watch for
executingwithnode === null→ a prompt finished; watchexecution_error→ a prompt failed (you getprompt_id,node_id, and the exception). - POST to whatever endpoint you control — your backend, Discord, Slack, Telegram.
A minimal Python skeleton (documentation-style, not claimed as a tested artifact):
import json, requests, websocket
WEBHOOK = "https://your-endpoint/hook"
def on_message(ws, raw):
msg = json.loads(raw)
if msg.get("type") == "executing":
data = msg["data"]
if data.get("node") is None: # whole prompt done
requests.post(WEBHOOK, json={
"event": "finished", "prompt_id": data["prompt_id"]})
elif msg.get("type") == "execution_error":
requests.post(WEBHOOK, json={
"event": "error", "detail": msg["data"]})
ws = websocket.WebSocketApp("ws://127.0.0.1:8188/ws?clientId=daemon",
on_message=on_message)
ws.run_forever()
The ComfyUI repo’s script_examples/websockets_api_example.py is the reference for the exact event payload structure — I’d point you there rather than trust my paraphrase of field names, since the schema shifts between versions.
The webhook receiver side should follow the standard pattern: validate the payload, respond 200 fast, and do any heavy work asynchronously so you don’t block the (already fragile) notification path.
Option 3: Use a hosted/webhook-native wrapper
If ComfyUI is running in the cloud or behind a managed API layer, that layer often already emits webhooks — you get notifications for free without touching custom nodes. For example, Replicate supports webhook callbacks for async completion, and several ComfyUI-as-a-service platforms expose the same. There’s also a community wrapper, SaladTechnologies/comfyui-api, whose server can forward ComfyUI WebSocket events to a configured webhook — effectively turning Option 2 into a config setting instead of code you maintain.
This is the lowest-effort path if you’re already on a managed stack; just confirm the webhook fires on failure as well as success, because the silent-failure problem from Option 1 applies to any notification path, not just custom nodes.
Common pitfalls to avoid
- Silent failures. Node-based webhooks (Option 1) won’t fire if the graph errors before the notifier. If you care about failures, use the WebSocket/daemon approach (Option 2) so
execution_errorreaches you. - Wrong completion signal. Don’t poll
queue_remainingor countexecutedevents; the reliable end-of-prompt marker isexecutingwithnode === null. (Also covered here.) - Blocking the webhook. If you POST synchronously and your endpoint is slow, you can stall the node or daemon. Respond
200immediately and process asynchronously. - Security. A webhook URL is a bearer secret. Don’t log it, don’t commit it, and consider requiring a shared token/header so a random POSTer can’t spam your endpoint.
- Forgotten node on new workflows. If you go the custom-node route, make it part of your template — otherwise every new workflow silently lacks notifications.
Which one should you pick?
Casual, GUI user who just wants a Discord ping with the finished image → ComfyUI-SendToDiscord or WebhookNotifier (Option 1).
Builders running headless jobs from scripts, who need to know about failures too → the WebSocket daemon (Option 2), or a wrapper that forwards events (Option 3).
Already on Replicate / a managed ComfyUI API → use the platform webhook (Option 3) and verify failure coverage.
The through-line in all three: the hard part isn’t sending the webhook, it’s not missing the failures. Pick the path that reports errors as loudly as successes, and you’ll stop babysitting the queue.
Leave a Reply