LangGraph create_react_agent Binding Errors — Fix Guide (2026)
Agents · create_react_agent Severity: High

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.

TL;DRcreate_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 does not support tools
ValueError — model does not support tools
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 is not a BaseTool
TypeError — tool is not a BaseTool
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.
State missing messages field
State missing messages field
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

ArgumentTypeNotes
modelBaseChatModel with bind_toolsMust support native tool calling
toolslist[BaseTool]Each item is @tool-decorated or a BaseTool subclass
prompt (or state_modifier)str, SystemMessage, or callablePrepended to messages before every model call
state_schematype[AgentState] subclassDefaults to AgentState. Must include messages field
checkpointerBaseCheckpointSaverOptional; required for resumable agents
interrupt_before / interrupt_afterlist[str]Pause execution at named nodes for HITL
response_formatPydantic model or schemaForce 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: str passed as state_schema without 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_schema but 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 def but agent invoked via app.invoke (not ainvoke). Tool call errors or hangs.
  • 7%
    Duplicate tool names. Two @tool-decorated functions named search in 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

Fix #1

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.

react_agent_basics.pypython
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
Note: Tool docstrings are the model's only guide to when to call the tool. Vague docstrings ("does search") produce agents that don't know when to invoke tools. Write docstrings for the model, not the human reader — describe what the tool does AND when to call it.
Fix #2

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.

agent_state_subclass.pypython
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
Note: 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.
Fix #3

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.

prompts_and_response_format.pypython
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)
Note: When using 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 a BaseTool subclass — never bare functions.
  • Every tool must have a real docstring — that's the model's only guide to when to call it.
  • Subclass AgentState rather than writing state from scratch — inherits the required messages field.
  • Use SystemMessage for 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

Do I always need create_react_agent, or can I build the same graph by hand?

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.

Can I add non-tool nodes to a create_react_agent graph?

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.

What's the difference between prompt and state_modifier?

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.

How do I limit the number of tool calls?

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.

Do tool results get added to state automatically?

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.

Related errors