Tools extend what agents can do—letting them fetch real-time data, execute code, query external databases, and take actions in the world.Under the hood, tools are callable functions with well-defined inputs and outputs that get passed to a chat model. The model decides when to invoke a tool based on the conversation context, and what input arguments to provide.
For details on how models handle tool calls, see Tool calling. Trace tool calls and debug errors with LangSmith. Follow the tracing quickstart to get set up.We recommend you also set up LangSmith Engine which monitors your traces, detects issues, and proposes fixes.
The simplest way to create a tool is with the @tool decorator. By default, the function’s docstring becomes the tool’s description that helps the model understand when to use it:
from langchain.tools import tool@tooldef search_database(query: str, limit: int = 10) -> str: """Search the customer database for records matching the query. Args: query: Search terms to look for limit: Maximum number of results to return """ return f"Found {limit} results for '{query}'"
Type hints are required as they define the tool’s input schema. The docstring should be informative and concise to help the model understand the tool’s purpose.
Server-side tool use: Some chat models feature built-in tools (web search, code interpreters) that are executed server-side. See Server-side tool use for details.
Prefer snake_case for tool names (e.g., web_search instead of Web Search). Some model providers have issues with or reject names containing spaces or special characters with errors. Sticking to alphanumeric characters, underscores, and hyphens helps to improve compatibility across providers.
Override the auto-generated tool description for clearer model guidance:
@tool("calculator", description="Performs arithmetic calculations. Use this for any math problems.")def calc(expression: str) -> str: """Evaluate mathematical expressions.""" return str(eval(expression))
The following parameter names are reserved and cannot be used as tool arguments. Using these names will cause runtime errors.
Parameter name
Purpose
config
Reserved for passing RunnableConfig to tools internally
runtime
Reserved for ToolRuntime parameter (accessing state, context, store)
To access runtime information, use the ToolRuntime parameter instead of naming your own arguments config or runtime.If you use InjectedState, InjectedStore, get_runtime(), or InjectedToolCallId, see Migrate from older injection patterns.
Tools are most powerful when they can access runtime information like conversation history, user data, and persistent memory. This section covers how to access and update this information from within your tools.Tools can access runtime information through the ToolRuntime parameter, which provides:
Component
Description
Use case
State
Short-term memory - mutable data that exists for the current conversation (messages, counters, custom fields)
State represents short-term memory that exists for the duration of a conversation. It includes the message history and any custom fields you define in your graph state.
Add runtime: ToolRuntime to your tool signature to access state. This parameter is automatically injected and hidden from the LLM - it won’t appear in the tool’s schema.
Tools can access the current conversation state using runtime.state:
from langchain.tools import tool, ToolRuntimefrom langchain.messages import HumanMessage@tooldef get_last_user_message(runtime: ToolRuntime) -> str: """Get the most recent message from the user.""" messages = runtime.state["messages"] # Find the last human message for message in reversed(messages): if isinstance(message, HumanMessage): return message.content return "No user messages found"# Access custom state fields@tooldef get_user_preference( pref_name: str, runtime: ToolRuntime) -> str: """Get a user preference value.""" preferences = runtime.state.get("user_preferences", {}) return preferences.get(pref_name, "Not set")
The runtime parameter is hidden from the model. For the example above, the model only sees pref_name in the tool schema.
Use Command to update the agent’s state. This is useful for tools that need to update custom state fields.
Include a ToolMessage in the update so the model can see the result of the tool call:
from langchain.agents import AgentStatefrom langchain.messages import ToolMessagefrom langchain.tools import ToolRuntime, toolfrom langgraph.types import Commandclass CustomState(AgentState): user_name: str@tooldef set_user_name(new_name: str, runtime: ToolRuntime[None, CustomState]) -> Command: """Set the user's name in the conversation state.""" return Command( update={ "user_name": new_name, "messages": [ ToolMessage( content=f"User name set to {new_name}.", tool_call_id=runtime.tool_call_id, ) ], } )
When tools update state variables, consider defining a reducer for those fields. Since LLMs can call multiple tools in parallel, a reducer determines how to resolve conflicts when the same state field is updated by concurrent tool calls.
Context provides immutable configuration data that is passed at invocation time. Use it for user IDs, session details, or application-specific settings that shouldn’t change during a conversation.
While thread_id (passed via config={"configurable": {"thread_id": ...}}) scopes the conversation: message history and checkpoints, context carries per-run data your tools and middleware read at invocation time. In production you typically pass both together: a stable thread_id per conversation, and a context object on every invoke.
Access context through runtime.context. Pass it alongside a thread_id so the conversation is persisted across turns:
from dataclasses import dataclassfrom langchain.agents import create_agentfrom langchain.tools import tool, ToolRuntimefrom langchain_core.utils.uuid import uuid7from langchain_openai import ChatOpenAIUSER_DATABASE = { "user123": { "name": "Alice Johnson", "account_type": "Premium", "balance": 5000, "email": "alice@example.com", }, "user456": { "name": "Bob Smith", "account_type": "Standard", "balance": 1200, "email": "bob@example.com", },}@dataclassclass UserContext: user_id: str@tooldef get_account_info(runtime: ToolRuntime[UserContext]) -> str: """Get the current user's account information.""" user_id = runtime.context.user_id if user_id in USER_DATABASE: user = USER_DATABASE[user_id] return ( f"Account holder: {user['name']}\n" f"Type: {user['account_type']}\n" f"Balance: ${user['balance']}" ) return "User not found"model = ChatOpenAI(model="google_genai:gemini-3.5-flash")agent = create_agent( model, tools=[get_account_info], context_schema=UserContext, system_prompt="You are a financial assistant.",)result = agent.invoke( {"messages": [{"role": "user", "content": "What's my current balance?"}]}, config={"configurable": {"thread_id": str(uuid7())}}, context=UserContext(user_id="user123"),)
from dataclasses import dataclassfrom langchain.agents import create_agentfrom langchain.tools import tool, ToolRuntimefrom langchain_core.utils.uuid import uuid7from langchain_openai import ChatOpenAIUSER_DATABASE = { "user123": { "name": "Alice Johnson", "account_type": "Premium", "balance": 5000, "email": "alice@example.com", }, "user456": { "name": "Bob Smith", "account_type": "Standard", "balance": 1200, "email": "bob@example.com", },}@dataclassclass UserContext: user_id: str@tooldef get_account_info(runtime: ToolRuntime[UserContext]) -> str: """Get the current user's account information.""" user_id = runtime.context.user_id if user_id in USER_DATABASE: user = USER_DATABASE[user_id] return ( f"Account holder: {user['name']}\n" f"Type: {user['account_type']}\n" f"Balance: ${user['balance']}" ) return "User not found"model = ChatOpenAI(model="openai:gpt-5.5")agent = create_agent( model, tools=[get_account_info], context_schema=UserContext, system_prompt="You are a financial assistant.",)result = agent.invoke( {"messages": [{"role": "user", "content": "What's my current balance?"}]}, config={"configurable": {"thread_id": str(uuid7())}}, context=UserContext(user_id="user123"),)
from dataclasses import dataclassfrom langchain.agents import create_agentfrom langchain.tools import tool, ToolRuntimefrom langchain_core.utils.uuid import uuid7from langchain_openai import ChatOpenAIUSER_DATABASE = { "user123": { "name": "Alice Johnson", "account_type": "Premium", "balance": 5000, "email": "alice@example.com", }, "user456": { "name": "Bob Smith", "account_type": "Standard", "balance": 1200, "email": "bob@example.com", },}@dataclassclass UserContext: user_id: str@tooldef get_account_info(runtime: ToolRuntime[UserContext]) -> str: """Get the current user's account information.""" user_id = runtime.context.user_id if user_id in USER_DATABASE: user = USER_DATABASE[user_id] return ( f"Account holder: {user['name']}\n" f"Type: {user['account_type']}\n" f"Balance: ${user['balance']}" ) return "User not found"model = ChatOpenAI(model="anthropic:claude-sonnet-4-6")agent = create_agent( model, tools=[get_account_info], context_schema=UserContext, system_prompt="You are a financial assistant.",)result = agent.invoke( {"messages": [{"role": "user", "content": "What's my current balance?"}]}, config={"configurable": {"thread_id": str(uuid7())}}, context=UserContext(user_id="user123"),)
from dataclasses import dataclassfrom langchain.agents import create_agentfrom langchain.tools import tool, ToolRuntimefrom langchain_core.utils.uuid import uuid7from langchain_openai import ChatOpenAIUSER_DATABASE = { "user123": { "name": "Alice Johnson", "account_type": "Premium", "balance": 5000, "email": "alice@example.com", }, "user456": { "name": "Bob Smith", "account_type": "Standard", "balance": 1200, "email": "bob@example.com", },}@dataclassclass UserContext: user_id: str@tooldef get_account_info(runtime: ToolRuntime[UserContext]) -> str: """Get the current user's account information.""" user_id = runtime.context.user_id if user_id in USER_DATABASE: user = USER_DATABASE[user_id] return ( f"Account holder: {user['name']}\n" f"Type: {user['account_type']}\n" f"Balance: ${user['balance']}" ) return "User not found"model = ChatOpenAI(model="openrouter:z-ai/glm-5.2")agent = create_agent( model, tools=[get_account_info], context_schema=UserContext, system_prompt="You are a financial assistant.",)result = agent.invoke( {"messages": [{"role": "user", "content": "What's my current balance?"}]}, config={"configurable": {"thread_id": str(uuid7())}}, context=UserContext(user_id="user123"),)
from dataclasses import dataclassfrom langchain.agents import create_agentfrom langchain.tools import tool, ToolRuntimefrom langchain_core.utils.uuid import uuid7from langchain_openai import ChatOpenAIUSER_DATABASE = { "user123": { "name": "Alice Johnson", "account_type": "Premium", "balance": 5000, "email": "alice@example.com", }, "user456": { "name": "Bob Smith", "account_type": "Standard", "balance": 1200, "email": "bob@example.com", },}@dataclassclass UserContext: user_id: str@tooldef get_account_info(runtime: ToolRuntime[UserContext]) -> str: """Get the current user's account information.""" user_id = runtime.context.user_id if user_id in USER_DATABASE: user = USER_DATABASE[user_id] return ( f"Account holder: {user['name']}\n" f"Type: {user['account_type']}\n" f"Balance: ${user['balance']}" ) return "User not found"model = ChatOpenAI(model="fireworks:accounts/fireworks/models/glm-5p2")agent = create_agent( model, tools=[get_account_info], context_schema=UserContext, system_prompt="You are a financial assistant.",)result = agent.invoke( {"messages": [{"role": "user", "content": "What's my current balance?"}]}, config={"configurable": {"thread_id": str(uuid7())}}, context=UserContext(user_id="user123"),)
from dataclasses import dataclassfrom langchain.agents import create_agentfrom langchain.tools import tool, ToolRuntimefrom langchain_core.utils.uuid import uuid7from langchain_openai import ChatOpenAIUSER_DATABASE = { "user123": { "name": "Alice Johnson", "account_type": "Premium", "balance": 5000, "email": "alice@example.com", }, "user456": { "name": "Bob Smith", "account_type": "Standard", "balance": 1200, "email": "bob@example.com", },}@dataclassclass UserContext: user_id: str@tooldef get_account_info(runtime: ToolRuntime[UserContext]) -> str: """Get the current user's account information.""" user_id = runtime.context.user_id if user_id in USER_DATABASE: user = USER_DATABASE[user_id] return ( f"Account holder: {user['name']}\n" f"Type: {user['account_type']}\n" f"Balance: ${user['balance']}" ) return "User not found"model = ChatOpenAI(model="baseten:zai-org/GLM-5.2")agent = create_agent( model, tools=[get_account_info], context_schema=UserContext, system_prompt="You are a financial assistant.",)result = agent.invoke( {"messages": [{"role": "user", "content": "What's my current balance?"}]}, config={"configurable": {"thread_id": str(uuid7())}}, context=UserContext(user_id="user123"),)
from dataclasses import dataclassfrom langchain.agents import create_agentfrom langchain.tools import tool, ToolRuntimefrom langchain_core.utils.uuid import uuid7from langchain_openai import ChatOpenAIUSER_DATABASE = { "user123": { "name": "Alice Johnson", "account_type": "Premium", "balance": 5000, "email": "alice@example.com", }, "user456": { "name": "Bob Smith", "account_type": "Standard", "balance": 1200, "email": "bob@example.com", },}@dataclassclass UserContext: user_id: str@tooldef get_account_info(runtime: ToolRuntime[UserContext]) -> str: """Get the current user's account information.""" user_id = runtime.context.user_id if user_id in USER_DATABASE: user = USER_DATABASE[user_id] return ( f"Account holder: {user['name']}\n" f"Type: {user['account_type']}\n" f"Balance: ${user['balance']}" ) return "User not found"model = ChatOpenAI(model="ollama:north-mini-code-1.0")agent = create_agent( model, tools=[get_account_info], context_schema=UserContext, system_prompt="You are a financial assistant.",)result = agent.invoke( {"messages": [{"role": "user", "content": "What's my current balance?"}]}, config={"configurable": {"thread_id": str(uuid7())}}, context=UserContext(user_id="user123"),)
The BaseStore provides persistent storage that survives across conversations. Unlike state (short-term memory), data saved to the store remains available in future sessions.Access the store through runtime.store. The store uses a namespace/key pattern to organize data:
For production deployments, use a persistent store implementation like PostgresStore instead of InMemoryStore. See the memory documentation for setup details.
from typing import Anyfrom langgraph.store.memory import InMemoryStorefrom langchain.agents import create_agentfrom langchain.tools import tool, ToolRuntimefrom langchain_openai import ChatOpenAI# Access memory@tooldef get_user_info(user_id: str, runtime: ToolRuntime) -> str: """Look up user info.""" store = runtime.store user_info = store.get(("users",), user_id) return str(user_info.value) if user_info else "Unknown user"# Update memory@tooldef save_user_info(user_id: str, user_info: dict[str, Any], runtime: ToolRuntime) -> str: """Save user info.""" store = runtime.store store.put(("users",), user_id, user_info) return "Successfully saved user info."model = ChatOpenAI(model="gpt-5.5")store = InMemoryStore()agent = create_agent( model, tools=[get_user_info, save_user_info], store=store)# First session: save user infoagent.invoke({ "messages": [{"role": "user", "content": "Save the following user: userid: abc123, name: Foo, age: 25, email: foo@langchain.dev"}]})# Second session: get user infoagent.invoke({ "messages": [{"role": "user", "content": "Get user info for user with id 'abc123'"}]})# Here is the user info for user with ID "abc123":# - Name: Foo# - Age: 25# - Email: foo@langchain.dev
Stream real-time updates from tools during execution. This is useful for providing progress feedback to users during long-running operations.Use runtime.stream_writer to emit custom updates:
from langchain.tools import tool, ToolRuntime@tooldef get_weather(city: str, runtime: ToolRuntime) -> str: """Get weather for a given city.""" writer = runtime.stream_writer # Stream custom updates as the tool executes writer(f"Looking up data for city: {city}") writer(f"Acquired data for city: {city}") return f"It's always sunny in {city}!"
If you use runtime.stream_writer inside your tool, the tool must be invoked within a LangGraph execution context. See Streaming for more details.
When your tool runs on LangGraph Server, access the assistant ID, graph ID, and authenticated user via runtime.server_info:
from langchain.tools import tool, ToolRuntime@tooldef get_assistant_scoped_data(runtime: ToolRuntime) -> str: """Fetch data scoped to the current assistant.""" server = runtime.server_info if server is not None: print(f"Assistant: {server.assistant_id}, Graph: {server.graph_id}") if server.user is not None: print(f"User: {server.user.identity}") return "done"
server_info is None when the tool is not running on LangGraph Server (e.g., during local development or testing).
Requires deepagents>=0.5.0 (or langgraph>=1.1.5).
Migrate from older injection patterns
Older examples used InjectedState, InjectedStore, get_runtime(), or InjectedToolCallId. Use ToolRuntime instead for one explicit interface to state, context, store, and execution metadata.
In LangChain, tools are used by agents (for example via create_agent) and tool error handling is configured through middleware.For LangGraph workflows, tool execution is handled by ToolNode. See ToolNode for Graph API usage, including how tools can access the current graph state and run-scoped context.
Return an object (for example, a dict) when your tool produces structured data that the model should inspect.
from langchain.tools import tool@tooldef get_weather_data(city: str) -> dict: """Get structured weather data for a city.""" return { "city": city, "temperature_c": 22, "conditions": "sunny", }
Behavior:
The object is serialized and sent back as tool output.
The model can read specific fields and reason over them.
Like string returns, this does not directly update graph state.
Use this when downstream reasoning benefits from explicit fields instead of free-form text.
Tools are not limited to plain text. When the model supports multimodal tool results, the tool can return standard content blocks so the model receives text, images, and other media in one tool result.
from langchain.tools import tool@tooldef capture_screenshot() -> list[dict]: """Capture a screenshot of the current page.""" return [ {"type": "text", "text": "Screenshot of the current page:"}, {"type": "image", "url": "https://example.com/page.png"}, ]
Behavior:
The return value is converted to a ToolMessage with multimodal content.
Use message.content_blocks to read the normalized block list after the tool runs.
The model must support the modalities you return. Check your model’s capabilities before returning images, audio, or video.
For block types and provider-specific requirements, see Multimodal messages. MCP tools that return images or mixed content are converted the same way; see Multimodal tool content.
Return a Command when the tool needs to update graph state (for example, setting user preferences or app state).
You can return a Command with or without including a ToolMessage.
If the model needs to see that the tool succeeded (for example, to confirm a preference change), include a ToolMessage in the update, using runtime.tool_call_id for the tool_call_id parameter.
from langchain.messages import ToolMessagefrom langchain.tools import ToolRuntime, toolfrom langgraph.types import Command@tooldef set_language(language: str, runtime: ToolRuntime) -> Command: """Set the preferred response language.""" return Command( update={ "preferred_language": language, "messages": [ ToolMessage( content=f"Language set to {language}.", tool_call_id=runtime.tool_call_id, ) ], } )
Behavior:
The command updates state using update.
Updated state is available to subsequent steps in the same run.
Use reducers for fields that may be updated by parallel tool calls.
Use this when the tool is not just returning data, but also mutating agent state.
Set return direct on a tool to short-circuit the agent loop: the agent returns the tool’s output to the caller immediately, without sending it back through the model for further processing.
from langchain.agents import create_agentfrom langchain.tools import toolfrom langchain_openai import ChatOpenAI@tool(return_direct=True)def fetch_order_status(order_id: str) -> str: """Fetch the current status of a customer order.""" # In production, query your order management system here return f"Order {order_id} is shipped and will arrive in 2 days."agent = create_agent( ChatOpenAI(model="google_genai:gemini-3.5-flash"), tools=[fetch_order_status],)result = agent.invoke({ "messages": [{"role": "user", "content": "What is the status of order #12345?"}]})# The agent returns the tool output directly without another LLM call:# "Order 12345 is shipped and will arrive in 2 days."
from langchain.agents import create_agentfrom langchain.tools import toolfrom langchain_openai import ChatOpenAI@tool(return_direct=True)def fetch_order_status(order_id: str) -> str: """Fetch the current status of a customer order.""" # In production, query your order management system here return f"Order {order_id} is shipped and will arrive in 2 days."agent = create_agent( ChatOpenAI(model="openai:gpt-5.5"), tools=[fetch_order_status],)result = agent.invoke({ "messages": [{"role": "user", "content": "What is the status of order #12345?"}]})# The agent returns the tool output directly without another LLM call:# "Order 12345 is shipped and will arrive in 2 days."
from langchain.agents import create_agentfrom langchain.tools import toolfrom langchain_openai import ChatOpenAI@tool(return_direct=True)def fetch_order_status(order_id: str) -> str: """Fetch the current status of a customer order.""" # In production, query your order management system here return f"Order {order_id} is shipped and will arrive in 2 days."agent = create_agent( ChatOpenAI(model="anthropic:claude-sonnet-4-6"), tools=[fetch_order_status],)result = agent.invoke({ "messages": [{"role": "user", "content": "What is the status of order #12345?"}]})# The agent returns the tool output directly without another LLM call:# "Order 12345 is shipped and will arrive in 2 days."
from langchain.agents import create_agentfrom langchain.tools import toolfrom langchain_openai import ChatOpenAI@tool(return_direct=True)def fetch_order_status(order_id: str) -> str: """Fetch the current status of a customer order.""" # In production, query your order management system here return f"Order {order_id} is shipped and will arrive in 2 days."agent = create_agent( ChatOpenAI(model="openrouter:z-ai/glm-5.2"), tools=[fetch_order_status],)result = agent.invoke({ "messages": [{"role": "user", "content": "What is the status of order #12345?"}]})# The agent returns the tool output directly without another LLM call:# "Order 12345 is shipped and will arrive in 2 days."
from langchain.agents import create_agentfrom langchain.tools import toolfrom langchain_openai import ChatOpenAI@tool(return_direct=True)def fetch_order_status(order_id: str) -> str: """Fetch the current status of a customer order.""" # In production, query your order management system here return f"Order {order_id} is shipped and will arrive in 2 days."agent = create_agent( ChatOpenAI(model="fireworks:accounts/fireworks/models/glm-5p2"), tools=[fetch_order_status],)result = agent.invoke({ "messages": [{"role": "user", "content": "What is the status of order #12345?"}]})# The agent returns the tool output directly without another LLM call:# "Order 12345 is shipped and will arrive in 2 days."
from langchain.agents import create_agentfrom langchain.tools import toolfrom langchain_openai import ChatOpenAI@tool(return_direct=True)def fetch_order_status(order_id: str) -> str: """Fetch the current status of a customer order.""" # In production, query your order management system here return f"Order {order_id} is shipped and will arrive in 2 days."agent = create_agent( ChatOpenAI(model="baseten:zai-org/GLM-5.2"), tools=[fetch_order_status],)result = agent.invoke({ "messages": [{"role": "user", "content": "What is the status of order #12345?"}]})# The agent returns the tool output directly without another LLM call:# "Order 12345 is shipped and will arrive in 2 days."
from langchain.agents import create_agentfrom langchain.tools import toolfrom langchain_openai import ChatOpenAI@tool(return_direct=True)def fetch_order_status(order_id: str) -> str: """Fetch the current status of a customer order.""" # In production, query your order management system here return f"Order {order_id} is shipped and will arrive in 2 days."agent = create_agent( ChatOpenAI(model="ollama:north-mini-code-1.0"), tools=[fetch_order_status],)result = agent.invoke({ "messages": [{"role": "user", "content": "What is the status of order #12345?"}]})# The agent returns the tool output directly without another LLM call:# "Order 12345 is shipped and will arrive in 2 days."
Behavior:
The tool executes normally and its output is wrapped in a ToolMessage.
The agent stops looping and returns the tool’s output as the final response, bypassing any additional model call.
If the model calls multiple tools in a single turn, return_direct takes effect only when all called tools have return_direct=True.
Use this when:
The tool’s output is the complete, user-ready answer (for example, a lookup that returns a ready-to-display result).
You want to avoid an extra model call when no additional reasoning is needed.
You need deterministic, unmodified output — the model cannot rephrase, summarize, or act on the tool result.
Because the model does not process the tool’s output, return_direct=True is not suitable for tools whose results require further reasoning, summarization, or chaining with other tool calls.
Tools access graph state through ToolRuntime. See Access context for state, context, store, and streaming APIs.
from langchain.tools import tool, ToolRuntime@tooldef get_message_count(runtime: ToolRuntime) -> str: """Get the number of messages in the conversation.""" messages = runtime.state["messages"] return f"There are {len(messages)} messages."
For more details on accessing state, context, and long-term memory from tools, see Access context.
With dynamic tools, the set of tools available to the agent is modified at runtime rather than defined all upfront. Not every tool is appropriate for every situation. Too many tools may overwhelm the model (overload context) and increase errors; too few limit capabilities. Dynamic tool selection enables adapting the available toolset based on authentication state, user permissions, feature flags, or conversation stage.There are two approaches depending on whether tools are known ahead of time:
Filtering pre-registered tools
Runtime tool registration
When all possible tools are known at agent creation time, you can pre-register them and dynamically filter which ones are exposed to the model based on state, permissions, or context.
State
Store
Runtime Context
Enable advanced tools only after certain conversation milestones:
from langchain.agents import create_agentfrom langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponsefrom typing import Callable@wrap_model_calldef state_based_tools( request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]) -> ModelResponse: """Filter tools based on conversation State.""" # Read from State: check if user has authenticated state = request.state is_authenticated = state.get("authenticated", False) message_count = len(state["messages"]) # Only enable sensitive tools after authentication if not is_authenticated: tools = [t for t in request.tools if t.name.startswith("public_")] request = request.override(tools=tools) elif message_count < 5: # Limit tools early in conversation tools = [t for t in request.tools if t.name != "advanced_search"] request = request.override(tools=tools) return handler(request)agent = create_agent( model="gpt-5.5", tools=[public_search, private_search, advanced_search], middleware=[state_based_tools])
Filter tools based on user preferences or feature flags in Store:
from dataclasses import dataclassfrom langchain.agents import create_agentfrom langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponsefrom typing import Callablefrom langgraph.store.memory import InMemoryStore@dataclassclass Context: user_id: str@wrap_model_calldef store_based_tools( request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]) -> ModelResponse: """Filter tools based on Store preferences.""" user_id = request.runtime.context.user_id # Read from Store: get user's enabled features store = request.runtime.store feature_flags = store.get(("features",), user_id) if feature_flags: enabled_features = feature_flags.value.get("enabled_tools", []) # Only include tools that are enabled for this user tools = [t for t in request.tools if t.name in enabled_features] request = request.override(tools=tools) return handler(request)agent = create_agent( model="gpt-5.5", tools=[search_tool, analysis_tool, export_tool], middleware=[store_based_tools], context_schema=Context, store=InMemoryStore())
Filter tools based on user permissions from Runtime Context:
from dataclasses import dataclassfrom langchain.agents import create_agentfrom langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponsefrom typing import Callable@dataclassclass Context: user_role: str@wrap_model_calldef context_based_tools( request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]) -> ModelResponse: """Filter tools based on Runtime Context permissions.""" # Read from Runtime Context: get user role if request.runtime is None or request.runtime.context is None: # If no context provided, default to viewer (most restrictive) user_role = "viewer" else: user_role = request.runtime.context.user_role if user_role == "admin": # Admins get all tools pass elif user_role == "editor": # Editors can't delete tools = [t for t in request.tools if t.name != "delete_data"] request = request.override(tools=tools) else: # Viewers get read-only tools tools = [t for t in request.tools if t.name.startswith("read_")] request = request.override(tools=tools) return handler(request)agent = create_agent( model="gpt-5.5", tools=[read_data, write_data, delete_data], middleware=[context_based_tools], context_schema=Context)
This approach is best when:
All possible tools are known at compile/startup time
You want to filter based on permissions, feature flags, or conversation state
Tools are static but their availability is dynamic
When tools are discovered or created at runtime (e.g., loaded from an MCP server, generated based on user data, or fetched from a remote registry), you need to both register the tools and handle their execution dynamically.This requires two middleware hooks:
wrap_model_call - Add the dynamic tools to the request
wrap_tool_call - Handle execution of the dynamically added tools
from langchain.tools import toolfrom langchain.agents import create_agentfrom langchain.agents.middleware import AgentMiddleware, ModelRequest, ToolCallRequest# A tool that will be added dynamically at runtime@tooldef calculate_tip(bill_amount: float, tip_percentage: float = 20.0) -> str: """Calculate the tip amount for a bill.""" tip = bill_amount * (tip_percentage / 100) return f"Tip: ${tip:.2f}, Total: ${bill_amount + tip:.2f}"class DynamicToolMiddleware(AgentMiddleware): """Middleware that registers and handles dynamic tools.""" def wrap_model_call(self, request: ModelRequest, handler): # Add dynamic tool to the request # This could be loaded from an MCP server, database, etc. updated = request.override(tools=[*request.tools, calculate_tip]) return handler(updated) def wrap_tool_call(self, request: ToolCallRequest, handler): # Handle execution of the dynamic tool if request.tool_call["name"] == "calculate_tip": return handler(request.override(tool=calculate_tip)) return handler(request)agent = create_agent( model="gpt-4o", tools=[get_weather], # Only static tools registered here middleware=[DynamicToolMiddleware()],)# The agent can now use both get_weather AND calculate_tipresult = agent.invoke({ "messages": [{"role": "user", "content": "Calculate a 20% tip on $85"}]})
This approach is best when:
Tools are discovered at runtime (e.g., from an MCP server)
Tools are generated dynamically based on user data or configuration
You’re integrating with external tool registries
The wrap_tool_call hook is required for runtime-registered tools because the agent needs to know how to execute tools that weren’t in the original tool list. Without it, the agent won’t know how to invoke the dynamically added tool.
Some tools should run where your user’s app runs (typically the browser), not inside the process. Headless tools are tool definitions, which include the name, description, and argument schema, that you register on the server with your agent. The implementation is registered only on the client and executed after a short interrupt/resume handshake.This is different from ordinary tools whose function body runs on the server, and from server-side tool use where the model provider executes built-in tools remotely.
In both runtimes, the model sees a normal tool it can call, but the actual execution happens outside the server process.
Define a headless tool with tool(name=..., description=..., args_schema=...) from langchain.tools. A headless tool is schema-only, with no in-process implementation.
Register that tool with create_agent or your LangGraph graph so the model can call it normally.
Handle the interrupt payload when the tool is invoked. Instead of running locally, the graph pauses with a payload shaped like {"type": "tool", "tool_call": {"id", "name", "args"}}.
Resume the graph after your app, another service, or a human step performs the action. For browser-based flows, you can mirror the schema in the frontend and attach .implement(...) there.
If you call tool(...) in Python with only name, description, and args_schema, LangChain returns a HeadlessTool. There is no .implement() API on the Python side.
When the model issues a tool call for one of these tools, the run interrupts instead of executing the tool locally. Your app can inspect the payload, perform the action in the right environment (for example a browser, another service, or a human review step), then resume the graph with the tool result. When you use the supported JS SDK hooks, they can detect headless-tool interrupts, run the matching client implementation, and submit the resume command for you.Use the optional onTool callback to observe lifecycle events (start, success, error) for UI feedback such as spinners or toasts.
Headless tools frontend pattern
See an end-to-end example of schema-only tools executed in the client with useStream.
LangChain provides a large collection of prebuilt tools and toolkits for common tasks like web search, code interpretation, database access, and more. These ready-to-use tools can be directly integrated into your agents without writing custom code.See the tools and toolkits integration page for a complete list of available tools organized by category.
Some chat models feature built-in tools that are executed server-side by the model provider. These include capabilities like web search and code interpreters that don’t require you to define or host the tool logic.Refer to the individual chat model integration pages and the tool calling documentation for details on enabling and using these built-in tools.
Connect these docs to Claude, VSCode, and more via MCP for real-time answers.