By Sagar Shankaran, Founder of CallSphere
Discover how to build resilient long-running agent workflows using durable execution engines like Temporal and Celery, with activity retries, saga patterns, and persistent state across process restarts.
Key takeaways
Most agent frameworks treat each invocation as a short-lived function call. The agent receives a prompt, calls some tools, and returns a result — all within a single process lifetime. But real-world agent tasks often take minutes, hours, or even days. A due diligence agent might need to collect data from 50 sources over several hours. A monitoring agent runs indefinitely.
When these long-running tasks crash — and they will — you lose all progress. The agent has no memory of which steps completed, what intermediate results were produced, or where it left off. This is where durable execution comes in.
Durable execution means that workflow state survives process failures. If the worker crashes after completing step 3 of 10, it resumes at step 4 when restarted — not step 1. Two popular approaches in the Python ecosystem are Temporal and Celery.
flowchart LR
INPUT(["User intent"])
PARSE["Parse plus<br/>classify"]
PLAN["Plan and tool<br/>selection"]
AGENT["Agent loop<br/>LLM plus tools"]
GUARD{"Guardrails<br/>and policy"}
EXEC["Execute and<br/>verify result"]
OBS[("Trace and metrics")]
OUT(["Outcome plus<br/>next action"])
INPUT --> PARSE --> PLAN --> AGENT --> GUARD
GUARD -->|Pass| EXEC --> OUT
GUARD -->|Fail| AGENT
AGENT --> OBS
style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
style OBS fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style OUT fill:#059669,stroke:#047857,color:#fff
Temporal separates workflows (orchestration logic) from activities (actual work). The workflow is deterministic and replayed on failure. Activities are the non-deterministic side-effecting operations.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
from temporalio import workflow, activity
from datetime import timedelta
import asyncio
@activity.defn
async def fetch_source_data(source_url: str) -> dict:
"""Activity: fetch data from a single source."""
# This runs in a worker and can be retried independently
import httpx
async with httpx.AsyncClient() as client:
response = await client.get(source_url, timeout=30)
return response.json()
@activity.defn
async def analyze_with_llm(data: dict) -> str:
"""Activity: send collected data to an LLM for analysis."""
from openai import AsyncOpenAI
client = AsyncOpenAI()
response = await client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "Analyze the following data."},
{"role": "user", "content": str(data)},
],
)
return response.choices[0].message.content
@workflow.defn
class ResearchWorkflow:
"""Durable workflow that survives crashes."""
@workflow.run
async def run(self, sources: list[str]) -> str:
# Each activity call is persisted to Temporal history
collected = []
for source in sources:
data = await workflow.execute_activity(
fetch_source_data,
source,
start_to_close_timeout=timedelta(minutes=2),
retry_policy=RetryPolicy(maximum_attempts=3),
)
collected.append(data)
# If the worker crashes here, it resumes AFTER the loop
analysis = await workflow.execute_activity(
analyze_with_llm,
{"sources": collected},
start_to_close_timeout=timedelta(minutes=5),
)
return analysis
If the worker crashes after fetching 8 of 10 sources, Temporal replays the workflow history. It skips the 8 completed activities (their results are stored) and resumes fetching source 9.
When a long workflow fails partway through, you often need to undo earlier steps. The saga pattern pairs each action with a compensation:
from dataclasses import dataclass
from typing import Callable, Awaitable
@dataclass
class SagaStep:
action: Callable[..., Awaitable]
compensation: Callable[..., Awaitable]
name: str
class SagaOrchestrator:
def __init__(self):
self.completed_steps: list[SagaStep] = []
async def execute(self, steps: list[SagaStep], context: dict):
for step in steps:
try:
await step.action(context)
self.completed_steps.append(step)
except Exception as e:
print(f"Step '{step.name}' failed: {e}")
await self.compensate()
raise
async def compensate(self):
"""Roll back completed steps in reverse order."""
for step in reversed(self.completed_steps):
try:
await step.compensation({})
except Exception as comp_error:
print(f"Compensation for '{step.name}' failed: {comp_error}")
If Temporal feels heavyweight, Celery provides task queuing with retries and result persistence:
from celery import Celery, chain
app = Celery("agent_tasks", broker="redis://localhost:6379/0")
app.conf.result_backend = "redis://localhost:6379/1"
@app.task(bind=True, max_retries=3, default_retry_delay=60)
def fetch_data(self, source_url: str):
try:
import httpx
response = httpx.get(source_url, timeout=30)
return response.json()
except Exception as exc:
self.retry(exc=exc)
@app.task
def analyze_data(data: dict):
# LLM analysis step
return {"analysis": "completed", "data": data}
# Chain tasks: fetch then analyze
pipeline = chain(
fetch_data.s("https://api.example.com/data"),
analyze_data.s(),
)
result = pipeline.apply_async()
Regardless of the engine, persist your agent state at meaningful checkpoints:
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
import json
from pathlib import Path
class CheckpointManager:
def __init__(self, workflow_id: str, storage_dir: str = "./checkpoints"):
self.path = Path(storage_dir) / f"{workflow_id}.json"
self.path.parent.mkdir(parents=True, exist_ok=True)
def save(self, state: dict):
self.path.write_text(json.dumps(state, default=str))
def load(self) -> dict | None:
if self.path.exists():
return json.loads(self.path.read_text())
return None
def clear(self):
self.path.unlink(missing_ok=True)
Use Temporal when your workflow has complex branching, long-running wait states (hours or days), or when you need the replay guarantee that ensures exactly-once semantics. Use Celery when you need a simple task queue with retries and your workflows are linear chains of tasks without complex orchestration logic.
Temporal records every activity completion in its event history. During replay, the workflow code runs again, but when it hits execute_activity, Temporal checks the history. If that activity already completed, it returns the stored result immediately instead of dispatching it to a worker. This makes replay deterministic and fast.
The LLM call becomes orphaned — the API may still process it, but the result is lost. Temporal handles this with activity timeouts and retries. When the worker restarts and replays, it re-dispatches the activity. To avoid paying for the orphaned call, set short start_to_close_timeout values and implement idempotency on your LLM wrapper so duplicate calls return cached results.
#Temporal #Celery #DurableExecution #WorkflowEngines #Python #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
Step-by-step build of a working agent with the OpenAI Agents SDK — Agent class, tools, handoffs, tracing — plus an eval pipeline that catches regressions before merge.
Why static knowledge graphs fail for agents that learn over time, and how Graphiti's temporal edges fix it. Concrete schema examples and edge-case behavior.
Inngest's Agent Kit adds durable steps, retries, and concurrency control for agent runs. The right pick for agents that span hours or days without losing state.
Smolagents lets agents write Python instead of JSON. Why code-as-action reduces tool errors and where the security trade-offs are for production deployments.
Versioning agent workflows is the unsexy reliability primitive that decides whether your agent survives its second deploy. A 2026 deep dive.
Modal turns a Python function into autoscaling serverless compute with optional GPU. Deploy a LiveKit Agent with one command and get pay-per-second billing.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.
Try Live DemoBook a DemoCalculate Your ROI