LangChain or LangGraph? Let create_agent's Return Value Decide
LangChain or LangGraph? Let create_age…
LangChain 1.4.3's create_agent is a LangGraph graph underneath; choice depends on who decides the next step.
The article uses five zero-cost scripts to verify that LangChain 1.4.3's `create_agent` returns a `CompiledStateGraph` with only `model` and `tools` nodes, showing every LangChain agent runs atop LangGraph. The selection rule: model decides next step → `create_agent`; code decides → LangGraph `StateGraph`; both → embed the agent as a node. It demonstrates tool loops, human approval via `HumanInTheLoopMiddleware` and checkpointer, retry caps via conditional edges, and nesting a sub-agent sharing `messages`. Ends with a quick reference table, common errors, and an FAQ.
LangChain or LangGraph? Let create_agent's Return Value Decide
In LangChain 1.4.3, create_agent does not return a chain — it returns a LangGraph graph. Printing its type gives CompiledStateGraph, and the node list contains just model and tools. This fact directly answers the division of labor between the two frameworks: whoever decides the next step decides which framework you should use.
If the model decides the next step, use create_agent. If your code decides the next step, write a LangGraph StateGraph. If you need both, drop the agent into the graph as a node.
This article uses five zero-cost scripts to demonstrate each case, all relying on a fake model that repeats fixed replies in sequence — no API key required. We only verify wiring logic like routing and approvals. The rule of thumb boils down to one sentence: if I can write down the step order and branches before the model runs, I write a graph; if the number and order of steps depend on what the model discovers, I use create_agent.
Environment Setup and Three Version Details
You need Python 3.10 or above — both LangChain and LangGraph declare this on PyPI. All tests in this article were done on Python 3.13, pinned to langchain 1.4.3, langgraph 1.2.13, and langchain-core 1.6.6.
The install command is pip install langchain==1.4.3 langgraph==1.2.13. Version pinning matters because the outputs and error messages come from these specific versions, and newer versions may differ. Installing langchain alone automatically pulls in langgraph as a dependency — you can confirm this from the Requires line in pip show langchain.
One key ingredient is a shared helpers.py file. It defines a scripted model that cycles through pre-set replies in order, plus two tools, lookup_order and refund. Each subsequent step is saved as a separate file (e.g., step1.py) and run from the same directory.
Step 1: First, See What create_agent Actually Returns
Tutorials for older versions say LangChain is chains plus AgentExecutor. In 1.4.3, AgentExecutor can no longer be imported, and the agent API has become create_agent. The official LangChain docs call it a "minimal, highly configurable harness," while the LangGraph docs describe themselves as a low-level orchestration framework and runtime for long-running, stateful agents.
I verified this claim directly. Building a minimal agent and printing its type and nodes:
agent = create_agent(model=scripted(), tools=[lookup_order])
print(type(agent))
print(list(agent.get_graph().nodes))
The output is <class 'langgraph.graph.state.CompiledStateGraph'>, with nodes ['__start__', 'model', 'tools', '__end__']. The model node calls the LLM, the tools node executes the tools the model requests, and the loop repeats until the model stops requesting tools.

