Skip to main content
Subscribe

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

Deepak Bagada

Founder & Editor-in-Chief

Sep 20, 2026 Published
|
Sep 20, 2026 Updated
|
7 Minutes Reading Time
Core Takeaways for Founders & Builders
  • 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:

  1. Kill the worker mid-wait; the run must resume on a fresh worker.
  2. Fire approve mid-generation; the signal must apply at the next yield.
  3. Deploy mid-flight; old runs must stay on the legacy branch.
  4. 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 , Founder & Editor-in-Chief at Daily AI World.

Executive Briefing

Enjoyed this breakdown? Get our morning dispatch in your inbox.

Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.

🎉 Thank You for Subscribing!

Frequently Asked Questions
Checkpoints persist data at graph boundaries; durable execution persists the run itself as replayable event history. After a crash the runtime reconstructs execution on any worker and resumes exactly where it stopped, including mid-wait signal state, with no custom re-entry code.
Signals are push-based and backed by event history. An approval arriving while an activity still runs is queued by the runtime and applied when control returns to the workflow, so there is no poll interval and no window where the decision can be missed.
Only the failed activity retries under its own policy, so a Reviewer timeout re-runs just the Reviewer while the Writer output stands. Separate the Writer and Reviewer into independent activities instead of one opaque multi-agent call to get this scoping.
Use workflow.patched() to branch: in-flight instances keep the legacy code path while new runs take the refactored path. Without explicit versioning, replay diverges from stored history and mid-flight workflows break on deploy.
Deepak Bagada
Author Profile

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.

Related Intelligence Analysis

Research Breakdown AI Workflows

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...

Deepak Bagada Deepak Bagada
9m read
Research Breakdown AI Workflows

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...

Deepak Bagada Deepak Bagada
8m read
Breaking AI Workflows

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...

Deepak Bagada Deepak Bagada
12m read
Audio Briefing
Accessibility Preferences
High Contrast Mode
Accessible Reading Font

Keyboard Shortcuts

Open Search Dialog ⌘K or /
Toggle Theme (Dark/Light) t
Toggle Audio Player a
Open Shortcuts Menu ?
Close Active Dialog Esc

Cookie & Privacy Preferences

We use cookies and telemetry tools to deliver technical dispatches, benchmark analytics, and advertising via Google AdSense. Review our Privacy Policy.