Introduction
Building a quick AI agent that calls an LLM, processes some text, and returns a response is relatively easy. A single function call:
async function answerQuestion(question) {
const response = await openai.createChatCompletion({
messages: [{ role: 'user', content: question }]
});
return response.choices[0].message.content;
}
But building an autonomous AI agent that handles multi-step business operations in production—like scanning an inbox, generating invoice data, updating a database, and sending confirmation emails over several hours—is a completely different monster.
The number one reason AI agents fail when they hit the real world is a lack of durability.
This guide explains why stateless execution fails and what you need to build reliable agents.
The Failure Mode: Stateless Execution
What Happens With Stateless Agents
Standard Stateless Agent Execution:
[Step 1: Parse Email] ──► [Step 2: Generate Invoice] ──► [Step 3: Call Database] ──► [Server Blip]
(Email parsed) (Invoice data created) (Starting to write...) (CRASH)
(Process Lost)
(State Lost)
Result: Invoice was generated but never written to database
Email was parsed but not marked as processed
System is in inconsistent state
User gets charged/notified, but no invoice record exists
If your agent runs inside a standard, stateless serverless function or API route, it lives on borrowed time:
- If a third-party API takes 45 seconds to respond, your execution times out
- If your server restarts mid-task, your agent loses its memory of where it was
- If a database call fails halfway through, you're left with corrupted data
- If the network disconnects, the entire workflow is lost
You're left with incomplete data, corrupted state, and angry customers.
The Pitfalls of Stateless Execution
Pitfall #1: No Recovery Mechanism
async function processInvoice(emailId) {
// Step 1: Extract data
const email = await getEmail(emailId);
const invoiceData = parseEmail(email);
// Step 2: Call external API
const pricing = await getPricingFromExternalAPI(); // Takes 40 seconds
// Step 3: Create invoice
const invoice = await createInvoice(invoiceData, pricing);
// Server crashes here before we can return
return invoice;
}
If the server crashes after Step 2, you've already made the API call (and possibly been charged for it). You've parsed the email. But you never created the invoice.
Next time the job runs, the external API gets called again (duplicate charge, duplicate work).
Pitfall #2: Timeouts on Long-Running Operations
// Serverless function with 30-second timeout
async function sendBulkEmails(userIds) {
for (const userId of userIds) {
// This loop might take 5 minutes to complete
await sendEmail(userId);
}
}
// Problem: Function times out after 30 seconds
// Only 100 emails sent, 400 remain
// No way to resume from where we left off
Pitfall #3: Inconsistent State
async function transferMoney(fromAccount, toAccount, amount) {
// Step 1: Deduct from source
await deductFromAccount(fromAccount, amount); // ✓ Success
// Step 2: Add to destination
await addToAccount(toAccount, amount); // ✗ Network timeout
// Result: Money was deducted but never credited
// System is in an inconsistent state
}
These are hard bugs to debug and even harder to recover from.
Enter Durable Runtimes
A durable runtime is an execution environment where the state of the code is continuously saved to a database. If execution fails, the runtime automatically resumes from the last saved state.
How Durable Execution Works
Durable Agent Execution:
[Step 1] [Step 2] [Step 3] [Server Blip]
[Parse Email] [Generate [Database Call]
│ Invoice] │
│ │ │
↓ ↓ ↓
Save State 1 Save State 2 Save State 3 (State is persisted)
(Email data) (Invoice data) (DB write started)
[Server comes back online]
Durable Runtime detects: "You were at Step 3, let me resume..."
Resumes from Step 3 with all variables intact
Completes the database write
[Step 4: Send confirmation]
[Save State 4]
The key difference:
Stateless Execution:
Memory: Variables only exist in current process
Recovery: None (if process dies, all state is lost)
Durable Execution:
Memory: Variables saved after each step
Recovery: Resume from last saved state automatically
Example: Building a Durable Agent
Using a library like Temporal or Durable Functions:
// Pseudo-code using Temporal
async function processInvoiceWorkflow(emailId: string): Promise<Invoice> {
// Each step is durable and automatically checkpointed
// Step 1: Parse email (state is saved after this)
const email = await activities.getEmail(emailId);
const invoiceData = parseEmail(email);
// If execution fails here, resume will start from Step 2
// with invoiceData intact
// Step 2: Call external API (state is saved after this)
const pricing = await activities.getPricingFromAPI();
// Step 3: Create invoice (state is saved after this)
const invoice = await activities.createInvoice(invoiceData, pricing);
// Step 4: Send confirmation (state is saved after this)
await activities.sendConfirmationEmail(invoice);
return invoice;
}
If the server crashes at any point:
- The runtime detects the failure
- It reads the last saved checkpoint
- It resumes execution from the next step
- All variables are restored
- The workflow completes
Benefits:
✓ No duplicate API calls (idempotent) ✓ No lost data ✓ No inconsistent state ✓ Automatic retries with exponential backoff ✓ Long-running workflows (hours, days, weeks) ✓ Full visibility into execution history
Durable vs. Stateless: Side-by-Side
| Feature | Stateless | Durable |
|---|---|---|
| Server crashes mid-execution | All state lost | Resume from last checkpoint |
| API call fails | Entire workflow fails | Automatic retry |
| Third-party service times out | Timeout error | Wait up to configured timeout |
| Long-running workflows (hours) | Impossible | Fully supported |
| Idempotency | Manual (you must implement) | Automatic |
| Execution history | None | Complete audit trail |
Real-World Scenarios
Scenario 1: Email Processing Agent
Workflow: Process customer support emails
Step 1: Fetch unread emails from inbox
Step 2: For each email, call Claude to summarize
Step 3: Extract action items from summary
Step 4: Create tickets in Jira
Step 5: Send confirmation to customer
Problems with stateless:
- Crashes mid-loop = some emails processed, some not
- API timeout on Claude = entire batch fails
- Can't resume = need to reprocess everything
With durable:
- Crashes mid-loop = resume from next email
- API timeout = retry with backoff
- Can restart = resumes from saved state
Scenario 2: Bulk Data Migration
Workflow: Migrate 100,000 user records from old DB to new DB
Step 1: Read chunk of 1000 records
Step 2: Transform data
Step 3: Write to new database
Step 4: Mark as migrated in old database
Step 5: Repeat for next chunk
Problems with stateless:
- Crash at record 50,000 = start over from scratch
- Network timeout = entire migration fails
- Database lock = entire process blocks
With durable:
- Crash at record 50,000 = resume from record 50,001
- Network timeout = retry just that chunk
- Database lock = wait briefly, resume automatically
Scenario 3: Long-Running Invoice Generation
Workflow: Generate monthly invoices for 10,000 customers
Step 1: Start loop through customers
Step 2: For each customer, aggregate usage data
Step 3: Calculate costs
Step 4: Generate PDF invoice
Step 5: Send via email
Step 6: Record in database
Takes: 2 hours total
Runs: Once per month
Problems with stateless:
- Must complete in 30 seconds (serverless timeout)
- Impossible with current architecture
With durable:
- Can run for hours
- Survives crashes
- Can be paused/resumed manually
- Full visibility into progress
Implementing Durable Execution
Option 1: Use Temporal (Recommended for Complex Workflows)
Temporal is a durable workflow framework designed for exactly this use case:
import { workflow, activity } from '@temporalio/workflow';
// Define activities (work that can fail)
async function fetchEmailActivity(emailId: string): Promise<Email> {
// If this fails, Temporal handles the retry
}
async function generateInvoiceActivity(data: InvoiceData): Promise<Invoice> {
// If this fails, Temporal retries
}
// Define workflow (orchestration logic)
export async function invoiceWorkflow(emailId: string): Promise<Invoice> {
// Temporal automatically checkpoints after each activity
const email = await fetchEmailActivity(emailId);
const invoiceData = parseEmail(email);
const invoice = await generateInvoiceActivity(invoiceData);
return invoice;
}
Pros:
- Production-proven (used by Uber, Figma, etc.)
- Complex multi-step workflows
- Full execution history and visibility
Cons:
- Requires running Temporal server
- More complex to set up
Option 2: AWS Step Functions (If You're on AWS)
AWS Step Functions is a managed service for durable workflows:
{
"Comment": "Invoice processing workflow",
"StartAt": "FetchEmail",
"States": {
"FetchEmail": {
"Type": "Task",
"Resource": "arn:aws:lambda:region:account:function:getEmail",
"Next": "ParseEmail"
},
"ParseEmail": {
"Type": "Task",
"Resource": "arn:aws:lambda:region:account:function:parseEmail",
"Next": "GenerateInvoice"
},
"GenerateInvoice": {
"Type": "Task",
"Resource": "arn:aws:lambda:region:account:function:generateInvoice",
"End": true
}
}
}
Pros:
- Managed service (no infrastructure)
- Automatic retries and error handling
- Deep AWS integration
Cons:
- AWS-specific
- Less flexible for custom logic
Option 3: Database-Backed State Machine (DIY)
If you want more control, implement your own:
async function durableWorkflow(emailId: string) {
// Load or create workflow state
let state = await getWorkflowState(emailId);
// Resume from last step
if (state.step === 'STARTED') {
state.email = await fetchEmail(emailId);
state.step = 'EMAIL_FETCHED';
await saveWorkflowState(state);
}
if (state.step === 'EMAIL_FETCHED') {
state.invoiceData = parseEmail(state.email);
state.step = 'DATA_PARSED';
await saveWorkflowState(state);
}
if (state.step === 'DATA_PARSED') {
state.invoice = await generateInvoice(state.invoiceData);
state.step = 'INVOICE_GENERATED';
await saveWorkflowState(state);
}
return state.invoice;
}
Pros:
- Complete control
- No new dependencies
Cons:
- More code to maintain
- Harder to debug
- Easy to make mistakes
Key Takeaways
- Stateless execution fails in production — AI agents need durability
- Durable runtimes save state automatically — Resume from last checkpoint on failure
- Use Temporal for complex workflows — Production-proven, feature-rich
- Use Step Functions if on AWS — Managed, built-in error handling
- Implement your own if you need simplicity — Database-backed state machine
- Every step should be idempotent — Safe to retry without side effects
When you shift from writing simple LLM scripts to engineering production-grade agentic systems, durability is as important as the AI itself.
A brilliant prompt and a fast model are worthless if your agent crashes in the middle of updating your database.
Remember: The best AI agent is the one that actually completes its work. Build for durability first, optimization second.
Ready to start building?
Explore the most comprehensive directory of APIs for Nigerian developers and find exactly what you need.
Browse the API Directory