This means a LangChain agent automatically gains LangGraph's checkpointing and interrupt capabilities — Step 3 tests this in practice. Middleware (functions that run at fixed points before and after model and tool calls) adds nodes to the graph: adding Step 3's approval middleware brings a fifth node into the graph, HumanInTheLoopMiddleware.after_model.
The comparison between the two libraries can be summarized in a table: LangChain's package name is langchain, you write models, tools, prompts, and middleware, and it depends on langgraph; LangGraph's package name is langgraph, you write state, nodes, and edges, and it only needs langchain-core.
One pitfall to watch out for: old tutorials import create_react_agent from langgraph.prebuilt, or AgentExecutor from langchain.agents. In the tested versions, the former is marked deprecated with a recommendation to switch to create_agent, and the latter cannot be imported at all. Any tutorial using either of these predates the current design. Pipeline-style chains (the LCEL syntax of prompt | model) still work, and they suit fixed call sequences with no loops.
How CompiledStateGraph relates to checkpointing and interrupt: CompiledStateGraph is the executable object LangGraph produces after compiling a graph definition. Compilation validates node and edge topology, binds runtime configuration, and mounts optional persistence and interrupt capabilities. Checkpointing means writing a snapshot of the current state to a checkpointer after each "super-step" (a batch of parallelizable nodes), enabling resume-from-breakpoint, time-travel debugging, and cross-call continuation; interrupt means pausing execution before or after a specified node, handing control back to the caller, and resuming once external input arrives. Both capabilities rely on LangGraph's state machine and checkpointer abstractions rather than anything the LangChain agent implements itself, so when using create_agent you get them for free just by passing a checkpointer — no hand-written graph needed.
Step 2: Write a Tool Loop with create_agent
If you can describe the task as "here's the goal, here are the tools, keep going until done," use create_agent. Customer service that looks up orders, a doc bot that searches a vector store, an assistant that files tickets — these are all fundamentally the same loop with different tools.
agent = create_agent(
model=scripted(tool_call("lookup_order", order_id="A1"), say("Order A1 has shipped.")),
tools=[lookup_order],
system_prompt="You answer order questions. Use the tool, never guess.",
)
After running, the thread contains four messages: the question, the tool call, the tool result, and the final answer. Switching to a real model just means replacing model=scripted(...) with a model string (e.g., "anthropic:claude-haiku-4-5"), installing the corresponding integration package, and setting environment variables.
Deep Agents is a third option, but this article does not test it — I'll just relay LangChain's official advice: when building or refactoring an agent, you can start with create_deep_agent, and choose create_agent when you need less built-in context management and finer loop control.
Step 3: Add Approval and Memory to the Agent
LangGraph docs list human in the loop and memory as core strengths, yet a single human approval requires no graph code at all. create_agent accepts checkpointer, store, interrupt_before, interrupt_after, and a middleware list.
The key code is this block:
agent = create_agent(
model=scripted(tool_call("refund", order_id="A1"), say("Refund issued.")),
tools=[refund],
middleware=[HumanInTheLoopMiddleware(interrupt_on={"refund": True})],
checkpointer=InMemorySaver(),
)
Binding a session with thread_id, the first invoke pauses the agent. __interrupt__ is the key that appears in the result when paused; its payload contains action_requests (the tool names and arguments to present) and review_configs (the allowed decisions per tool). Send the human's decision back with Command(resume={"decisions": [{"type": "approve"}]}).
Here the model still decides every step — middleware only decides whether the tool call it chose may run. Sending reject means the tool does not run, and the model receives a tool message with the rejection reason.
Before going to production, swap InMemorySaver for a database backend, or state is lost on restart. The checkpointer only covers a single thread; facts that should survive across sessions belong in a store.
One detail to note: current docs add version="v2" to the invoke call, returning a GraphOutput object with .value and .interrupts. Both versions were tested; the examples here use the default behavior, returning a dict with a __interrupt__ key.
Distinguishing checkpointer and store responsibilities: the checkpointer saves execution state for a "single thread," keyed by thread_id, used for interrupt recovery and multi-turn conversation within the same session; its lifecycle is usually bound to a session, and after a service restart you need a persistence backend (such as SQLite or Postgres) to retain it. The store, by contrast, is cross-thread long-term key-value storage for facts that should be shared across sessions — user preferences, historical ticket summaries, long-term memory entries. The two are often used together: the checkpointer makes the current conversation interruptible and resumable, while the store provides shared context for any thread. Mistakenly putting cross-session data in the checkpointer means it vanishes once you switch thread_id; conversely, putting transient state that needs interrupt recovery in the store loses the pause-point information.
Step 4: When Your Code Decides the Next Step, Move to StateGraph
Some tasks have branches and retries you can write down in advance. In LangGraph, state is a shared dict, nodes are functions that update it, and edges say which node runs next. Conditional edges pick the next node by calling your function, and they can point back to earlier nodes to form loops.
Here we demonstrate with a refund pipeline: the model extracts an amount, plain rules decide the outcome, and extraction failures retry up to three times before escalating to a human. This retry cap is a business rule, so it lives in code rather than in the prompt.
The graph structure is: START connects to extract, and extract branches three ways via add_conditional_edges — retry points back to itself, while approve and review each connect to END.
In the run results, the first request ($30) goes around the loop once, and the second reply reads 30 and is then approved by the rules — two model calls in total; the second request ($120) exceeds the threshold on the first try and goes straight to human review with only one model call. Because the scripted model replies in call order regardless of input text, this run only demonstrates control flow, not real extraction quality.
How state, nodes, and reducers cooperate: LangGraph state is a shared TypedDict or Pydantic model; node functions receive the current state and return a "state update dict," and the runtime merges these updates back into the state. The default merge is overwrite, but you can declare a reducer for a given key to change merge behavior — for example, using add_messages on a message list to deduplicate and append by id, or summing a counter. Conditional edges (add_conditional_edges) take a pure function that returns the next node name or END based on current state, so routing logic is expressed in plain Python — testable and diagrammable. Unlike prompt-driven routing, this consumes no model calls and is unaffected by model output variance, making it well suited to business rules like retry caps, threshold checks, and human fallbacks.
Step 5: When You Need Both, Drop the Agent into the Graph
A system can use both at once. Because create_agent returns a compiled graph, you can add it to a StateGraph as a node. It works because the agent and the graph share the messages key — MessagesState merges messages by id, so the agent's returned messages are not duplicated in the parent graph.
graph.add_node("triage", triage)
graph.add_node("agent", sub_agent) # a LangChain agent as a node
graph.add_node("template", template)
Urgent messages take the agent branch to look up orders, leaving four messages in the thread; normal messages take the template branch, which is pure Python, consumes no model calls, and leaves just two messages.

