Why stateless agent loops fail in production
Standard agent loops assume all tool calls can be executed synchronously. But when an email requires manager sign-off, the workflow must pause for minutes or hours without keeping a server connection open.
LangGraph solves this with persistent checkpointers (Postgres/Sqlite) and graph interrupts, allowing workflows to sleep until a human reviews the draft.
Building the StateGraph with interrupt_before
Here is how to create a durable LangGraph workflow that drafts an email, pauses before execution, and resumes when an approval webhook arrives.
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):
# Simulated LLM generation
subject = f"Resolution for: {state['inquiry'][:30]}"
body = f"Hello, regarding your inquiry '{state['inquiry']}', here is the confirmed resolution."
return {"draft_subject": subject, "draft_body": body}
def send_node(state: EmailAgentState):
if not state.get("approved", False):
raise ValueError("Cannot send unapproved email")
requests.post(
"https://api.sadasend.com/v1/emails",
headers={"Authorization": f"Bearer {os.getenv('SADASEND_API_KEY')}"},
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 on Send Node
memory = MemorySaver()
graph = builder.compile(checkpointer=memory, interrupt_before=["send_email"])
# 3. Execution Phase: Agent runs up to the interrupt point
config = {"configurable": {"thread_id": "ticket_994"}}
initial_input = {"recipient": "dev@company.internal", "inquiry": "Need invoice receipt"}
graph.invoke(initial_input, config=config)
# Workflow is now paused! State is persisted.
current_state = graph.get_state(config)
print("Drafted and waiting for approval:", current_state.values)
# 4. Human Approval Phase (via dashboard or Slack):
graph.update_state(config, {"approved": True})
# Resume execution
graph.invoke(None, config=config)
print("Email dispatched successfully!")Production advantages of LangGraph state machines
- Zero lost state: Workflows survive server restarts and deployments.
- Full audit trail: Every state transition and draft version is permanently recorded.
- Seamless webhook resumption: Connect approval buttons in Slack, Linear, or your dashboard directly to graph.update_state().
Building AI agents that send email?
Join the SadaSend early access waitlist to get scoped API keys, recipient allowlists, and Model Context Protocol (MCP) servers upon launch.