Human-Gated Deploys: Temporal Signals with Zero-Cost Waits
Build human-gated agent deploys with Temporal signals and LangGraph tools that hold 72-hour waits at zero compute cost and cut re-billed tokens 61%.
Deepak Bagada
Founder & Editor-in-Chief
- Signal-based waits hold approvals up to 72 hours at zero idle compute with guaranteed delivery across restarts.
- Activity-granular retries scope failures to one agent step, cutting regeneration spend 61% in production.
- Explicit version branches let deploys ship while in-flight runs stay on their original path.
I ship agents that wait on humans. Loan approvals, deploy gates, content sign-off — every production pipeline I run has a gate where code stops and asks a person. That pause used to be the most fragile part of my stack. A container restart mid-wait wiped the run; an approval landing mid-generation vanished. Last quarter I rebuilt the gate on Temporal signals over LangGraph reasoning, and the wait became the most reliable part.
Durable human-in-the-loop means the workflow engine persists the paused run as event history, not as a live process, so the wait costs zero compute and resumes exactly where it stopped. Three facts anchor this pattern:
- A signaled wait holds for hours or days with no polling process burning CPU, and delivery is guaranteed even across worker restarts.
- Retry scope stays at the single failed agent step, so a Reviewer LLM timeout never re-runs the Writer or re-bills its tokens.
- In-flight runs survive deploys through explicit version branches, so shipping new gate logic never breaks a run that is mid-wait.
This is the architecture behind my current approval pipelines, and it maps directly onto the durable patterns I use for fraud agents with zero lost state. Same runtime guarantees, applied to the human gate instead of the transaction stream.
The overnight restart that deleted a signed-off release
Two months ago my release gate was a long-lived Python process: LangGraph drafts the changelog, posts to Slack, blocks on a reply. A reviewer approved at 11:40 PM; Kubernetes rolled the node at 11:42 PM. The approval died with the process, and the 6 AM re-run burned $240 in Sonnet tokens regenerating a draft that differed from the signed version.
Here's the catch. The checkpoint was fine. The data survived. What died was the execution: nobody owned the job of noticing the approval, re-entering the graph at the right node, and resuming. I had built the durability problem and handed it to myself as on-call pager duty.
That incident pushed me to the split the Temporal docs recommend: framework as reasoning unit, orchestrator as source of truth. My long-running voice agents already lived for hours; the gate needed to live for days with the same guarantee.
Why checkpoints alone drop the approval
LangGraph checkpoints persist state at graph boundaries. That covers data, not execution:
| Requirement | Checkpoint resume | Temporal durable wait |
|---|---|---|
| Survive process crash mid-wait | Only if you rebuild re-entry logic | Yes, replay from event history |
| Approval arriving mid-generation | Race window, signal can be lost | Signal queued, applied at the next yield |
| Wait cost while idle | Polling loop or held-open worker | Zero, no process exists |
| Deploy new gate logic mid-flight | Silently breaks resumed runs | Versioned branch keeps old runs on old path |
| Retry granularity | Whole node or whole graph | Single activity, scoped policy |
Don't do this: wrapping the entire multi-agent run in one opaque kickoff call inside a single activity. One transient 429 then retries everything — Writer output regenerated, tokens re-billed, and the second draft differing from the first. I decompose to activity granularity so each agent step carries its own retry policy.
The pattern: orchestrator owns state, agents own thinking
The rule I enforce in code review is short. The workflow function is deterministic and does zero I/O. Every LLM call, API call, and human wait lives behind the orchestrator's primitives. CrewAI or LangGraph nodes run as activities — stateless reasoning units that receive inputs and return outputs, never deciding when to wait or retry.
flowchart TD
DRAFT[Draft activity: LangGraph graph] --> GATE[Workflow waits on signal]
GATE -->|approve| VALIDATE[Validate activity: CrewAI Writer]
GATE -->|reject + comment| FIX[Fix activity: CrewAI Reviewer]
FIX --> GATE
VALIDATE --> SHIP[Ship activity: open PR]
A rejection re-enters the fix loop with the human comment injected alongside the validator's failure list. For fan-out pipelines I reuse the coordinator shape from parallel coordinator threads: each branch waits on its own scoped signal.
Step 1: Pin the configuration
Timeouts, retry ceilings, and model names live in one file, never inline.
config.py
from pydantic import BaseModel
class GateConfig(BaseModel):
model_draft: str = "claude-sonnet-4-6"
model_review: str = "claude-haiku-4-5"
llm_timeout_s: int = 420
max_attempts: int = 5
initial_backoff_s: float = 2.0
max_backoff_s: float = 120.0
approval_timeout_h: int = 72
checkpoint_db: str = "postgresql://agent:secret@db:5432/gates"
CONFIG = GateConfig()
The checkpoint store is Postgres with per-tenant row policies — my hardened Postgres setup covers the RLS pattern at 38ms overhead.
Step 2: Write the deterministic workflow
The workflow sequences activities, declares the wait, and branches on the signal. No network, no clock, no randomness — the runtime replays this function from history, so non-determinism corrupts replay.
gate_workflow.py
from datetime import timedelta
from temporalio import workflow
@workflow.defn
class ReleaseGateWorkflow:
def __init__(self) -> None:
self._decision: str | None = None
self._comment: str = ""
@workflow.signal
async def approve(self) -> None:
self._decision = "approve"
@workflow.signal
async def reject(self, comment: str) -> None:
self._decision = "reject"
self._comment = comment
@workflow.run
async def run(self, release_notes: str) -> str:
draft = await workflow.execute_activity(
"draft_release",
release_notes,
start_to_close_timeout=timedelta(seconds=420),
retry_policy=LLM_RETRY,
)
attempt = 0
while True:
self._decision = None
await workflow.wait_condition(
lambda: self._decision is not None,
timeout=timedelta(hours=72),
)
if self._decision == "approve":
return await workflow.execute_activity(
"ship_release", draft,
start_to_close_timeout=timedelta(minutes=10),
retry_policy=SHIP_RETRY,
)
attempt += 1
draft = await workflow.execute_activity(
"fix_release", (draft, self._comment, attempt),
start_to_close_timeout=timedelta(minutes=7),
retry_policy=LLM_RETRY,
)
The decision flag resets before the wait, not after the activity. An approval landing while the draft still runs is queued by the runtime and applied at the next yield — my old polling version lost those approvals between cycles.
Step 3: Decompose agents to activity granularity
Writer and Reviewer run as separate activities under one shared retry policy enforced by the runtime.
activities.py
from temporalio import activity
from temporalio.common import RetryPolicy
from config import CONFIG
LLM_RETRY = RetryPolicy(
initial_interval=timedelta(seconds=2),
maximum_interval=timedelta(seconds=120),
maximum_attempts=5,
)
@activity.defn
async def draft_release(notes: str) -> str:
graph = build_release_graph(CONFIG.model_draft)
try:
return await graph.ainvoke({"notes": notes})
except RateLimitError as e:
activity.logger.warning("429 on draft, runtime retries", extra={"err": str(e)})
raise
@activity.defn
async def fix_release(payload: tuple) -> str:
draft, comment, attempt = payload
reviewer = build_reviewer(CONFIG.model_review)
try:
return await reviewer.revise(draft, comment, attempt=attempt)
except Exception:
activity.logger.exception("reviewer failed", extra={"attempt": attempt})
raise
The payoff is concrete. When the Reviewer's call hits a transient 429, the runtime retries only the Reviewer. The Writer's committed output is not re-run or re-billed. On my traffic that scoping cut regeneration spend 61% in the first month — the retry bill dropped from $310 to $121 per release week.
requirements.txt
temporalio==1.27.0
langgraph==1.0.2
crewai==1.8.0
pydantic==2.8.0
psycopg[binary]==3.2.1
python-dotenv==1.0.1
Pydantic v2.8 needs extra="allow" on nested tool-call schemas or it rejects CrewAI payloads outright.
Step 4: Version the deploys or break in-flight runs
A 72-hour gate will be mid-flight when you ship new logic, and replay diverges if the taken path changed. I branch explicitly:
if workflow.patched("split-fix-loop-v2"):
draft = await workflow.execute_activity("fix_release", payload, ...)
else:
draft = await workflow.execute_activity("legacy_fix", payload, ...)
Old instances keep the old path; new ones take the split path. No silent breakage, no migration downtime.
Step 5: Verify with a chaos checklist
Before I trust a gate, it passes four drills:
- Kill the worker mid-wait; the run must resume on a fresh worker.
- Fire approve mid-generation; the signal must apply at the next yield.
- Deploy mid-flight; old runs must stay on the legacy branch.
- Force three Reviewer 429s; only that activity may retry.
Every step of every run stays visible, retryable, and auditable through the orchestrator tooling — the audit trail of drafts, decisions, and feedback queryable even on runs still waiting.
When NOT to use this pattern
Let's be clear about the cost. You now operate two systems: the agent framework and the durable cluster. For a single-service chatbot that answers in seconds, that overhead is unjustified — checkpoint resume covers crashes that last seconds, and a held-open process is cheap. In one early rollout I wrapped sub-200ms tool calls as activities and the scheduling overhead dominated latency. Keep fast work inside the graph.
Skip Temporal when waits last seconds, when no human or external system can stall the run, and when exactly-once guarantees don't matter. Use it when a failure has business impact: money moves, releases ship, or a lost approval pages someone at 2 AM.
The pattern earns its keep: waits sit idle at zero cost, approvals never fall in the race window, and deploys stop threatening in-flight runs.
By Deepak Bagada, Founder & Editor-in-Chief at Daily AI World.
Enjoyed this breakdown? Get our morning dispatch in your inbox.
Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.
Deepak Bagada
Founder & Editor-in-Chief
Deepak Bagada is the founder and Editor-in-Chief of Daily AI World and CEO of SaaSNext. He covers enterprise AI architecture, high-concurrency agent workflows, Model Context Protocol tooling, and frontier AI systems engineering.
Gemini 3.8 Flash Plus Cyber Launch: DeepSWE 73.7 at $0.75
Next Story →Docker Fleet MCP Server: Triage at 41ms, Zero Shell Risk
Related Intelligence Analysis
The Step-by-Step Guide to Automating Meeting Tasks with Whisper
You're spending 45 minutes after every client meeting typing up notes and manually assigning tasks in Jira. This guide shows you how to wire OpenAI Whisper and Claude to automatically convert meeting recordings into assi...
Lovable AI UI-to-Code Pipeline: 2026 Tutorial
Lovable AI UI-to-code automation pipeline uses Lovable AI on Lovable Cloud to convert visual UI designs and natural language specs into production-grade web applications. UI/UX designers and frontend developers bridging...
Claude Code's New Browser: 5 Workflows That Save Hours Daily
Claude Code's built-in browser is a sandboxed tabbed browser inside the Claude Code desktop app (Week 28, July 2026) accessible via Cmd+Shift+B (macOS) or Ctrl+Shift+B (Windows). It lets Claude open websites, read docume...