Skip to content
Writing
LangGraphPythonState MachinesHuman in the Loop

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.

Tayyab MughalFounder & AI Chief2 min read

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().
Early Access

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.

Rolling out in developer batches·No credit card needed
Social Hashtags & Share
#EmailAPI#DeveloperTools#LangGraph#Python#DevCommunity#StateMachines
Tayyab MughalFounder & AI Chief

Building SadaSend — transactional email with an MCP server that has a ceiling. Writes about deliverability, email infrastructure, and what happens when you hand an autonomous agent a sending credential.

Keep reading