Why stateless agent loops fail in production workflows
Standard autonomous loops assume all tool calls execute synchronously within a single HTTP request lifecycle. But in enterprise customer communications, outbound emails often require manager approval, legal sign-off, or financial verification.
Keeping a server connection open for hours while waiting for human feedback causes worker timeouts and lost memory state. LangGraph solves this by persisting execution state into durable checkpointers (PostgreSQL or SQLite) and pausing the graph at designated interrupt_before boundaries.
| Workflow Architecture | Stateless LLM Loop | LangGraph StateGraph + SadaSend |
|---|---|---|
| State Persistence | In-memory (lost on restart) | Durable PostgreSQL / SQLite checkpointer |
| Approval Mechanism | Requires busy-waiting or polling | Native interrupt_before with zero idle compute |
| Audit Trail | Ephemeral console logs | Immutable state transition snapshots |
| Delivery Gateway | Direct SMTP without safeguards | Scoped API key with recipient allowlist |
Building the StateGraph with interrupt_before and Checkpointers
Here is how to create a durable LangGraph workflow that drafts an email, persists state, pauses execution, and safely resumes upon human confirmation:
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
import requests
import os
class EmailAgentState(TypedDict):
recipient: str
inquiry: str
draft_subject: str
draft_body: str
approved: bool
def draft_node(state: EmailAgentState):
subject = f"Resolution: {state['inquiry'][:30]}"
body = f"Hello, regarding '{state['inquiry']}', here is the verified resolution."
return {"draft_subject": subject, "draft_body": body}
def send_node(state: EmailAgentState):
if not state.get("approved", False):
raise ValueError("Security Violation: Cannot send unapproved email")
res = requests.post(
"https://api.sadasend.com/emails",
headers={
"Authorization": f"Bearer {os.getenv('SADASEND_API_KEY')}",
"Idempotency-Key": f"graph_{state['recipient']}_{hash(state['draft_subject'])}",
},
json={
"to": state["recipient"],
"subject": state["draft_subject"],
"text": state["draft_body"],
}
)
return state
# 1. Construct State Graph
builder = StateGraph(EmailAgentState)
builder.add_node("draft_email", draft_node)
builder.add_node("send_email", send_node)
builder.set_entry_point("draft_email")
builder.add_edge("draft_email", "send_email")
builder.add_edge("send_email", END)
# 2. Compile with Checkpointer and Interrupt before execution
memory = MemorySaver()
graph = builder.compile(checkpointer=memory, interrupt_before=["send_email"])
# 3. Execution Phase: Workflow runs and pauses right before send_email
config = {"configurable": {"thread_id": "ticket_994"}}
graph.invoke({"recipient": "alex@customer.com", "inquiry": "Need refund receipt"}, config=config)
# State is safely persisted in checkpointer. Server can restart freely.
# 4. Operator Approval Phase (via webhook or Slack action):
graph.update_state(config, {"approved": True})
graph.invoke(None, config=config) # Dispatches email via SadaSendProduction advantages of LangGraph state machines
- Zero lost state: Workflows survive server restarts, rolling deployments, and container restarts.
- Full audit trail: Every state transition and draft edit is permanently recorded in your database.
- Seamless webhook resumption: Connect approval buttons in Slack, Linear, or your dashboard directly to graph.update_state().
Building AI agents that send email?
Scoped API keys, per-key recipient allowlists, approval mode and a hosted MCP server with ten tools — on the free plan, without a card.