How to Run LangGraph: A Practical Setup Guide for Developers

LangGraph is a framework for building stateful, multi-step AI applications as graphs of nodes and edges. If you have been working with large language models and found that simple chains break down once you need loops, branching, retries, or human-in-the-loop checkpoints, LangGraph is designed for exactly that problem. This guide walks you through installing the library, running your first graph locally, adding state and tools, and taking the result toward production. By the end, you will know how to run LangGraph in a Python project, how to debug a graph when it misbehaves, and which deployment options make sense for real applications.

Prerequisites and Installation

LangGraph is a Python library (with a JavaScript/TypeScript version also available), so you need a working Python environment first. Recent releases of langgraph and langchain-core require Python 3.10 or newer, so use 3.10+ as your baseline, and a virtual environment is strongly recommended so your dependencies stay isolated.

Install the core package with pip:

python -m venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
pip install -U langgraph langchain-core typing-extensions

You will also need an LLM to power the nodes. The most common setup uses a model provider integration such as langchain-openai or langchain-anthropic:

pip install langchain-openai
export OPENAI_API_KEY="your-provider-key"
export OPENAI_MODEL="your-available-chat-model"

If you prefer Anthropic's models, install langchain-anthropic and set ANTHROPIC_API_KEY instead. The graph structure itself is model-agnostic — the same graph can run against any chat model that supports the LangChain chat model interface, which is one of the main reasons teams choose this framework over hand-rolled orchestration code.

One common mistake at this stage: installing langgraph but forgetting a provider package, then getting a cryptic import error when instantiating the model. The graph library and the model bindings are deliberately separate, so both need to be present.

Running Your First Graph

The core mental model in LangGraph is a state graph: you define a shared state object, add nodes (functions that read and update that state), and connect them with edges. The graph starts at an entry point, executes nodes in order, and stops at a defined end.

Here is a minimal but complete runnable example:

import os
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model=os.environ["OPENAI_MODEL"])

class State(TypedDict):
    topic: str
    joke: str
    critique: str
    attempts: int

def generate_joke(state: State) -> dict:
    response = llm.invoke(f"Write a short joke about {state['topic']}")
    return {"joke": response.content, "attempts": state.get("attempts", 0) + 1}

def critique_joke(state: State) -> dict:
    response = llm.invoke(
        f"Assess this joke: {state['joke']}. Reply exactly ACCEPT if satisfactory; "
        "otherwise reply REVISE followed by a short reason."
    )
    return {"critique": response.content}

builder = StateGraph(State)
builder.add_node("generate", generate_joke)
builder.add_node("critique", critique_joke)

builder.add_edge(START, "generate")
builder.add_edge("generate", "critique")
builder.add_edge("critique", END)

graph = builder.compile()

result = graph.invoke({"topic": "software bugs"})
print(result["joke"])
print(result["critique"])

Save this as main.py and run it with python main.py. Set OPENAI_MODEL to a chat model available to your account; its name is configuration, not a fixed dependency of LangGraph. This example expects textual responses from that model. What happens under the hood: invoke() starts at START, runs the generate node, merges its returned dictionary into the state, runs critique, and returns the final state. Each node returns only the keys it wants to update — LangGraph merges those updates into the running state rather than requiring nodes to pass the whole object around.

To run the graph step by step instead of all at once, use streaming:

for update in graph.stream({"topic": "software bugs"}):
    print(update)

With the default updates mode, this example's stream() call yields node updates, which is invaluable for debugging because you can see exactly what state each node produced. This is one of the most practical habits to adopt early: streaming output makes graph behavior visible instead of forcing you to inspect only the final result.

Adding Loops, Conditional Edges, and Tools

Linear graphs are easy. The real power of LangGraph shows up when you add cycles — for example, a generate-critique-revise loop that runs until the output passes review, or a ReAct-style agent that decides at each step whether to call a tool or finish.

Conditional edges let the graph route dynamically based on the current state:

Rebuild the graph with a conditional route instead of adding it to the earlier builder. Keep the State, generate_joke, critique_joke, and llm definitions from main.py, and replace its builder/compile/invoke block with:

from typing import Literal

