Skip to main content
The Temporal adapter is signal-based. Inside a workflow, await_human() registers a signal handler and workflow.wait_condition() parks the workflow. Outside, your web server receives the awaithumans completion webhook and signals the workflow back to life.

Install

Workflow side

The workflow.unsafe.imports_passed_through() context tells Temporal’s sandbox the import is safe — required because the awaithumans adapter module imports things the sandbox would otherwise block.

Web-server side

Your web server hosts the callback receiver. The awaithumans server POSTs there when the human completes; you signal the workflow back.
dispatch_signal() does the security-critical bits (HMAC verify, payload parse, signal routing). The route is just web-framework glue.

Constructing callback_url

The workflow ID has to round-trip through the awaithumans server. The simplest pattern: bake it into the callback URL as a query param.
For local dev, expose the receiver via a tunnel so the awaithumans server can reach it:

Error contract

The adapter maps webhook status to typed Python exceptions: Catch them in the workflow body to recover (retry with a different reviewer, escalate, etc.).

Why this works under failure

  • Worker dies during the await — Temporal restarts the workflow; await_human() re-registers the signal handler with the same idempotency key. The awaithumans server returns the existing task (idempotency dedup). When the human eventually completes, the signal fires on the live worker.
  • Callback server is down when the human submits — the awaithumans server’s outbound webhook fails-loudly in its logs but doesn’t retry. The workflow times out at timeout_seconds, raises TaskTimeoutError, and the operator sees the abandoned task in the dashboard.
  • awaithumans server restarts — tasks are persisted; on restart the timeout scheduler resumes and the dashboard reconnects.

End-to-end example

The examples/temporal/ directory in the repo is runnable on a laptop in three terminal windows. See the README in that folder for the full setup.

Cross-language

The TypeScript adapter at awaithumans/temporal produces the same wire format. A Python workflow can have its callback handler resume from a TypeScript web server and vice versa — the HMAC derivation is identical (HKDF over PAYLOAD_KEY with the same salt + info bytes).

Common gotchas

  • AWAITHUMANS_PAYLOAD_KEY mismatch — the HMAC signing key derives from this. If the awaithumans server and the callback receiver have different keys, signatures never verify. Use the same value on both processes.
  • callback_url not reachable — the awaithumans server logs show Webhook delivery failed task=… url=…: …. Most often a tunnel/firewall issue.
  • Duplicate idempotency key — two await_human() calls in the same workflow with the same (task, payload) get the same key by default. Pass idempotency_key= explicitly to disambiguate. See Idempotency.

Where to next