To draw the compiled graph yourself, call support.get_graph().draw_mermaid_png() — it sends the graph text to the mermaid.ink service, so it requires network access.
MessagesState and the add_messages reducer: MessagesState is a state structure LangGraph predefines for conversation scenarios, centered on the messages key whose reducer is add_messages. This reducer merges by message id: if a new message's id already exists it replaces, otherwise it appends, preventing message duplication when a nested graph shares messages with a parent graph. A sub-agent (a graph compiled by create_agent) and the parent StateGraph both read and write the same messages key, and intermediate messages inside the sub-graph also flow back into the parent state, so embedding an agent as a node needs no extra adaptation code. If you use a custom state, declaring add_messages for the message field gives you the same id-based merge semantics.
LangChain vs LangGraph Quick Reference

| Your need | Choice |
|---|---|
| Model calls tools until the task is done | create_agent |
| Same, but needs human approval for risky tools | create_agent plus HumanInTheLoopMiddleware and a checkpointer |
| Long-running open-ended tasks needing summarization and sub-agents | create_deep_agent |
| Fixed steps, some with LLM calls | LangGraph StateGraph |
| Your own routing before and after the agent | StateGraph, with create_agent as a node |
| Tracing and evaluation for all of the above | LangSmith |
Middleware lets your code hook into fixed points around model and tool calls, but the model still drives the loop; StateGraph gives your code ownership of routing. LangChain's official docs draw this line clearly: when built-in hooks aren't enough, LangGraph is the escape hatch for building fully custom graphs.
In practice, when should you leave create_agent plus middleware behind? When you need a step that must run no matter what the model says, a retry loop with a built-in cap, or fixed routing between several agents. If the whole task is two or three fixed model calls in sequence with no loops, I'd use neither — just write plain functions or a pipeline chain.
It's worth noting that these tests all use a scripted model to verify wiring; the rules should serve as heuristics, not hard conclusions.
Common Error Troubleshooting
ImportError: cannot import name 'AgentExecutor': 1.4.3 no longer exports it from langchain.agents. Switch to create_agent, or pip install langchain-classic and import from langchain_classic.agents. create_react_agent is also marked deprecated.
RuntimeError: Cannot use Command(resume=...) without checkpointer: the agent paused but there's no checkpoint to resume from. Pass checkpointer=InMemorySaver() and invoke again with the same thread_id. The trap is that the first call pauses fine even without a checkpointer; the error only surfaces on resume.
TypeError: 'bool' object is not subscriptable: you passed Command(resume=True). Middleware expects the dict format {"decisions": [{"type": "approve"}]}, one decision per pending action, in order.
Agent paused with no follow-up: the first invoke returns early, and it only continues when called again with a Command. Check "__interrupt__" in result after each call.
FAQ
Should I learn LangChain or LangGraph first? Learn create_agent first. LangGraph's own docs recommend LangChain agents for people just getting started with agents or wanting a higher-level abstraction, and since create_agent runs on LangGraph, you'll pick up state, checkpointing, and interrupts along the way. Move to StateGraph when you need your code to choose the next step.
Is LangGraph still relevant? Yes — it's the runtime underneath every LangChain agent. LangChain 1.4.3 depends on it, so every create_agent agent is a LangGraph graph. Its release cadence is also dense: according to PyPI history, six versions shipped between July and October.
Is LangGraph part of LangChain? In practice, yes. Both live under the langchain-ai GitHub organization and share a documentation site operated by LangChain, but they are separate PyPI packages. langchain depends on langgraph, not the other way around.
Do I have to use LangChain to use LangGraph? No. LangGraph's docs explicitly say LangGraph doesn't require LangChain, even though its examples use LangChain components for models and tools. Nodes can be plain Python functions, just like the Step 4 pipeline — although LangGraph does depend on langchain-core underneath.
Where does RAG fit? RAG is a pattern you can build with either library: a tool or node that searches documents and hands the results to the model. A doc Q&A bot is a great fit for create_agent plus a search tool, while a pipeline with fixed retrieval and scoring steps suits a graph.
One-line Summary
LangChain 1.4.3's create_agent is a LangGraph graph underneath; which to use depends on who decides the next step.
Paragraph Summary
Through five zero-cost scripts, the article verifies that LangChain 1.4.3's create_agent actually returns a CompiledStateGraph with only model and tools as nodes, showing every LangChain agent runs on top of LangGraph. From this comes the selection rule: if the model decides the next step, use create_agent; if your code decides, use a LangGraph StateGraph; if both, embed the agent as a node. The article demonstrates a tool loop, human approval and interrupt recovery via HumanInTheLoopMiddleware and a checkpointer, retry caps via conditional edges, and embedding a sub-agent into a parent graph sharing messages. It ends with a quick reference table, common errors (AgentExecutor import failure, missing checkpointer, resume needing a dict format), and an FAQ, noting the conclusions are based on a scripted model and serve only as heuristics.
Related articles

Amphibious Transforming Drone: Beyond 100x Power Savings, What Are the Real Needs?
Is the amphibious transforming drone just a gimmick? Starting from the 100x power difference of the M4 Morphobot, explore real application scenarios and thesis topic ideas, analyzing boundary conditions where pure drones and pure ground robots both fail.

Warhound Raises $200 Million: Genetically Engineered "War Dogs" Paired with Anduril Gear
Warhound raises $200M, backed by Ben Lamm and Palmer Luckey, to genetically engineer dogs for military and law enforcement use and equip them with Anduril gear. This article analyzes its technology direction, funding logic, and ethical controversy.

Claude API Comes to Google Cloud: IAM Integration and SDK Access Guide
Anthropic hosts the full Claude API on Google Cloud with IAM-controlled permissions and unified billing. This guide covers the Vertex AI and Anthropic-hosted entry points, IAM role configuration, and SDK access.