Documentation Index
Fetch the complete documentation index at: https://docs.awaithumans.dev/llms.txt
Use this file to discover all available pages before exploring further.
idempotency_key is the most important parameter most users won’t touch. It’s how await_human() stays safe to retry.
The model
awaithumans follows Stripe’s idempotency model: same key, same task, always. The first call with a givenidempotency_key creates the task. Every subsequent call with that key — while the task is in flight, after it completes, after it times out, after it’s cancelled, after the verifier exhausts — returns the same task. Different result, same row.
This is what you want. It means:
- ✅ Calling
await_human(...)twice with the same key while a human is reviewing → returns the same task (in-flight dedup) - ✅ Network blip during task creation → safe to retry without creating a duplicate
- ✅ Agent process crashes mid-
await_human()and the human completes during the outage → re-invoking on restart returns the stored response and yourif decision.approved:block runs - ✅ The original timed out → re-invoking returns the timed-out task; your code gets a typed
TaskTimeoutError, not a phantom new task
What happens under the hood
When you callawait_human(...):
- Server looks up tasks with this key — any status, terminal or not.
- Found? Return the existing task. The SDK long-polls it. If the status is already terminal the long-poll returns immediately with the stored response or the appropriate typed error.
- Not found? INSERT a fresh task.
- Race condition (two concurrent INSERTs with the same new key)? The unique constraint catches it; the loser re-fetches the winner.
tasks.idempotency_key column has a partial unique index gated on non-terminal status, kept from earlier versions for race safety on concurrent insertion. With the application-layer lookup now covering terminal rows, the index never gets exercised on the recovery path — the lookup returns the existing row before any INSERT is attempted.
Default keys
If you don’t passidempotency_key=, the SDK derives one from (task, payload):
temporal:{hash}— so operators can filter Temporal-driven tasks at the dashboardlanggraph:{hash}— same idea for LangGraph
When to pass an explicit key
(task, payload) default is content-addressed, so two refund requests for the same amount + same customer dedup, even if they’re a month apart. That’s usually wrong.
A natural-key approach (refund:{order_id}) makes intent explicit:
- Same
order_idwhile task is active → same task (intentional dedup) - Same
order_idafter task closed → same task (intentional recovery — see What about retries) - Different
order_id→ different task
Re-triggering a review
When you genuinely want to start a fresh review for the same logical event — for example, a refund timed out yesterday and you want to surface it again today — encode that intent in the key:refund:{order_id} or refund:{order_id}:retry-0) stays in the audit log; the new key creates a new task that the human can act on independently. Same approach Stripe recommends for retried payment intents.
What about retries from the agent’s side?
Two cases. Both work; the second is the one that distinguishes awaithumans from a naive long-poll. Crash before the human completes. Agent re-invokes with the same key, the server returns the still-in-flight task, the SDK resumes the long-poll. The human only ever sees one ticket. No duplicate work. Crash during human review, human completes during the outage, agent restarts. Agent re-invokes with the same key, the server returns the now-COMPLETED task with the stored response, the SDK skips long-polling and returns the typed result immediately. Your if decision.approved: block runs as if the agent had been alive the whole time.
This is the entire point. await_human() is the durable-but-with-humans equivalent of Temporal’s await activity() — your retry loop just works.
What about the verifier?
When the verifier rejects an attempt, the task goes toREJECTED (non-terminal). If your agent calls await_human(...) again with the same key, it returns the same task — the human can still resubmit and run another verifier attempt.
If the verifier exhausts attempts → VERIFICATION_EXHAUSTED (terminal). Subsequent calls with the same key return the same exhausted task and the SDK raises VerificationExhaustedError. To retry the whole pipeline with a fresh attempt counter, pass a new key (see Re-triggering a review).
Example: a refund pipeline
await_human() calls are safe to retry from a workflow restart. Stripe’s own idempotency key handles the side-effect side. The whole pipeline is recoverable.