def should_continue(state: State) -> Literal["generate", "__end__"]:
    accepted = state["critique"].strip() == "ACCEPT"
    if accepted or state["attempts"] >= 3:
        return END
    return "generate"

loop_builder = StateGraph(State)
loop_builder.add_node("generate", generate_joke)
loop_builder.add_node("critique", critique_joke)
loop_builder.add_edge(START, "generate")
loop_builder.add_edge("generate", "critique")
loop_builder.add_conditional_edges("critique", should_continue)
loop_graph = loop_builder.compile()

result = loop_graph.invoke(
    {"topic": "software bugs", "attempts": 0},
    config={"recursion_limit": 12},
)
print(result["joke"], result["critique"])

There is no unconditional edge from critique to END in this builder. Mixing static and conditional outgoing edges can schedule both paths; a conditional route does not replace a static edge. The exact ACCEPT token avoids mistaking a phrase such as "not good" for approval, while the attempt counter limits generation to three calls per invocation. Reaching that limit means stopping for inspection, not declaring the result satisfactory. The recursion limit is a separate graph-step guard. See the official Graph API overview for routing and state-update semantics.

For a tool-calling agent, the current LangGraph v1 migration guide recommends LangChain's create_agent in place of the deprecated create_react_agent. Install langchain in the same environment, then run this example after the earlier llm definition:

pip install langchain
from langchain.agents import create_agent
from langchain_core.tools import tool

@tool
def get_word_length(word: str) -> int:
    """Returns the length of a word."""
    return len(word)

agent = create_agent(llm, tools=[get_word_length])
result = agent.invoke({"messages": [("user", "How long is the word 'serialization'?")]})
print(result["messages"][-1].content)

The agent handles tool-call routing and message accumulation. The selected model must support tool calling; still set execution budgets and handle tool failures in a real application. When your requirements outgrow the prebuilt agent — custom state fields, parallel branches, subgraphs — you drop down to the raw StateGraph API and build it yourself.

Persistence, Checkpointing, and Human in the Loop

A graph that cannot remember anything between runs is limited to single-turn interactions. LangGraph solves this with checkpointer objects that persist state after every step. The built-in InMemorySaver keeps everything in process memory, which is perfect for development:

from langgraph.checkpoint.memory import InMemorySaver

checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)

config = {"configurable": {"thread_id": "user-42"}}
graph.invoke({"topic": "databases", "attempts": 0}, config=config)
saved = graph.get_state(config)
print(saved.values["joke"])

The thread_id is the key concept here: every conversation or workflow run gets its own thread, and the checkpointer stores a snapshot of state after each node execution. This preserves the state your schema actually contains and supports inspection and resumption. Our example stores only the latest joke and critique; it does not retain a conversation transcript or automatically include earlier exchanges in model prompts. For conversational memory, define message state and an appropriate message reducer. The official persistence guide distinguishes thread-scoped checkpoints from cross-thread stores.

For production, swap InMemorySaver for a durable backend such as a Postgres-backed checkpointer, since in-memory state disappears on restart and cannot be shared across server instances. A checkpointer is infrastructure, not a substitute for deciding which data to retain or how to isolate users.

Checkpointing also enables human-in-the-loop workflows. A dynamic interrupt() pauses inside a node; resume it using Command(resume=...) and the same thread configuration. The node restarts on resumption, so side effects before the interrupt must be idempotent or isolated. Static before-node breakpoints have different semantics; the interrupt guide explains the distinction. This matters enormously for any workflow with real-world side effects — sending emails, executing database writes, or spending money — where you want a human gate before the irreversible step.

Running LangGraph in Production

A graph that works in a notebook is not the same as a graph that serves traffic. A few production considerations deserve attention before you deploy.

First, decide on your serving model. The simplest option is to embed the compiled graph in a standard web service — a FastAPI endpoint that calls graph.invoke() per request works fine for moderate traffic. For a development server, follow the official local-server setup, which requires Python 3.11+ for the langgraph-cli in-memory environment — a stricter requirement than the Python 3.10+ baseline of the library itself. Create a configured application rather than running the server in an arbitrary directory:

pip install -U "langgraph-cli[inmem]"
langgraph new ./langgraph-demo --template new-langgraph-project-python
cd langgraph-demo
pip install -e .

