LangGraph pushed LangChain into a direction many people had been asking for: explicit state, explicit transitions, deterministic control flow. It's a better fit for production agents than a freeform ReAct loop. It's also a perfect fit for modeling email-driven workflows — because "wait for email" is literally a state.
Email workflows as graphs
Consider the most common agent task: sign up for a service, verify the email, complete the flow. Modeled as a graph:
create_inbox→submit_signup→wait_for_otp→submit_otp→done
Each node is a concrete step with inputs and outputs. The edges are deterministic. If OTP times out, transition to retry or fail. This is exactly what LangGraph's StateGraph was built for.
Wiring Lumbox as graph nodes
from langgraph.graph import StateGraph, END
from typing import TypedDict
from lumbox import Lumbox
lumbox = Lumbox(api_key=os.environ["LUMBOX_API_KEY"])
class AgentState(TypedDict):
inbox_id: str
address: str
otp: str | None
target_url: str
def create_inbox_node(state: AgentState) -> AgentState:
inbox = lumbox.inboxes.create(display_name="signup")
return {**state, "inbox_id": inbox.id, "address": inbox.address}
def submit_signup_node(state: AgentState) -> AgentState:
# browser automation fills form with state["address"]
return state
def wait_for_otp_node(state: AgentState) -> AgentState:
result = lumbox.inboxes.wait_for_otp(state["inbox_id"], timeout=120)
return {**state, "otp": result.otp}
def submit_otp_node(state: AgentState) -> AgentState:
# browser automation enters state["otp"]
return state
graph = StateGraph(AgentState)
graph.add_node("create_inbox", create_inbox_node)
graph.add_node("submit_signup", submit_signup_node)
graph.add_node("wait_for_otp", wait_for_otp_node)
graph.add_node("submit_otp", submit_otp_node)
graph.set_entry_point("create_inbox")
graph.add_edge("create_inbox", "submit_signup")
graph.add_edge("submit_signup", "wait_for_otp")
graph.add_edge("wait_for_otp", "submit_otp")
graph.add_edge("submit_otp", END)
app = graph.compile()
Durability
LangGraph's checkpointing plays perfectly with Lumbox — the wait_for_otp node can run, resume, or replay. If your process crashes mid-flow, the state is persisted and picks up where it left off. Lumbox inboxes persist server-side, so the OTP is still retrievable when you come back. lumbox.co.