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

_By Tayyab Mughal, Founder & AI Chief · 23 August 2026 · 2 min read_

> How to build durable, asynchronous state machines in LangGraph with human-in-the-loop approval interrupts before sending emails.

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

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.

```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):
    # 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().

---

_Tags: LangGraph, Python, State Machines, Human in the Loop_