Follow the generated project's environment instructions, configure any provider credentials it needs, and export your compiled graph through its langgraph.json entry. The official server walkthrough also lists LangSmith credentials for its Studio flow. Then run langgraph dev from that configured project. Installing the CLI alone does not discover main.py or create the graph configuration. Keep demonstration code guarded with if __name__ == "__main__": so importing a graph module does not trigger paid model calls. Hosted deployment is a separate operational choice; check its current requirements before selecting it.

Second, control costs and latency. Every node that calls an LLM is a network round trip, and looping graphs can multiply that quickly. Set explicit recursion limits (graph.invoke(input, config={"recursion_limit": 25})) so a faulty loop terminates instead of burning tokens indefinitely. Cache deterministic tool results where possible, and use smaller models for simple nodes like routing or classification while reserving larger models for generation steps.

Third, handle errors deliberately. A node that raises an exception fails the whole run by default. Wrap flaky external calls in retries inside the node function, or model failure paths explicitly as edges — for example, routing to a "repair" node when a validation node rejects the output. Explicit failure handling in the graph structure is often clearer than try/except scattered through node code.

Finally, keep state small. Everything you put in the state object gets checkpointed after every step, so stuffing large documents or full message histories into state inflates both storage and serialization time. Store large artifacts externally (a database or object store) and keep only references or trimmed summaries in the graph state.

Frequently Asked Questions

Do I need to use LangChain to run LangGraph?

No. You can use the LangGraph graph API without adopting LangChain chains or agents. Model integrations such as the ChatOpenAI example use LangChain interfaces, but nodes can also call your own services or plain Python functions. The create_agent example in this guide does require LangChain, so install that additional package only when using it.

Can I run LangGraph in JavaScript or TypeScript instead of Python?

Yes. An official @langchain/langgraph package is available for JavaScript and TypeScript. Its installation, type definitions, and integrations differ from the Python examples here; consult the documentation for your language and required features rather than copying Python snippets into a Node.js project.

How is LangGraph different from just writing a Python loop around an LLM?

For a simple two-step workflow, a plain loop is fine and you should probably use one. LangGraph earns its complexity when you need durable state across runs, conditional branching, cycles with termination conditions, parallel node execution, human approval gates, or resumable workflows. It also gives you streaming, checkpointing, and visual debugging out of the box — features that are tedious to build correctly yourself.

What does a “thread” mean in LangGraph, and why do I need a thread_id?

A thread identifies a separate checkpoint history using thread_id in the graph configuration. Supply it when invoking a graph with a checkpointer, and use different IDs for unrelated workflows or users. Reusing an ID loads that thread’s saved state; it does not automatically create a conversation transcript or make the model remember information absent from its inputs.

Is LangGraph free to use?

The open-source LangGraph library has an MIT license. Running it yourself can still incur model, infrastructure, storage, and monitoring costs. Hosted services are separate products with their own terms; check those directly rather than assuming library licensing determines hosted-service pricing.

Conclusion

Running LangGraph comes down to four moves: install the library and a model provider, define a typed state with node functions, compile the graph, and invoke or stream it. From there, conditional edges give you branching, checkpointers give you memory and resumability, and the prebuilt agent helpers remove most of the boilerplate from tool-calling workflows. The best next step is small: take the generate-critique example above, use the separate bounded-loop builder, and run it with stream() so you can watch the state evolve. That single exercise will teach you more about the framework's execution model than any amount of reading, and it maps directly onto the agent patterns you will build later.


Transparency Note: Links in this article are not affiliate links. I recommend these tools and platforms based on their relevance to the topic, and no compensation is received for including them.

Privacy Overview

This website uses cookies so that we can provide you with the best user experience possible. Cookie information is stored in your browser and performs functions such as recognising you when you return to our website and helping our team to understand which sections of the website you find most interesting and useful.

Necessary

Strictly Necessary Cookie should be enabled at all times so that we can save your preferences for cookie settings.

Analytics

This website uses Google Analytics to collect anonymous information such as the number of visitors to the site, and the most popular pages.

Keeping this cookie enabled helps us to improve our website.