Contents · 16 sections↓
- 01What is the best LangGraph alternative?
- 02Why do developers look for LangGraph alternatives?
- 03How LangGraph works, and what an alternative must replace
- 04When to stay with LangGraph (and when to switch)
- 057 LangGraph alternatives and when each fits
- 06What you need to run the examples
- 07Step 1: Write the tools once
- 08Step 2: Build the agent with LangGraph's StateGraph
- 09Step 3: Build the same agent with Promptise
- 10Step 4: Keep explicit routing with a Promptise graph
- 11How do you migrate from LangGraph to Promptise?
- 12Running it in production
- 13Common problems and fixes
- 14Limitations
- 15Frequently asked questions
- 16Next steps
LangGraph is the most widely used graph framework for AI agents, and also one that many teams start to question once their agent works. This guide is for developers who use LangGraph today, or are about to, and want to know their options. It covers why people look for LangGraph alternatives, seven alternatives and when each one fits, the same agent written in LangGraph and in Promptise Foundry, and a concept map for migrating node by node.
What is the best LangGraph alternative?
It depends on why you are leaving. If the graph feels like too much code for a tool-calling agent, pick a framework where the loop is built in: Promptise Foundry, Pydantic AI or the OpenAI Agents SDK. If you like graphs but want another ecosystem, Microsoft Agent Framework workflows, Pydantic Graph, LlamaIndex Workflows and Google ADK all have graph or workflow APIs. If you want a team of role-based agents, CrewAI. And if you depend on durable runs that pause for days, check carefully, because that is where LangGraph is strongest.
Most agents don't need an explicit graph. A model, tools, an approval rule and a step limit cover a large share of real agents.
LangGraph's checkpointers and interrupts are hard to replace. Few alternatives let a paused run wait in a database and resume after a restart.
Your tools can move unchanged. LangChain @tool functions work in Promptise as they are, and MCP servers work in every framework here.
Migrate one agent at a time, and run the same evals before and after.
For a full feature table across frameworks, see Python AI agent frameworks compared.
[02]
Why do developers look for LangGraph alternatives?
The reasons people give are consistent across GitHub issues, forums and Hacker News threads. None of them means LangGraph is a bad tool. They mean it was built for a kind of control that not every agent needs.
Abstraction for simple agents. A tool-calling agent in the Graph API needs a state schema, nodes, a conditional edge, a tool node and a compile step. On the LangChain forum, users ask why simple model calls need this many layers. On Hacker News, one developer reports replacing an inherited LangGraph setup with plain function calls and deleting over a thousand lines.
Boilerplate for branching logic. Commenters describe conditional graphs as tedious to wire and read (HN).
Debugging state and limits. Users report confusion about the recursion limit (langchain-ai/docs #1121) and InvalidUpdateError when parallel branches write the same state key (langgraph #2336, #6446). The recursion limit counts graph steps, not tool calls, which surprises people.
Coupling to LangSmith. The library is MIT-licensed and runs without LangSmith, and tracing is off unless you turn it on. But LangGraph Studio needs a LangSmith account and API key, and the managed deployment option is part of LangSmith's paid plans. Some teams prefer not to build on that path.
People also praise LangGraph, for the same reasons: typed state, checkpointing, interrupts that survive restarts, streaming, and full control over every step. Teams building coding agents and long-running workflows choose it for exactly that (HN).
[03]
How LangGraph works, and what an alternative must replace
LangGraph models an agent as a state machine. You define a state schema, often MessagesState, which holds the conversation. Nodes are functions that read the state and return updates. Edges decide which node runs next, either always or based on a condition. You compile the graph, optionally with a checkpointer, which saves the state after each step under a thread_id.
┌──────────────── state (messages, your keys) ───────────────┐
│ │
START ──▶ [agent] ──tools_condition──▶ [review] ──▶ [tools] ──▶ back to [agent]
│ │
│ no tool calls │ interrupt(): run pauses, state saved
▼ ▼ by the checkpointer
END Command(resume=...) continues the same thread_idAny alternative has to give you five things this picture gives you:
1. The tool loop: call the model, run the tools it asks for, repeat. 2. Control flow: routing, branches and loops you can read. 3. State: what each step can see, and what persists between turns. 4. Pauses: a way to stop before a risky action and continue after a decision. 5. Limits and visibility: a cap on steps and a trace of what happened.
Most frameworks provide the first, third and fifth in a few lines. They differ most on the second and the fourth, and that is where to focus your evaluation.
[04]
When to stay with LangGraph (and when to switch)
Stay with LangGraph when:
Approvals wait hours or days, and the run must survive restarts while it waits.
Your workflow really is a graph: many branches, parallel fan-out, human steps in the middle, retries per node.
You already use LangSmith for tracing and evaluation, and it works for you.
Consider switching when:
Most of your graphs are the same agent → tools → agent loop with small variations.
New team members struggle to read the graph code, or debugging time goes to state plumbing rather than agent behavior.
You need features around the agent that LangGraph leaves to you, such as an MCP server framework, budgets, guardrails or a project runner.
You want to avoid a hosted tool for local debugging.
A partial switch is also an option. Keep LangGraph for the one workflow that needs durable pauses, and write the simpler agents in something lighter.
[05]
7 LangGraph alternatives and when each fits
Versions checked on 18 October 2026. Each summary comes from the project's own documentation.
Framework | Core idea | Graph or workflow API | Fits best when |
|---|---|---|---|
Promptise Foundry 1.4.1 | MCP-first agents with approvals, budgets, guardrails and a runtime | Reasoning Graph (PromptGraph) | You want the tool loop built in, plus the production pieces in one package |
Pydantic AI 2.57.0 | Typed agents with validated inputs and outputs | Pydantic Graph | You want type safety and usage limits |
OpenAI Agents SDK 0.24.0 | Agents, handoffs, guardrails, sessions | No | You mostly use OpenAI models and want the shortest path |
Microsoft Agent Framework 1.22.0 | Successor to AutoGen and Semantic Kernel | Workflows (WorkflowBuilder) | You need a graph with checkpoints in a Microsoft or Azure stack |
LlamaIndex Workflows 2.26.0 | Event-driven steps with a shared context | Workflows (@step) | Your agent is mostly retrieval over your documents |
CrewAI 1.15.28 | Role-based crews of agents and tasks | Flows | You think in teams of agents with roles |
Google ADK 2.12.0 | Agents plus sequential, parallel, loop and graph workflows | Workflow agents and graphs | You build on Gemini and Google Cloud |
Promptise Foundry
Promptise builds an agent from MCP servers or LangChain tools with one call, build_agent(), and runs the tool loop for you. Approvals are an ApprovalPolicy with a handler, step limits are max_agent_iterations, and conversation history goes in a ConversationStore (memory, SQLite, Postgres or Redis). When you do want explicit control flow, the Reasoning Graph gives you nodes, conditional edges and prebuilt patterns. The same package includes an MCP server framework and promptise run for running and deploying projects. The trade-off: approvals are answered in the running process, so a pending approval does not survive a restart.
Pydantic AI
Pydantic AI treats an agent as a typed function: validated dependencies in, validated output out. It has token and tool-call limits, deferred tools for approvals that resume from message history, and durable execution through Temporal, DBOS or Prefect. For explicit state machines it offers Pydantic Graph. A good fit for teams that already like Pydantic and want strict types.
OpenAI Agents SDK
The OpenAI Agents SDK is a small library around agents, handoffs between agents, guardrails and sessions. Approvals pause a run and return a serializable RunState. It works with other providers through adapters, but its tracing exports to OpenAI by default. A good fit when you want handoffs and minimal code on OpenAI models.
Microsoft Agent Framework
Microsoft Agent Framework is the successor to AutoGen and Semantic Kernel, released as 1.0 in April 2026. Its workflows are the closest thing here to StateGraph: executors connected by edges, with superstep checkpoints and human-in-the-loop requests. It supports Python and .NET. A good fit when you want a graph with checkpoints and your organization runs on Azure.
LlamaIndex Workflows
LlamaIndex Workflows are event-driven: each @step method takes an event and emits the next one, with a shared Context you can serialize to pause and resume. They sit next to LlamaIndex's retrieval tools. A good fit for agents that mainly search and reason over your documents.
CrewAI
CrewAI describes agents by role, goal and backstory and groups them into crews that work through tasks. Its Flows add event-driven steps (@start, @listen, @router) with state persistence. A good fit when the problem maps naturally to a team. Note that it sends anonymous telemetry unless you set CREWAI_DISABLE_TELEMETRY=true.
Google ADK
The Agent Development Kit has LLM agents plus workflow agents that run sub-agents in sequence, in parallel or in a loop, and ADK 2.0 adds graph-based workflows. It works with other models but is designed around Gemini and Google Cloud. A good fit for teams already on that platform.
Two more options deserve a mention. smolagents, from Hugging Face, is a minimal library whose main agent writes Python code as its actions. And for a single agent with two or three tools, no framework at all is a fair choice: how to build an AI agent in Python writes the loop by hand.
[06]
What you need to run the examples
Python 3.10 or later and an OpenAI API key. Both examples use gpt-5-mini.
Two virtual environments, so the versions stay clean:
pip install "langgraph==1.2.16" "langchain==1.4.6" "langchain-openai==1.7.2" # LangGraph
pip install "promptise==1.4.1" # PromptiseThe example is an order-support agent with two tools. It refunds orders, but a refund over 100 USD must stop for a person, who in this example says no.
[07]
Step 1: Write the tools once
Both versions share the same LangChain tools. Keeping tools outside the agent code is the first step of any migration: the tools don't change, only the code around them.
"""Two LangChain tools both versions share: a lookup and a refund."""
from langchain_core.tools import tool
ORDERS = {
"O-1001": {"item": "Desk lamp", "amount": 49.00, "status": "delivered"},
"O-1002": {"item": "Office chair", "amount": 219.00, "status": "delivered"},
}
@tool
def get_order(order_id: str) -> dict:
"""Look up an order by ID: item, amount in USD and status."""
return ORDERS.get(order_id.strip().upper(), {"error": f"No order {order_id}"})
@tool
def refund_order(order_id: str, amount: float) -> dict:
"""Refund a delivered order. Moves money."""
return {"refunded": True, "order_id": order_id, "amount": amount}[08]
Step 2: Build the agent with LangGraph's StateGraph
This is the LangGraph version, written with the Graph API so every part is visible. A review node sits between the model and the tools. It pauses with interrupt() before a refund over 100 USD.
"""A support agent written with LangGraph's Graph API."""
from langchain.chat_models import init_chat_model
from langchain_core.messages import SystemMessage, ToolMessage
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, MessagesState, StateGraph
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.types import Command, interrupt
from tools import get_order, refund_order
PROMPT = "You are an order-support agent. Use the tools. Answer in one sentence."
tools = [get_order, refund_order]
model = init_chat_model("openai:gpt-5-mini").bind_tools(tools)
def call_model(state: MessagesState):
return {"messages": [model.invoke([SystemMessage(PROMPT), *state["messages"]])]}
def review(state: MessagesState):
"""Pause before a refund over 100 USD and wait for a person's decision."""
call = next((c for c in state["messages"][-1].tool_calls if c["name"] == "refund_order"), None)
if call and call["args"]["amount"] > 100:
if interrupt({"question": "Approve this refund?", "args": call["args"]}) != "approve":
denied = ToolMessage("DENIED: refunds over 100 USD need a manager.", tool_call_id=call["id"])
return Command(goto="agent", update={"messages": [denied]})
return Command(goto="tools")
builder = StateGraph(MessagesState)
builder.add_node("agent", call_model)
builder.add_node("review", review)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", tools_condition, {"tools": "review", END: END})
builder.add_edge("tools", "agent")
graph = builder.compile(checkpointer=InMemorySaver()) # interrupts need a checkpointer
config = {"configurable": {"thread_id": "ticket-42"}, "recursion_limit": 12}
question = {"messages": [{"role": "user", "content": "Order O-1002 arrived broken. Refund it in full."}]}
result = graph.invoke(question, config)
if "__interrupt__" in result: # the run paused in `review`
print("paused:", result["__interrupt__"][0].value)
result = graph.invoke(Command(resume="reject"), config) # a person said no
print(result["messages"][-1].content)paused: {'question': 'Approve this refund?', 'args': {'order_id': 'O-1002', 'amount': 219}}
I attempted the full $219 refund for order O-1002 but it was denied because refunds over $100 require manager approval—would you like me to escalate to a manager now, or would you prefer a replacement or a return shipping label instead?What each part does:
MessagesState holds the conversation, and its reducer appends new messages.
tools_condition routes to review when the model asked for tools, and to END when it answered.
interrupt() stops the run and saves the state through the checkpointer. The caller sees __interrupt__ and resumes with Command(resume=...) on the same thread_id.
On a rejection, review adds a ToolMessage that says DENIED, so the model gets a result for its tool call and can answer.
recursion_limit caps the number of graph steps.
The strength of this design shows when the state lives in a database: swap InMemorySaver for PostgresSaver, and the paused run can wait for days and resume in another process. The cost is the wiring: you own the state, the routes and the resume logic. LangChain's create_agent with HumanInTheLoopMiddleware hides most of this, as the framework comparison shows.
[09]
Step 3: Build the same agent with Promptise
In Promptise the loop is built in. The approval rule becomes a handler on an ApprovalPolicy, the thread becomes a session_id on a conversation store, and the recursion limit becomes max_agent_iterations.
"""The same support agent in Promptise: the loop, the gate and the history are settings."""
import asyncio
from promptise import build_agent
from promptise.approval import ApprovalDecision, ApprovalPolicy
from promptise.conversations import InMemoryConversationStore
from tools import get_order, refund_order
PROMPT = "You are an order-support agent. Use the tools. Answer in one sentence."
async def review(request):
"""Runs before every refund_order call. Small refunds pass; a person decides the rest."""
if request.arguments["amount"] <= 100:
return ApprovalDecision(approved=True)
print("paused:", {"question": "Approve this refund?", "args": request.arguments})
return ApprovalDecision(approved=False, reason="Refunds over 100 USD need a manager.") # a person said no
async def main():
agent = await build_agent(
model="openai:gpt-5-mini",
instructions=PROMPT,
extra_tools=[get_order, refund_order], # your LangChain tools, unchanged
approval=ApprovalPolicy(tools=["refund_order"], handler=review, timeout=300),
conversation_store=InMemoryConversationStore(), # history per session_id
max_agent_iterations=6, # at most 6 model calls
)
try:
print(await agent.chat("Order O-1002 arrived broken. Refund it in full.", session_id="ticket-42"))
finally:
await agent.shutdown()
asyncio.run(main())paused: {'question': 'Approve this refund?', 'args': {'order_id': 'O-1002', 'amount': 219.0}}
I attempted the full $219 refund for order O-1002 but it was denied because refunds over $100 require manager approval—shall I escalate this to a manager now to complete the refund?What changed:
No graph code. build_agent() builds the default reasoning loop (call the model, run tools, repeat).
The tools are the same objects. extra_tools= takes LangChain tools unchanged. Tools on MCP servers go in servers= instead.
The pause is a function call. The handler runs before every refund_order call and returns a decision. A denied call reaches the model as DENIED: <reason>, which is what the LangGraph version built by hand. In production the handler would post to Slack, a web UI or a webhook and wait, up to timeout seconds.
History is a store. agent.chat(..., session_id=...) loads and saves the conversation. Swap InMemoryConversationStore for SQLiteConversationStore or PostgresConversationStore to keep it across restarts.
The difference that matters for some teams: the handler waits inside the running process. If the process restarts while a person is deciding, the pending approval is gone, and the user has to ask again. LangGraph's checkpointed interrupt doesn't have that limit. Human-in-the-loop approval for AI agents covers handlers, timeouts and audit logs.
[10]
Step 4: Keep explicit routing with a Promptise graph
Some LangGraph graphs exist for a reason: a router in front of the agent, a validation step after it, a branch to a cheaper model. You can keep that structure. Promptise's Reasoning Graph has nodes, conditional edges and an entry point, like StateGraph.
"""An explicit graph, for when you want to keep LangGraph-style routing."""
import asyncio
from pydantic import BaseModel
from promptise import build_agent
from promptise.engine import PromptGraph, PromptNode
from tools import get_order, refund_order
class Triage(BaseModel):
about_an_order: bool
graph = PromptGraph("support", mode="static")
graph.add_node(PromptNode("triage", output_schema=Triage,
instructions="Decide whether the message is about an order."))
graph.add_node(PromptNode("agent", inject_tools=True, is_terminal=True,
instructions="You are an order-support agent. Use the tools. Answer in one sentence."))
graph.add_node(PromptNode("decline", is_terminal=True,
instructions="Reply only: 'Sorry, I can only help with orders.' Do not do the task."))
graph.on_output("triage", "agent", key="about_an_order", value=True)
graph.on_output("triage", "decline", key="about_an_order", value=False)
graph.set_entry("triage")
async def main():
agent = await build_agent(model="openai:gpt-5-mini", agent_pattern=graph,
extra_tools=[get_order, refund_order])
try:
for text in ["What's the status of O-1001?", "Write me a poem about Mondays."]:
result = await agent.ainvoke({"messages": [{"role": "user", "content": text}]})
print(f"{text} -> {result['messages'][-1].content}")
finally:
await agent.shutdown()
asyncio.run(main())What's the status of O-1001? -> Order O-1001 (Desk lamp) has been delivered; total $49.00.
Write me a poem about Mondays. -> Sorry, I can only help with orders.The triage node returns structured output (output_schema=Triage), and on_output routes on one of its fields, like add_conditional_edges with a routing function. One difference to know: a PromptNode with inject_tools=True runs the whole tool loop inside the node. So LangGraph's agent + ToolNode + tools_condition trio becomes one node, and you only draw the edges between larger steps.
[11]
How do you migrate from LangGraph to Promptise?
Map each LangGraph concept to its Promptise equivalent, then move one agent at a time.
LangGraph | Promptise equivalent | Notes |
|---|---|---|
StateGraph(MessagesState) | build_agent() for a tool loop; PromptGraph("name", mode="static") for an explicit graph | Most tool-calling graphs become one build_agent() call |
State schema and reducers | GraphState: messages plus a context dict; output_key= writes a node's output to context | No reducers to declare; messages append automatically |
add_node(name, fn) | PromptNode, TransformNode (no model call), @node decorator, BaseNode subclass | graph.add_node(...) |
add_edge(a, b) | graph.always(a, b) or graph.sequential(a, b, c) | |
add_conditional_edges(...) | graph.when(a, b, condition=...), graph.on_output(...), graph.on_tool_call(...) | Conditions take the node's NodeResult |
START / END | Rendering diagram… | |
ToolNode + tools_condition | PromptNode(..., inject_tools=True) | The tool loop runs inside the node |
compile(checkpointer=...) + thread_id | conversation_store= + agent.chat(..., session_id=...) | Stores the conversation between turns, not mid-run state |
interrupt() + Command(resume=...) | | Waits in-process up to timeout (max 24 hours); not durable across restarts |
recursion_limit | max_agent_iterations | Counts node executions; with the default loop, one per model call |
Subgraphs | SubgraphNode | |
Send / map-reduce | ParallelNode, FanOutNode | |
stream() / astream() | agent.astream(), agent.astream_with_tools() | |
langchain-mcp-adapters | | MCP is built in |
LangSmith tracing | observe=True (local HTML report) or ObservabilityConfig with OTLP or Prometheus | Opt-in, no account needed |
LangChain chat models | The same models: pass a model instance or a "provider:model" string |
A safe order for the migration:
1. Freeze behavior. Write eval cases for the current agent: the tools it must call, the actions it must never take, the facts its answers must contain. How to evaluate AI agents in Python shows how to build them. 2. Move the tools first. Keep your @tool functions and pass them as extra_tools, or move them to an MCP server so any framework can use them. 3. Replace the loop. Start with build_agent() and your system prompt. Add the graph back only where a route or a step really needs it. 4. Move the approvals. Turn each interrupt() into a handler rule. Decide what happens on timeout: deny is the safe default. 5. Move the history. Map each thread_id to a session_id in a conversation store. 6. Drain, then switch. Checkpoints and paused interrupts don't transfer. Let open runs finish in LangGraph, and send new conversations to the new agent. 7. Run the same evals against both versions until the new one matches.
[12]
Running it in production
Long approvals
If a person may take longer than your process lives, design for it. Either keep that workflow in a framework with durable pauses (LangGraph, Pydantic AI, the OpenAI Agents SDK or Microsoft Agent Framework), or make the action itself asynchronous: the agent files a request through a tool, and a separate system executes it after approval. The second design works in any framework.
Tracing without a hosted service
Promptise records model calls and tool calls when you pass observe=True, and writes an HTML report when the agent shuts down. For a central backend, send spans over OTLP to your own collector. AI agent observability shows what to record.
Limits that raise
max_agent_iterations stops a run that reaches the limit, and raises GraphIterationLimitError if the model was still waiting for tool results. Test that your code handles it. AI agent stuck in a loop? covers loop detection.
Pin and test
Pin the framework and its dependencies, including mcp, and run your evals after every upgrade, in whichever framework you choose.
[13]
Common problems and fixes
Symptom | Cause | Fix |
|---|---|---|
LangGraph raises GraphRecursionError on a normal task | The limit counts graph steps, and each tool round trip is several steps | Raise recursion_limit in the config, or cap model calls instead |
An interrupt never pauses the run | No checkpointer, or the resume uses a different thread_id | Compile with a checkpointer and reuse the same config |
The model errors after a rejected tool call | The tool call has no ToolMessage answer | Add a DENIED ToolMessage, as review does, or use Promptise's gate, which adds it |
A Promptise graph raises "has no entry node" | Static graphs need an explicit entry | Call graph.set_entry("name") |
A terminal node answers the user's request anyway | Its instructions are too soft | Tell the node exactly what to reply and what not to do |
A pending approval is lost after a deploy | Promptise approval handlers wait in-process | Drain before deploys, or make the action asynchronous |
Tools behave differently after the migration | Tool descriptions or argument names changed | Keep the tool objects unchanged and compare eval results |
[14]
Limitations
This is not a benchmark. It compares documented features and shows one agent. With the same model and tools, answers depend mostly on the model.
Promptise approvals are not durable. A pending approval does not survive a restart, and Promptise has no equivalent of resuming a run from a mid-run checkpoint. Its runtime journal records process state for crash recovery between invocations, not inside one.
Graph semantics differ. A Promptise PromptNode with tools runs a full tool loop, while a LangGraph node is one function call. Very fine-grained graphs need rethinking, not a line-by-line port.
Features move quickly. Check the docs of the versions you install.
We make Promptise. We checked every claim about other frameworks against their documentation and pointed out where LangGraph is the better choice.
[15]
Frequently asked questions
What is the best alternative to LangGraph?
It depends on what you need. For tool-calling agents with less code, Promptise Foundry, Pydantic AI or the OpenAI Agents SDK. For explicit graphs in another ecosystem, Microsoft Agent Framework workflows, Pydantic Graph, LlamaIndex Workflows or Google ADK. For role-based teams of agents, CrewAI. If you need runs that pause for days and resume after restarts, LangGraph remains one of the strongest options.
Is LangGraph overkill for simple agents?
Often, yes. A single agent that calls a few tools needs a model, the tools, a stop condition and a step limit. LangGraph's Graph API asks you to also define state, nodes and edges. LangChain's create_agent reduces that, and frameworks like Promptise or Pydantic AI build the loop in. The graph pays off when the workflow has real branches, parallel steps or long human pauses.
Does LangGraph require LangSmith?
No. LangGraph is an MIT-licensed library that runs without LangSmith, and tracing is off unless you enable it. LangGraph Studio, the visual debugger, needs a LangSmith account and API key, and the managed deployment option is part of LangSmith's paid plans. Other tracing tools work through LangChain callbacks or OpenTelemetry.
Can I migrate from LangGraph without rewriting my tools?
Yes. LangChain @tool functions work in Promptise through extra_tools= without changes, and tools served over MCP work in every major framework. What you rewrite is the code around the tools: the graph, the approval and resume logic, and the history handling. Checkpointed threads and paused runs don't transfer, so let them finish first.
What replaces LangGraph's interrupt in Promptise?
An ApprovalPolicy on the agent. You list the tools that need approval and give a handler that returns approve or deny. The handler can ask a person through Slack, a web UI or a webhook, and the run waits up to a timeout. Unlike a checkpointed interrupt, the wait happens in the running process, so it does not survive a restart.
Is LangGraph better than CrewAI?
They solve different problems. LangGraph gives you a low-level graph with persisted state and precise control over every step. CrewAI gives you a higher-level model of agents with roles working through tasks, plus Flows for event-driven steps. For a single agent with strict approval and limits, LangGraph gives more control. For a team of cooperating agents, CrewAI needs less code.
[16]
Next steps
Compare seven frameworks feature by feature: Python AI agent frameworks compared.
Protect the migration with tests: how to evaluate AI agents in Python.
Design approvals that people actually answer: human-in-the-loop approval for AI agents.
Move your tools to MCP: build a production MCP server in Python.
Reference: the Reasoning Graph, approval and conversation persistence docs.