Skip to content
Writing
NewLangGraphPythonState MachinesHuman in the LoopAI Agents

LangGraph State Machines for Email: Designing Human-in-the-Loop Approval Interrupts

LangGraph allows developers to pause agent workflows indefinitely waiting for human approval. Learn how to construct an email state graph with durable checkpoints.

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 ArchitectureStateless LLM LoopLangGraph StateGraph + SadaSend
State PersistenceIn-memory (lost on restart)Durable PostgreSQL / SQLite checkpointer
Approval MechanismRequires busy-waiting or pollingNative interrupt_before with zero idle compute
Audit TrailEphemeral console logsImmutable state transition snapshots
Delivery GatewayDirect SMTP without safeguardsScoped 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:

PYTHON
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 SadaSend

Production 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().
Free plan

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.