Agent Frameworks Reason, Temporal Survives: Durable Execution for Agents
LangGraph, CrewAI and AutoGen orchestrate LLM calls but not crashes or week-long waits. How Temporal's durable execution fills that gap, and when it's overkill.
This article is published in English.
Sources: Agentic AI Frameworks in 2026: The 3 Everyone’s Using and the 1 Nobody Talks About
Poll engineers building agentic systems in 2026 about their framework and nearly all of them name LangGraph, CrewAI, AutoGen or some blend. None of those tools is bad. The trouble is that the whole conversation takes place at the wrong layer of the stack.
Production agent systems show a repeating pattern. What wakes the on-call engineer at 3 a.m. is seldom the framework that decides what the agent thinks. It's nearly always one family of faults: a process dies partway through a workflow, a step runs twice since its retry wasn't idempotent, a pod eviction kills a long-running agent task at the 40-minute mark with no way to pick it back up, or workflow state lived in a Python variable that disappeared when the container was recycled.
None of LangGraph, CrewAI or AutoGen handle that family end to end, and that's by design, since they exist to orchestrate LLM calls and tool use. Production agent systems really die from a lack of durable execution, a much older and duller field that predates the word "agent" by roughly a decade. Temporal, the system that owns it, barely features in agent-framework debates, yet it has quietly turned into the backbone beneath a growing number of serious agent deployments. What follows covers what the three popular frameworks do well, where each abstraction gives out, and why the unfashionable fourth option deserves a business bet.
LangGraph
LangGraph's contribution is making an agent an explicit graph over typed state, not an implicit chain of prompts. That has real value because control flow becomes inspectable and testable. A triage workflow, for instance, can look like this:
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, END
class TriageState(TypedDict):
ticket_id: str
category: Literal["billing", "technical", "abuse", "unknown"]
priority: int
assigned_queue: str | None
def classify_node(state: TriageState) -> TriageState:
category = llm_classify(state["ticket_id"]) # the only probabilistic step
return {**state, "category": category}
def route_node(state: TriageState) -> str:
# deterministic, testable, no LLM involved
return {
"billing": "billing_queue",
"technical": "tech_queue",
"abuse": "trust_and_safety_queue",
"unknown": "human_review",
}[state["category"]]
graph = StateGraph(TriageState)
graph.add_node("classify", classify_node)
graph.set_entry_point("classify")
graph.add_conditional_edges("classify", route_node)
app = graph.compile()
That's a good idea, cleanly written. Trouble starts when something has to live longer than one invoke() call and its checkpointer. Think of coordination across services, workflows spanning days, or steps running where LangGraph has no say: a Spark job, an approval queue that goes through Slack and returns days later, or a service that another team deploys on its own cadence. The checkpointing does work, yet it only covers LangGraph's own execution model. After a handoff to infrastructure that has no connection to LLMs, durability becomes something to build manually.
CrewAI
A multi-agent crew can be declared in a few lines:
from crewai import Agent, Task, Crew
researcher = Agent(role="Research Analyst", goal="Find relevant market data", tools=[search_tool])
writer = Agent(role="Report Writer", goal="Turn research into a client-ready summary")
crew = Crew(agents=[researcher, writer], tasks=[
Task(description="Research {company}'s Q2 earnings", agent=researcher),
Task(description="Write a 3-paragraph summary for the client", agent=writer),
])
crew.kickoff(inputs={"company": "Acme Corp"})
No tool gets from a loose idea for a multi-agent workflow to a running demo in twenty minutes faster than CrewAI. The cost comes afterward. kickoff() blocks, with no first-class way to pause for hours or days, no retry behavior beyond what's attached to individual tools, and no durable state except what the developer saves. It orchestrates LLM reasoning and isn't a workflow engine, and it was never advertised as one, though many teams end up treating it that way by accident.
AutoGen
A minimal AutoGen setup pairs an assistant with a user proxy:
import autogen
assistant = autogen.AssistantAgent(name="assistant", llm_config={"config_list": config_list})
user_proxy = autogen.UserProxyAgent(name="user_proxy", human_input_mode="NEVER")
user_proxy.initiate_chat(assistant, message="Summarize this quarter's incident reports.")
AutoGen's conversational, emergent style fits exploratory work. Its 2026 incarnation, with more structured workbench and event-driven patterns, even leans toward separating orchestration from conversation. Beneath that, the foundation is still a transcript of messages instead of a durable, typed ledger of execution. When the process hosting a conversation dies, the survivor is a partial chat log and not a computation that can resume.
The shared gap
All three do well at what's actually new: getting an LLM to reason, call tools and work with other LLM-driven agents. The older, plainer concern, making sure a multi-step process completes properly whatever crashes, restarts, times out or ships underneath it, isn't something they try to cover. That's no fault of theirs. Expecting it of them is a category mistake.
Think about an actual workflow, not a demo: an agent reconciling vendor invoices. It extracts line items from a PDF, compares them with a purchase order in an ERP system, flags discrepancies above a threshold for a human to review, then may wait days for the answer before posting the approved entry to accounting. A failed step must retry without double-posting. A redeploy mid-workflow must not cause a restart from zero or a silent vanishing act.
┌────────────┐ ┌───────────────┐ ┌─────────────────┐ ┌───────────────┐
│ Extract │ --> │ Match against │ --> │ Human approval │ --> │ Post to ERP │
│ line items│ │ purchase order│ │ (can take days)│ │ (idempotent) │
└────────────┘ └───────────────┘ └─────────────────┘ └───────────────┘
▲ │
└───────────── retry on failure ───────────┘
No one designed LangGraph, CrewAI or AutoGen to promise that this diagram survives a pod restart on day three while someone is still weighing line item 14. That isn't about LLM orchestration. It's durable execution, and the systems that do it well, with Temporal at the front, have powered unglamorous backend work for years: payment processing, order fulfillment and saga-pattern microservices, long before the word agent described such programs.
How Temporal works
Temporal's model looks deceptively simple. The workflow is written as a function in ordinary code, with loops, conditionals and function calls, and Temporal makes that function's execution history durable. If the process crashes at any moment, Temporal replays the function from its persisted event history up to the crash point and carries on deterministically, without rerunning side effects. The quiet magic is that side effects, such as calling an LLM, hitting an API or writing to a database, sit in separate functions called activities. Temporal retries those independently with configurable backoff, while the workflow function only coordinates them.
The invoice-reconciliation agent below treats the LLM call as one activity among several. First, the activities, which are the parts that touch the outside world:
# activities.py — the parts that touch the outside world (LLMs, APIs, DBs)
from temporalio import activity
from dataclasses import dataclass
@dataclass
class Invoice:
invoice_id: str
vendor: str
line_items: list[dict]
@activity.defn
async def extract_line_items(pdf_url: str) -> list[dict]:
# calls a vision-capable LLM, or a dedicated OCR model — doesn't matter to the workflow
text = await download_and_ocr(pdf_url)
return await llm_extract_structured(text, schema=LINE_ITEM_SCHEMA)
@activity.defn
async def match_purchase_order(invoice: Invoice) -> dict:
po = await erp_client.get_purchase_order(invoice.vendor)
discrepancies = diff_line_items(invoice.line_items, po["line_items"])
return {"po_id": po["id"], "discrepancies": discrepancies}
@activity.defn
async def post_to_erp(invoice_id: str, po_id: str) -> dict:
# idempotency is enforced with a deterministic key derived from workflow inputs,
# not a random UUID generated per attempt — this is the same lesson as always
return await erp_client.post_journal_entry(
idempotency_key=f"invoice-post-{invoice_id}",
invoice_id=invoice_id,
po_id=po_id,
)
@activity.defn
async def notify_human_reviewer(invoice_id: str, discrepancies: list[dict]) -> None:
await slack_client.post_message(
channel="#ap-review",
text=f"Invoice {invoice_id} has {len(discrepancies)} discrepancies needing review.",
)
Next comes the workflow, which takes over the orchestration role LangGraph or CrewAI would usually play, except durability is built in and doesn't depend on opting into a checkpointer:
# workflows.py
from temporalio import workflow
from datetime import timedelta
@workflow.defn
class InvoiceReconciliationWorkflow:
def __init__(self) -> None:
self._approval_received = False
self._approved = False
@workflow.signal
async def submit_human_decision(self, approved: bool) -> None:
# A signal is how the outside world talks to a running, possibly-days-old workflow
self._approval_received = True
self._approved = approved
@workflow.run
async def run(self, pdf_url: str, invoice_id: str, vendor: str) -> dict:
line_items = await workflow.execute_activity(
extract_line_items, pdf_url,
start_to_close_timeout=timedelta(minutes=2),
retry_policy=workflow.RetryPolicy(maximum_attempts=3),
)
invoice = Invoice(invoice_id=invoice_id, vendor=vendor, line_items=line_items)
match_result = await workflow.execute_activity(
match_purchase_order, invoice,
start_to_close_timeout=timedelta(seconds=30),
retry_policy=workflow.RetryPolicy(maximum_attempts=5),
)
if match_result["discrepancies"]:
await workflow.execute_activity(
notify_human_reviewer, invoice_id, match_result["discrepancies"],
start_to_close_timeout=timedelta(seconds=10),
)
# This is the line that would be genuinely hard to write safely in the
# other three frameworks: wait, durably, for up to 7 days, for a signal
# that might arrive from a Slack button, a web form, anything —
# while the workflow's entire state sits safely in Temporal's history,
# costing zero compute while it waits.
await workflow.wait_condition(
lambda: self._approval_received,
timeout=timedelta(days=7),
)
if not self._approved:
return {"status": "rejected", "invoice_id": invoice_id}
result = await workflow.execute_activity(
post_to_erp, invoice_id, match_result["po_id"],
start_to_close_timeout=timedelta(seconds=30),
retry_policy=workflow.RetryPolicy(maximum_attempts=5),
)
return {"status": "posted", "invoice_id": invoice_id, "erp_result": result}
Several details deserve attention, because they're the very failure modes that make agent systems flaky, and here they're properties of the runtime and not features someone must remember to add.
A seven-day wait is free and survives everything. workflow.wait_condition keeps no thread, process or container open for a week. Temporal stores the workflow's state in its own durable store, so the worker may be redeployed, scaled to zero or crash entirely meanwhile. When the signal at last arrives, Temporal replays the history and resumes at that very await. Doing the same on a LangGraph checkpointer or a CrewAI crew would essentially mean rebuilding Temporal's event-sourcing model.
Retries are automatic, narrowly scoped and never repeat finished work. Suppose post_to_erp fails after match_purchase_order succeeded. Temporal won't rerun match_purchase_order, since its result already sits in the durable history. Set that beside the naive take on the same idea:
# BEFORE — naive retry loop, the kind you write at 11pm under deadline pressure
async def reconcile_invoice_naive(pdf_url, invoice_id, vendor):
for attempt in range(3):
try:
line_items = await extract_line_items(pdf_url)
match_result = await match_purchase_order(Invoice(invoice_id, vendor, line_items))
if match_result["discrepancies"]:
await notify_human_reviewer(invoice_id, match_result["discrepancies"])
approved = await wait_for_approval_somehow(invoice_id) # good luck
if not approved:
return {"status": "rejected"}
return await post_to_erp(invoice_id, match_result["po_id"])
except Exception:
continue # retries EVERYTHING from the top, including re-extracting the PDF
That naive version repeats the LLM extraction on every retry, spending tokens and money on a step that already worked, and wait_for_approval_somehow has no good answer if it must outlive a redeploy. Temporal's version doesn't deal with these problems through extra code. In that form the problems never arise, thanks to how execution is structured.
Plain Python means testable. Temporal includes a test framework that can skip through a timedelta(days=7) wait in milliseconds inside a unit test, and activities can be mocked without touching the network:
import pytest
from temporalio.testing import WorkflowEnvironment
from temporalio.worker import Worker
@pytest.mark.asyncio
async def test_rejected_invoice_does_not_post_to_erp():
async with await WorkflowEnvironment.start_time_skipping() as env:
async with Worker(
env.client, task_queue="test-queue",
workflows=[InvoiceReconciliationWorkflow],
activities=[extract_line_items, match_purchase_order, notify_human_reviewer, post_to_erp],
):
handle = await env.client.start_workflow(
InvoiceReconciliationWorkflow.run,
args=["fake.pdf", "INV-001", "Acme"],
id="test-invoice-001",
task_queue="test-queue",
)
await handle.signal(InvoiceReconciliationWorkflow.submit_human_decision, False)
result = await handle.result()
assert result["status"] == "rejected"
Notice the absences: no real seven-day sleep, no prodding at a checkpointer's internals, no manual reasoning about restarts. Durability is load-bearing infrastructure, and not something each test must prove again.
Using both layers
The even-handed answer isn't dropping LangGraph in favor of Temporal. They work at different layers, and the best production setups of 2026 pair them: LangGraph or CrewAI for reasoning inside one step, Temporal for the workflow layer that binds the multi-day, multi-service process. An activity can wrap one bounded reasoning step like this:
@activity.defn
async def run_agent_reasoning_step(ticket_text: str) -> dict:
# LangGraph handles the bounded, single-turn reasoning —
# classify, decide, draft — inside an activity that Temporal
# retries and times out like any other side effect.
result = triage_graph.invoke({"ticket_id": ticket_text, "audit_log": []})
return {"category": result["category"], "priority": result["priority"]}
@workflow.defn
class SupportTicketWorkflow:
@workflow.run
async def run(self, ticket_id: str, ticket_text: str) -> dict:
triage = await workflow.execute_activity(
run_agent_reasoning_step, ticket_text,
start_to_close_timeout=timedelta(seconds=30),
retry_policy=workflow.RetryPolicy(maximum_attempts=3),
)
if triage["priority"] >= 8:
await workflow.execute_activity(page_oncall_engineer, ticket_id, ...)
await workflow.wait_condition(lambda: self._acknowledged, timeout=timedelta(minutes=15))
# ... routing, SLA timers, escalation chains — all durable, all testable
This is the architecture to recommend for a team whose agent system has to withstand production. The LLM-native framework handles what it's good at, reasoning within a bounded step. A durable execution engine handles what it was built for, ensuring the whole process, whether it lasts minutes or weeks and whether it spans LLM calls or human approvals, finishes correctly or fails loudly. Neither has to play the other's part.
Where Temporal costs too much
Temporal has drawbacks, and they make it a poor default for plenty of agent work. It's one more distributed system to run, whether a self-hosted server or a Temporal Cloud bill, plus workers and task queues, far more infrastructure than a Python process running LangGraph. Workflow code also follows strict determinism rules. Calling datetime.now() directly, using unguarded randomness or doing raw I/O in the workflow function itself are all off limits. Teams need time to absorb that, and breaking the rules yields puzzling non-determinism errors on replay. For a short, single-process job, like answering a question or summarizing a document in one LLM call, durable execution is plain overhead, since no multi-step process exists that a crash could damage.
A decision rule
Choose durable execution as the backbone whenever a workflow might outlive one process, has to coordinate across services or wait for a person, or moves anything that mustn't be processed twice, such as money, inventory or an email that shouldn't go out twice. Let the preferred LLM framework run as a component inside one of its steps. When everything begins and ends inside a single request-response cycle, skip it, since adding it would solve a problem that doesn't exist yet.
Plenty of "which agent framework should I use" discussions overlook a bigger question: which layer of the system actually needs hardening? LangGraph, CrewAI and AutoGen are having a worthwhile debate over how to structure LLM reasoning and multi-agent coordination. But the question of whether an agent system survives a bad deploy, a network partition or a person needing three days to click "approve" is a separate one, and workflow engines largely settled it quietly, having handled durable execution before anyone said agentic AI. The popular frameworks tune what's visible in a demo. The overlooked one decides whether anything is still standing a month later.