LangGraph create_react_agent Binding Errors
create_react_agent is the shortest path to a working agent — until the model doesn't call your tools, or drops the system prompt, or crashes on a subclassed state. Here are the binding failures and the corrections that unblock the prebuilt path.
By Sana K. · Last updated Aug 14, 2026 · LangGraph · Page #130
create_react_agent failures are almost always about (1) using a model that doesn't support tool calling natively, (2) passing tools that aren't @tool-decorated or BaseTool instances, or (3) subclassing the state schema in a way that omits messages. Fix by picking a tool-calling model, wrapping every callable with @tool, and starting from AgentState when you need extra fields.Real error messages you'll see
ValueError: Model claude-instant-1.2 does not support tool calling. create_react_agent requires a model with .bind_tools().
at create_react_agent()
# Old / small models lack tool_use. Use a recent flagship model (claude-opus-4-7, gpt-4.1, gemini-2.5) or pin an explicit tool-using variant.
TypeError: Tool must be a BaseTool or a callable decorated with @tool. Got: <function search at 0x...>
at create_react_agent(tools=[search])
# Passing a bare function. Wrap with @tool or convert with tool(func) from langchain_core.tools.
langgraph.errors.InvalidUpdateError: State schema missing required field 'messages'.
at create_react_agent(state_schema=MyState)
# Subclassed AgentState but removed the messages field. Every ReAct agent state needs messages with add_messages reducer.
create_react_agent argument reference
| Argument | Type | Notes |
|---|---|---|
model | BaseChatModel with bind_tools | Must support native tool calling |
tools | list[BaseTool] | Each item is @tool-decorated or a BaseTool subclass |
prompt (or state_modifier) | str, SystemMessage, or callable | Prepended to messages before every model call |
state_schema | type[AgentState] subclass | Defaults to AgentState. Must include messages field |
checkpointer | BaseCheckpointSaver | Optional; required for resumable agents |
interrupt_before / interrupt_after | list[str] | Pause execution at named nodes for HITL |
response_format | Pydantic model or schema | Force structured final answer |
Root causes (ranked by frequency)
Based on LangGraph developer reports; percentages sum to 100%.
- 25%Model doesn't support tool calling. Legacy or small models (
claude-instant,gpt-3.5-turbo-instruct, base Llama variants without a chat-template tool head) can't be bound to tools. Prebuilt raises immediately. - 19%Passing raw functions instead of
BaseTool.def search(q): ...passed directly. LangChain needs a docstring + schema for the model to call it. Wrap with@tool. - 15%Subclassed state missing
messages.class MyState(TypedDict): question: strpassed asstate_schemawithout a messages field. Agent has no history channel. - 11%Prompt overrides all system context. Passing
prompt="You are a bot."replaces any tool-usage instructions the model was auto-primed with. Model may then refuse to call tools. - 10%Wrong tool schema — args_schema mismatch. Tool declares
args_schemabut the function signature doesn't match. Model sends valid JSON, function receives wrong types. - 8%Async tool inside sync agent invocation. Tool defined with
async defbut agent invoked viaapp.invoke(notainvoke). Tool call errors or hangs. - 7%Duplicate tool names. Two
@tool-decorated functions namedsearchin different modules. Second one silently wins; model can't distinguish. - 5%Missing return statement in tool. Tool returns
None. Model receives an empty tool result and either loops or gives a low-confidence final answer.
How to fix it
Use a tool-calling model and decorate every tool with @tool
Fixes both "model doesn't support tools" and "tool is not a BaseTool".
Pick a modern flagship model with native tool calling (Claude Opus/Sonnet 4.x+, GPT-4.1+, Gemini 2.5+). Wrap every tool function with LangChain's @tool decorator so the model gets a proper JSON schema. The two together unblock 44% of create_react_agent errors.
from typing import Annotated
from langchain_core.tools import tool
from langchain_anthropic import ChatAnthropic
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import MemorySaver
# ✅ Model that supports native tool calling
model = ChatAnthropic(
model="claude-opus-4-7", # or "claude-sonnet-4-5", "claude-haiku-4-5"
temperature=0.2,
)
# ✅ Tools decorated with @tool — schema auto-derived from type hints + docstring
@tool
def search(query: str, top_k: int = 5) -> list[str]:
"""Search the web for the given query and return top_k result URLs."""
# ... call your search API ...
return [f"https://example.com/{i}" for i in range(top_k)]
@tool
def calculator(expression: str) -> float:
"""Evaluate a mathematical expression and return the numeric result."""
return float(eval(expression, {"__builtins__": {}}))
# ✅ Pydantic schema for typed args (optional, cleaner for complex tools)
from pydantic import BaseModel, Field
class SearchArgs(BaseModel):
query: str = Field(min_length=1, description="Search query text")
top_k: int = Field(default=5, ge=1, le=20, description="Number of results")
@tool(args_schema=SearchArgs)
def typed_search(query: str, top_k: int = 5) -> list[str]:
"""Search with validated arguments."""
return search.invoke({"query": query, "top_k": top_k})
# ✅ Build the agent
agent = create_react_agent(
model=model,
tools=[search, calculator],
prompt="You are a helpful assistant. Use tools when needed.",
checkpointer=MemorySaver(),
)
# ✅ Invoke
config = {"configurable": {"thread_id": "session-1"}}
result = agent.invoke(
{"messages": [("user", "What is 27 * 43?")]},
config,
)
for m in result["messages"]:
m.pretty_print()
# HumanMessage: What is 27 * 43?
# AIMessage: [tool_call: calculator(expression="27 * 43")]
# ToolMessage: 1161.0
# AIMessage: 27 multiplied by 43 is 1,161.
# ❌ COMMON BUGS
# 1. Old model: model = ChatAnthropic(model="claude-instant-1.2") # no tool support
# 2. Raw function: agent = create_react_agent(model, tools=[search])
# where def search(q): ... # not @tool-decorated
# 3. Missing docstring on @tool — model gets an empty description
Subclass AgentState when you need extra fields
Fixes "State schema missing required field messages".
The prebuilt agent needs a messages channel with add_messages. Rather than writing your own state from scratch, subclass AgentState from langgraph.prebuilt.chat_agent_executor. You inherit messages and add your own fields on top.
from typing import Annotated
from langgraph.prebuilt import create_react_agent
from langgraph.prebuilt.chat_agent_executor import AgentState
# ✅ Subclass AgentState — messages field is inherited
class MyAgentState(AgentState):
user_id: str
intent: str | None
tool_call_count: int
# ✅ Custom state modifier (function that runs before every model call)
from langchain_core.messages import SystemMessage
def state_modifier(state: MyAgentState) -> list:
"""Inject dynamic system context — runs on every turn."""
system = SystemMessage(
content=f"You are helping user {state['user_id']}. "
f"Detected intent: {state.get('intent', 'unknown')}. "
f"Tools called so far: {state.get('tool_call_count', 0)}."
)
return [system] + state["messages"]
agent = create_react_agent(
model=model,
tools=[search, calculator],
state_schema=MyAgentState, # subclass, not TypedDict from scratch
prompt=state_modifier, # dynamic prompt as a function
checkpointer=MemorySaver(),
)
# ✅ Invoke — provide values for the extra fields
config = {"configurable": {"thread_id": "user-42"}}
result = agent.invoke({
"messages": [("user", "help me plan a trip")],
"user_id": "user-42",
"intent": "travel_planning",
"tool_call_count": 0,
}, config)
# ❌ ANTI-PATTERN — starting from scratch and missing messages
# class BrokenState(TypedDict):
# user_id: str
# intent: str
# # Missing messages field — create_react_agent will fail at first invoke
# agent = create_react_agent(model, tools, state_schema=BrokenState)
# ✅ If you MUST write from scratch, include the messages channel
from typing import TypedDict
from langchain_core.messages import AnyMessage
from langgraph.graph.message import add_messages
class HandmadeState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages] # required
user_id: str
intent: str
state_modifier (or its deprecated alias prompt=) is the extension point for injecting dynamic context. It runs before every model call and receives the full state — great for user personalization, few-shot injection based on intent, and tool-call budgets.Pass structured prompts, and consider response_format for typed output
Fixes tool-call refusal and untyped final answers.
A bare string prompt replaces the model's default guidance to use tools when helpful. Pass a SystemMessage that adds to tool-usage guidance rather than replacing it. When the agent's final answer must be structured (JSON, schema-validated), pass response_format and the agent will emit a validated final message.
from pydantic import BaseModel, Field
from langchain_core.messages import SystemMessage
from langgraph.prebuilt import create_react_agent
# ✅ Prompt that reinforces tool use rather than replacing default guidance
SYSTEM = """You are a research assistant.
You have access to a `search` tool for facts and a `calculator` tool for math.
Use tools whenever the answer requires up-to-date info or arithmetic.
Cite sources for factual claims.
Respond in plain English unless the user asks for structured output.
"""
agent = create_react_agent(
model=model,
tools=[search, calculator],
prompt=SystemMessage(content=SYSTEM), # SystemMessage keeps tool guidance intact
checkpointer=MemorySaver(),
)
# ✅ response_format — forces the final message to match a schema
class TripPlan(BaseModel):
destination: str
duration_days: int = Field(ge=1, le=30)
estimated_cost_usd: float = Field(ge=0)
activities: list[str] = Field(min_length=1, max_length=10)
citations: list[str]
typed_agent = create_react_agent(
model=model,
tools=[search],
prompt=SystemMessage(content="You are a trip planner. Cite every fact."),
response_format=TripPlan, # Pydantic model as schema
checkpointer=MemorySaver(),
)
result = typed_agent.invoke(
{"messages": [("user", "Plan a 5-day trip to Kyoto under $2000")]},
{"configurable": {"thread_id": "trip-1"}},
)
# Final validated output is under result["structured_response"]
plan: TripPlan = result["structured_response"]
print(plan.destination, plan.estimated_cost_usd)
for act in plan.activities:
print("-", act)
# ❌ BUG — bare string prompt REPLACES tool usage guidance
# agent = create_react_agent(
# model=model,
# tools=[search, calculator],
# prompt="Be concise.", # <-- no mention of tools; model may skip them
# )
# ✅ Callable prompt for full control — must return list[BaseMessage]
def dynamic_prompt(state):
from datetime import datetime
system = SystemMessage(content=(
f"Today is {datetime.now():%Y-%m-%d}. "
"You have tools: search, calculator. Use them when appropriate."
))
return [system] + state["messages"]
agent2 = create_react_agent(model, tools=[search, calculator], prompt=dynamic_prompt)
response_format, the agent still runs the ReAct loop with tools — response_format only shapes the final answer. If you also need every intermediate message structured, wrap individual node outputs with llm.with_structured_output(Schema) instead.Prevention checklist
- Use a model with native tool calling. Verify:
hasattr(model, "bind_tools"). - Every tool must be
@tool-decorated or aBaseToolsubclass — never bare functions. - Every tool must have a real docstring — that's the model's only guide to when to call it.
- Subclass
AgentStaterather than writing state from scratch — inherits the requiredmessagesfield. - Use
SystemMessagefor prompts, not bare strings — preserves tool-use guidance. - Match async: async tools require
agent.ainvoke; mixing sync tools with async agents (or vice versa) is fragile. - For structured final answers, use
response_format=PydanticModel— cleaner than post-processing.
Frequently asked questions
You can absolutely build the same graph manually — a tool-calling model node, a ToolNode, and a conditional edge that loops until no more tool calls. Use the manual version when you need custom routing (e.g. tool budget limits, per-tool timeouts, structured intermediate steps). Use create_react_agent when a standard tool-use loop is fine and you value fewer lines of code.
Not directly — create_react_agent returns a compiled graph you can't modify. If you need pre-processing (retrieval, classification) or post-processing (formatting, logging) around the agent, wrap the agent invocation as a node inside your own outer StateGraph. That gives you full control over what runs before and after.
state_modifier is the older name; prompt is its newer alias in modern LangGraph. Both do the same thing: transform the state into the list of messages the model sees on each turn. Prefer prompt in new code; state_modifier still works for backward compatibility. Both accept a string, a SystemMessage, or a callable.
Track calls in state and short-circuit in a wrapper node. Subclass AgentState with tool_call_count: int, increment it in a state_modifier, and inject a system message like "You have reached your tool call budget. Answer with what you know." once the count exceeds a threshold. LangGraph also enforces a global recursion_limit at the graph level as a hard safety net.
Yes — create_react_agent uses ToolNode internally, which emits ToolMessage objects that flow into state["messages"] via add_messages. You see them in the final result["messages"] alongside the human and AI messages. This is the audit trail; keep the tool_call_id intact so requests and responses can be paired.