Temporal workflow signal from an AI video webhook
Start the video job in a Temporal activity, wait on a signal your webhook receiver sends by workflow id, and keep a timer that reads the run anyway.

A Temporal workflow signal is an asynchronous message that a client sends to a running workflow execution, addressed by its workflow id. The workflow handles it in a signal handler, which can change the workflow's state but can't return a value, and the sender doesn't wait for the workflow to process it. That makes a signal the natural way to wake a workflow when an outside job, such as an AI video render, sends its webhook.
Here the outside job is a Sume Format run: an activity starts it, your webhook receiver signals the workflow when Sume POSTs the result, and a timer covers runs that never POST. Temporal facts come from its message passing, Python message passing, Workflow Definition, Activities, and Retry Policies pages; Sume facts from Create a run, Runs and results, and Run webhooks. All were read on 2026-09-27. Sume has no Temporal integration: your activities call its HTTPS API.
Why does the API call go in an activity, not the workflow?
Temporal requires workflow code to be deterministic so it can be replayed, and tells you to put API calls in activities, which run outside the replay path and are retried automatically. The default retry policy starts at a 1-second interval with a backoff coefficient of 2.0 and unlimited attempts, which is why Temporal recommends that activities be idempotent.
For a paid API that matters: a retried activity must not buy a second video. Send Sume an Idempotency-Key built from the same business id as the workflow id. The same key with the same body returns 200 and the original run, with no second run and no second charge; the same key with a different body is 409 idempotency_conflict, so build the whole body from that id too.
start_run(order_id)POSTs/v1/formats/acme/product-promo/runswithIdempotency-Key: video-<order_id>, the string you also use as the workflow id, and acommunication.webhook_urlsuch ashttps://hooks.example.com/sume?workflow_id=video-<order_id>. It returnsdata.id.read_run(run_id)GETs/v1/format-runs/{run_id}and returns onlystatus,primary_output_url, anderrorfrom the receipt.
How does the workflow wait for the signal?
It starts the run, then blocks on workflow.wait_condition until the signal handler has recorded that run's id. With a timeout, wait_condition throws asyncio.TimeoutError when time runs out, and the workflow reads the run once either way:
import asyncio
from datetime import timedelta
from temporalio import workflow
@workflow.defn
class VideoWorkflow:
def __init__(self) -> None:
self.ended_run_id: str | None = None
@workflow.signal
def run_ended(self, run_id: str) -> None:
self.ended_run_id = run_id # a repeated delivery sets the same value
@workflow.run
async def run(self, order_id: str) -> dict:
t = timedelta(seconds=30)
run_id = await workflow.execute_activity(start_run, order_id, start_to_close_timeout=t)
try:
await workflow.wait_condition(
lambda: self.ended_run_id == run_id, timeout=timedelta(minutes=100))
except asyncio.TimeoutError:
pass # no webhook came: canceled, or every delivery refused
return await workflow.execute_activity(read_run, run_id, start_to_close_timeout=t)How does the webhook receiver send the signal?
First it verifies Sume's signature on the raw body: refuse an empty signing secret, then check HMAC-SHA256 over <timestamp>.<raw_body>, a match on any sume-v1= entry in constant time, and a five-minute timestamp window. Python webhook HMAC verification in FastAPI and Django has that code. Then it signals by workflow id. A receiver usually doesn't import the workflow class, so it uses Temporal's untyped get_workflow_handle and passes the signal's name:
signalreturns when the Temporal server accepts it, without waiting for it to be delivered to the workflow, so the receiver can answer2xxinside the 10 seconds Sume gives each attempt.- Signals reach only executions that haven't closed, and a failed signal raises an
RPCError, with statusNOT_FOUNDwhen the workflow doesn't exist. Answer2xxfor a workflow that's gone, or Sume retries, up to 10 attempts; for other errors, a non-2xxanswer gets you a retry. - Each signal is recorded in the workflow's Event History, and the handler only stores a value, so a webhook that lands before
wait_conditionruns is not lost. - Send only the run id. A Sume receipt can reach 1 MiB, while Temporal's self-hosted defaults warn at 256 KB for a payload;
read_runfetches the receipt instead.
from temporalio.client import Client
async def on_verified_delivery(client: Client, workflow_id: str, event: dict) -> None:
handle = client.get_workflow_handle(workflow_id) # from ?workflow_id=
await handle.signal("run_ended", event["run_id"])Should the receiver use Signal-With-Start?
No. Signal-With-Start sends a signal and starts the workflow execution if it isn't already running; in Python you pass start_signal to start_workflow. From a webhook receiver, that turns a late or repeated delivery into a new workflow for a video that already exists. Keep it for messages that should create work, and send results with a plain signal.
How long should the workflow wait?
Past Sume's own deadline: a run still in progress is force-finalized as failed 90 minutes after created_at, so with a 100-minute timeout, the run has ended by the time the workflow reads it. A timeout creates a Temporal timer, and timers are persisted, so the wait survives a worker restart. The timer also catches the runs that never signal:
| What happened to the run | Webhook | What the workflow sees |
|---|---|---|
| Completed or failed | One signed POST, retried up to 10 attempts | The signal |
| Canceled | Never sent | The timeout; read_run shows canceled |
| Your endpoint refused all 10 attempts | Delivery failed; the run is unchanged | The timeout; read_run has the result |
Sources
- Create a run
- Runs and results
- Run webhooks
- Temporal: Workflow message passing (read 2026-09-27)
- Temporal: Workflow message passing, Python SDK (read 2026-09-27)
- Temporal Python API: temporalio.workflow (read 2026-09-27)
- Temporal: Workflow Definition (read 2026-09-27)
- Temporal: What is a Temporal Activity? (read 2026-09-27)
- Temporal: Retry Policies (read 2026-09-27)
- Temporal: Timers, Python SDK (read 2026-09-27)
- Temporal: Self-hosted defaults and limits (read 2026-09-27)
Related posts
More in Integrations
- Test Sume webhooks locally with ngrok or a Cloudflare Tunnel
Sume refuses localhost webhook URLs. Expose your handler with ngrok or a Cloudflare Quick Tunnel, send a signed test, then redeliver real events.
- Text-to-video API in Python: submit, poll, and download
Call Sume's text-to-video API from Python with Requests: POST /v1/videos, poll with timeouts, then stream the MP4 the content route redirects to.
- Transcribe audio to text in n8n with an HTTP Request node
Transcribe audio to text in n8n: send the recording's public URL from an HTTP Request node, loop a Wait node until the job ends, then map the text.
- How to upload a file to an S3 bucket from a URL in Python
Stream the URL's response straight into boto3's upload_fileobj, with no temp file. For a generated video, copy on the webhook and check the bytes.
Written by Sume