LangGraph Agents: A Practical Guide to Building Stateful AI Workflows LangGraph, a low-level orchestration framework by LangChain, enables developers to build stateful AI workflows and agents as graphs of steps, with state, nodes, and edges for branching, looping, persistence, and human-in-the-loop control. The guide explains how to define state schemas, implement nodes and conditional edges, and compile graphs using the StateGraph API, emphasizing explicit control flow and model-driven decisions only where useful. A useful AI application often needs much more than “send a prompt, receive an answer.” It may need to classify a request, retrieve trusted data, call APIs, ask for approval, remember progress, retry after failure, and decide what should happen next. LangGraph is a low-level orchestration framework for building these stateful workflows and agents as a graph of steps. Those steps can branch, loop, persist progress, call tools, stream intermediate events, and pause for human input. The simplest mental model is: state is what the system knows, nodes do the work, and edges decide what runs next. Consider a customer asking: “My order arrived damaged. Can I get a refund?” A production system may need to: If all of this logic is hidden inside one large function or one opaque model loop, it becomes difficult to test, observe, resume, and control. LangGraph makes the control flow explicit while still allowing model-driven decisions where they are useful. Imagine a parcel moving through a delivery hub: The analogy is useful because it makes execution steps and transitions visible. It is not exact: software nodes can run in parallel, invoke probabilistic models, revisit earlier nodes, and update only selected fields of shared state. State is the current snapshot of information available to graph steps. A state schema defines which fields exist and, in Python, usually their types. python from typing extensions import TypedDictclass CounterState TypedDict : count: int If the current state is {"count": 4}, a node can read that value and return an update such as {"count": 5}. A node does not need to return every state field. A useful rule is to keep state limited to the information required to coordinate the workflow. Treat it as shared working data, not as a dump of every temporary local variable. A node is executable logic. It may call a model, run deterministic code, validate output, query a service, perform a tool action, or request human input. python def increment state: CounterState : return {"count": state "count" + 1} If count is 4, this node returns 5. Returning an explicit update gives the runtime a clean boundary for state management. This boundary is also valuable for software engineering: small nodes let you test routing separately from database access, tool execution, validation, or an LLM call. An edge determines which node executes next. A normal edge always follows the same transition. A conditional edge calls routing logic and chooses a destination based on state. php from typing import Literaldef route state: CounterState - Literal "again", "done" : return "again" if state "count" < 3 else "done" Conditional routing is especially useful for exact business rules. If ordinary code can determine the answer precisely, there is usually no benefit in asking an LLM to guess it. With the Graph API, StateGraph is a builder. You add nodes and edges, then call compile to produce an executable graph. python from langgraph.graph import StateGraph, START, ENDbuilder = StateGraph MyState builder.add node "work", work node builder.add edge START, "work" builder.add edge "work", END graph = builder.compile START and END are special markers rather than business-logic nodes. START represents where input enters; END represents termination. A tool-capable model can produce a structured request containing a tool name and arguments. The application, not the model itself, performs the real operation. php messages - model - tool request? | no - finish | | yes v tool - tool result - model - ... The model chooses only from the tools you expose. Tool descriptions and schemas therefore become part of the agent’s interface design. A vague tool description can lead the model to call the right capability incorrectly. This is also where an important distinction appears: a workflow follows predetermined code paths, while an agent dynamically chooses actions and tool usage. Strong systems often combine the two: deterministic workflow structure around a smaller agentic region. One of the most practical design lessons in the source material is that an LLM does not have to control everything. A robust refund workflow might divide responsibility like this: Use models where flexible judgment adds value. Keep exact rules, permissions, and irreversible actions constrained. The section distinguishes a higher-level agent harness from lower-level orchestration. As of the current LangChain documentation, LangChain’s create agent is the recommended higher-level option for a familiar model-plus-tools loop and is built on LangGraph. php from langchain.agents import create agentdef get weather city: str - str: """Return weather data for a city.""" return f"Weather lookup for {city}"agent = create agent model="openai:gpt-5.5", tools= get weather , system prompt="Use tools when needed, then answer concisely.", result = agent.invoke { "messages": {"role": "user", "content": "Weather in Kolkata?"} } The expected behavior is that the model can choose the weather tool, receive its returned value, and then answer the user. Exact wording depends on the configured model and provider. Use create agent first when: Use custom LangGraph when you need: A reducer defines how an existing state value combines with a new update. Without a special reducer, a new update commonly replaces the previous value. Some fields instead need accumulation. For example, if a list field uses concatenation, conceptually: new list = old list + incoming items Here + means list concatenation, not numeric addition. "a" + "b" becomes "a", "b" . For chat-style agents, MessagesState provides a standard convenience state for maintaining a chronological sequence of user messages, model replies, tool requests, and tool results. Reducers matter even more when multiple branches update the same key. Parallel execution creates a state-design problem: simultaneous updates must have intentional merge behavior. LangGraph separates two kinds of persistence A checkpointer supports conversation continuity, resumption, human-in-the-loop flows, debugging or time-travel behavior, and fault recovery. A thread id identifies which thread state to load. python from langgraph.checkpoint.memory import InMemorySavercheckpointer = InMemorySaver graph = builder.compile checkpointer=checkpointer config = { "configurable": { "thread id": "user-42-chat-1" }}result = graph.invoke input state, config=config InMemorySaver is useful for examples, but it loses data when the process restarts. A production system that needs durable recovery should use a persistence backend suited to its deployment. A store solves a different problem: durable application information that should not be tied to only one conversation thread. An interrupt pauses graph execution and surfaces a JSON-serializable payload to the caller. This is useful when the workflow needs an approval or other external decision before continuing. python from langgraph.types import interruptdef approval node state : approved = interrupt { "question": "Approve refund?", "amount": state "refund amount" , } return {"approved": bool approved } To continue, the same thread is resumed with a Command resume=... value. The key implementation detail is easy to miss: when execution resumes, the interrupted node starts again from its beginning. That means side effects performed before interrupt should be idempotent, or better, moved after approval. Streaming lets a caller observe intermediate execution instead of waiting only for the final result. Depending on the API and stream mode, the caller may observe message chunks, state snapshots or updates, custom events, and interrupts. Streaming does not make the underlying work faster; it makes progress visible sooner and can make long-running agent execution easier to debug. LangGraph also provides Command, which can combine a state update with a routing decision from inside a node. It is useful when a node naturally produces both data and control flow. The source cautions against using it everywhere: ordinary edges keep simple flows easier to read. For dynamic parallel fan-out, Send can create downstream work items with different inputs. Subgraphs can package reusable workflows, teams of agents, or specialized stages behind a clear input/output contract. The PDF’s integrated example combines the main ideas into a refund workflow. It is presented as an original teaching example rather than an official policy. State might include: php START - understand request - load order - policy check / | \ ineligible small large | | | explain refund approval | | | END reply <- refund | END A model can turn free text such as “Please refund order A123; the item arrived damaged” into structured fields. This is a good model task because natural language varies. A deterministic service call should fetch the real order record. Do not let the model invent price, payment status, delivery state, or whether the order exists. python def policy check state : order = state "order" eligible = order "delivered" and order "days since delivery" <= 30 and not order "already refunded" return {"eligible": eligible} If the order was delivered eight days ago and has not already been refunded, all three Boolean conditions are true and eligible becomes True. In the teaching example, eligible refunds at or below Rs. 2,000 can proceed automatically, while larger refunds require human review. python def refund route state : if not state "eligible" : return "reject" if state "refund amount" <= 2000: return "auto refund" return "human review" For eligible=True and refund amount=1500, the route is "auto refund". For Rs. 3,500, the route is "human review". The graph pauses with an interrupt containing only the information a reviewer needs: order ID, amount, reason, and relevant policy facts. The same thread id must be reused when execution resumes. The refund API should accept an idempotency key such as refund: