Tools

This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

This page was written against langchain 1.4.3, langchain-core 1.6.5, langgraph 1.2.12 and langchain-ollama 1.1.0 on Python 3.12, as verified on 2026-09-29. The running example is DocsAssistant, which answers questions over this site’s documentation using Ollama’s llama3.1:8b chat model and the nomic-embed-text embedding model over the pgvector store built in Integrating with LangChain. Ollama was not available while the snippets were checked, so every output shown here was produced by running the code against the real packages with a scripted fake chat model (GenericFakeChatModel) and a small in-memory stub in place of the pgvector retriever; no output comes from a live model.

A LangChain tool is a callable plus the metadata a model needs to decide when to call it: a name, a description and an argument schema. LangChain builds that metadata from the function you write, runs the function when the model asks for it, and wraps the result in a ToolMessage the model can read on the next turn. This page covers the mechanics of doing that in LangChain 1.x. The rules for deciding what a good tool looks like (narrow, typed, paginated, least-privilege, approval-gated) are deliberately not repeated here; they live in Tool Design, and the wire-level function-calling formats live in Structured Output & Tool Calling. The agent that uses these tools is built in Agents with create_agent; this page uses create_agent only where a tool needs an agent around it.

Defining a tool with @tool

The @tool decorator turns a plain function into a tool object. It is imported from langchain.tools in LangChain 1.x. The function name becomes the tool name, the docstring becomes the description the model reads, and the type hints become the argument schema:

DocsAssistant’s search_docs tool
from langchain.tools import tool

from docsassistant.search import retriever   # pgvector-backed in DocsAssistant


@tool(parse_docstring=True)
def search_docs(query: str, limit: int = 4) -> str:
    """Search the documentation site and return matching sections.

    Use a key phrase from the question, not the whole question.

    Args:
        query: Key phrase to search for, for example "HNSW ef_search".
        limit: Maximum number of sections to return.
    """
    hits = retriever(query, k=limit)
    if not hits:
        return "No matching sections. Try a shorter key phrase."
    return "\n".join(f"{h['page']} :: {h['section']} :: {h['content']}" for h in hits)


print(search_docs.name)
print(search_docs.description)
print(search_docs.invoke({"query": "HNSW"}))

Run against a two-section in-memory stub, this printed:

Output
search_docs
Search the documentation site and return matching sections. Use a key phrase from the question, not the whole question.
modules/ROOT/pages/database/vector-rag/ann-indexes.adoc :: HNSW :: HNSW ef_search trades recall for latency.
modules/ROOT/pages/database/vector-rag/postgresql-pgvector.adoc :: Indexes :: pgvector supports HNSW and IVFFlat.

A tool is a Runnable, so invoke takes the argument dictionary. The name defaults to the function name and can be overridden with @tool("other_name"). See LangChain — Tools and the reference for the tool decorator.

Schemas and docstrings

Three things reach the model: the name, the description and the JSON Schema of the arguments. Where each comes from is under your control:

  • Description. By default the whole docstring. With parse_docstring=True LangChain parses a Google-style docstring: the summary and body become the tool description and the Args: entries become per-argument descriptions in the schema, as seen above. The installed tool decorator also accepts description= to set it explicitly.

  • Arguments. Inferred from the type hints. Literal["a", "b"] becomes an enum, defaults become default, and arguments without a default are required.

  • Explicit schema. args_schema takes a Pydantic model (or a JSON Schema dictionary) when the signature alone is not expressive enough, or when you want field-level descriptions and validation without a docstring.

The schema the model actually sees can be inspected on the tool. For the docstring-parsed tool above:

The JSON Schema derived from search_docs
import json

print(json.dumps(search_docs.tool_call_schema.model_json_schema(), indent=1))
Output
{
 "description": "Search the documentation site and return matching sections. Use a key phrase from the question, not the whole question.",
 "properties": {
  "query": {
   "description": "Key phrase to search for, for example \"HNSW ef_search\".",
   "title": "Query",
   "type": "string"
  },
  "limit": {
   "default": 4,
   "description": "Maximum number of sections to return.",
   "title": "Limit",
   "type": "integer"
  }
 },
 "required": [
  "query"
 ],
 "title": "search_docs",
 "type": "object"
}

The same tool with an explicit args_schema. Pydantic validates the arguments before the function runs, so a value outside the Literal never reaches your code:

An explicit args_schema
from typing import Literal

from pydantic import BaseModel, Field


class SearchInput(BaseModel):
    """Input for documentation search."""

    query: str = Field(description="Key phrase to search for")
    component: Literal["vector-rag", "mcp"] = Field(default="vector-rag", description="Documentation area")


@tool("search_docs_v2", args_schema=SearchInput)
def search_docs_v2(query: str, component: str = "vector-rag") -> str:
    """Search one documentation area."""
    return f"{component}: {query}"


print(search_docs_v2.args)
print(search_docs_v2.invoke({"query": "x", "component": "mcp"}))
search_docs_v2.invoke({"query": "x", "component": "nope"})   # raises pydantic ValidationError
Output
{'query': {'description': 'Key phrase to search for', 'title': 'Query', 'type': 'string'}, 'component': {'default': 'vector-rag', 'description': 'Documentation area', 'enum': ['vector-rag', 'mcp'], 'title': 'Component', 'type': 'string'}}
mcp: x
ValidationError

The design-side advice on descriptions (write them for a new colleague, say when not to use the tool, evaluate them like prompts) is in Tool Design. See LangChain — Tools.

Binding tools to a model with bind_tools

A tool does nothing until a chat model is told about it. bind_tools returns a new runnable that sends the tool definitions with every request; the model then answers either with text or with tool_calls on its AIMessage. The bound model does not run anything. Running tool calls is the job of the caller, which is what create_agent automates.

Binding search_docs to DocsAssistant’s chat model
from langchain_ollama import ChatOllama

model = ChatOllama(model="llama3.1:8b", temperature=0)
bound = model.bind_tools([search_docs])

print(type(bound).__name__)
print(bound.kwargs["tools"][0]["function"]["name"])

bind_tools only builds the request; nothing was sent to Ollama, so this ran without a server. The definition it attached is the OpenAI-style function description of the schema shown earlier (type: "function", then the name, description and parameters), and the bound object is a _ChatModelBinding that forwards it as the tools keyword. bind_tools also accepts tool_choice, whose accepted values are provider-specific; the models page shows "any", for example, to require that the model call some tool. See LangChain — Models (tool calling).

When the model replies, its AIMessage carries structured calls. Invoking a tool with one of those calls (a dictionary with name, args, id and type: "tool_call") returns a ToolMessage that already has the right tool_call_id, ready to append to the message list:

Running a tool call by hand
from langchain_core.messages import AIMessage

reply = AIMessage(
    content="",
    tool_calls=[{"name": "search_docs", "args": {"query": "HNSW"}, "id": "call_1", "type": "tool_call"}],
)
tool_message = search_docs.invoke(reply.tool_calls[0])

print(type(tool_message).__name__, tool_message.tool_call_id, tool_message.name)
Output
ToolMessage call_1 search_docs

Here the AIMessage was scripted rather than produced by a model. Handing tool_calls to invoke and appending the resulting ToolMessage after the AIMessage is the manual form of the loop that agents run for you. The message types themselves are covered in Chat Models & Messages, and structured output is the other main consumer of bind_tools (see Structured Output). See LangChain — Models for tool calling on chat models.

ToolRuntime and context injection

Some things a tool needs must never be arguments the model chooses: the current user, the tenant, a database connection, the conversation so far. LangChain 1.x gives a tool access to them through a parameter typed ToolRuntime. The parameter is named runtime by convention, is injected when the tool runs, and is not included in the schema sent to the model, so the model can neither see nor forge it.

From runtime a tool can read:

  • runtime.state — the current agent state, such as the message list

  • runtime.context — immutable per-invocation context you pass at invoke time (user ID, component, role)

  • runtime.store — the long-term memory store (see Long-Term Memory Stores)

  • runtime.tool_call_id — the ID of the call being answered, needed to build a ToolMessage by hand

  • runtime.stream_writer — emits custom updates while the tool runs (see Streaming)

ToolRuntime is generic over the context and state types, which is how a type checker knows what runtime.context holds. The example below gives DocsAssistant a search tool that searches only the current user’s documentation area, and a tool that remembers a topic in the store and writes to agent state by returning a Command:

State, context and store inside tools
from dataclasses import dataclass

from langchain.agents import AgentState, create_agent
from langchain.messages import ToolMessage
from langchain.tools import ToolRuntime, tool
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.memory import InMemoryStore
from langgraph.types import Command


@dataclass
class DocsContext:
    user_id: str
    component: str


class DocsState(AgentState):
    last_query: str


@tool
def scoped_search(query: str, runtime: ToolRuntime[DocsContext]) -> str:
    """Search the docs of the user's current component."""
    n = len(runtime.state["messages"])
    return f"[{runtime.context.component}] results for {query!r} (messages so far: {n}, user: {runtime.context.user_id})"


@tool
def remember_topic(topic: str, runtime: ToolRuntime[DocsContext, DocsState]) -> Command:
    """Remember the topic the user is interested in, for later sessions."""
    runtime.store.put(("topics", runtime.context.user_id), "latest", {"topic": topic})
    return Command(update={
        "last_query": topic,
        "messages": [ToolMessage(content=f"Noted: {topic}", tool_call_id=runtime.tool_call_id)],
    })


print("schema seen by model:", list(scoped_search.tool_call_schema.model_json_schema()["properties"]))

store = InMemoryStore()
agent = create_agent(
    model,                                   # scripted fake model in the run below
    tools=[scoped_search, remember_topic],
    context_schema=DocsContext,
    state_schema=DocsState,
    store=store,
    checkpointer=InMemorySaver(),
)
out = agent.invoke(
    {"messages": [{"role": "user", "content": "Tell me about HNSW"}]},
    config={"configurable": {"thread_id": "t1"}},
    context=DocsContext(user_id="u1", component="vector-rag"),
)
print("state field:", out["last_query"])
print("store:", store.get(("topics", "u1"), "latest").value)

With a fake model scripted to call scoped_search, then remember_topic, then answer, this printed:

Output
schema seen by model: ['query']
HumanMessage | Tell me about HNSW
AIMessage |
ToolMessage | [vector-rag] results for 'HNSW' (messages so far: 2, user: u1)
AIMessage |
ToolMessage | Noted: HNSW
AIMessage | Done.
state field: HNSW
store: {'topic': 'HNSW'}

Three details are worth noting. The model-visible schema contains only query; runtime is hidden. A tool that returns a Command updates agent state directly, and because it is then responsible for the conversation record it must include a ToolMessage carrying runtime.tool_call_id in its messages update. And context is passed per call, next to the thread_id that persists the conversation (see Short-Term Memory & Checkpointers), which makes it the right place for identity and authorization: the tool trusts runtime.context.user_id, never an argument. See LangChain — Tools and LangChain — Runtime.

Tool errors

A tool can fail in three ways: the model passes arguments that do not match the schema, the function raises, or an external system misbehaves. What happens next depends on where the tool runs, and each layer was exercised with the same tiny tool (flaky(n) divides 10 by n) so the behavior is observed rather than assumed.

Raising ToolException on the tool

A tool can raise ToolException for a failure it expects and describes. Setting handle_tool_error on the tool makes the exception’s message come back to the model as the tool result instead of aborting the run:

ToolException with handle_tool_error
from langchain.tools import ToolException, tool


@tool
def get_page(path: str) -> str:
    """Return the text of one documentation page by repository path."""
    if not path.startswith("modules/ROOT/pages/"):
        raise ToolException(
            f"Unknown path {path!r}. Pass a path returned by search_docs, e.g. modules/ROOT/pages/ai/index.adoc"
        )
    return "page text"


get_page.handle_tool_error = True   # return the exception message as the tool result
msg = get_page.invoke({"name": "get_page", "args": {"path": "etc/passwd"}, "id": "x", "type": "tool_call"})
print(repr(msg.content))
Output
"Unknown path 'etc/passwd'. Pass a path returned by search_docs, e.g. modules/ROOT/pages/ai/index.adoc"

The message is the whole point: it tells the model what was wrong and what to try, which is the "actionable error" rule from Tool Design.

ToolNode and handle_tool_errors

Inside a hand-built graph, tools run in ToolNode (from langgraph.prebuilt). Its handle_tool_errors argument controls what is caught. Observed with the version above:

The probe tool
@tool
def flaky(n: int) -> str:
    """Divide 10 by n."""
    return str(10 // n)
Table 1. ToolNode error handling, observed
Setting Invalid arguments (n="abc") Exception in the tool (n=0)

default

Returned to the model as an error ToolMessage naming the failing field

Re-raised as ZeroDivisionError; the run stops

True

Returned to the model

Returned as Error: ZeroDivisionError(…​) followed by "Please fix your mistakes."

a string

Returned to the model

That fixed string is the result and the message status is error

a tuple of exception types

Returned to the model only if ToolInvocationError is in the tuple; otherwise raised as ToolInvocationError

Caught and returned only if it is one of those types; any other exception is re-raised

The default therefore forwards schema violations to the model (which can fix its own arguments) but lets genuine bugs surface to the developer. See LangGraph Fundamentals for ToolNode in a graph. See LangChain — Tools (ToolNode).

Agent-wide handling with wrap_tool_call

When you use create_agent, the documented place to handle tool errors for every tool at once is a wrap_tool_call middleware, which wraps each execution and can turn any exception into a ToolMessage:

Turning every tool failure into a model-readable message
from collections.abc import Callable

from langchain.agents import create_agent
from langchain.agents.middleware import wrap_tool_call
from langchain.messages import ToolMessage
from langchain.tools.tool_node import ToolCallRequest


@wrap_tool_call
def tool_errors_to_model(
    request: ToolCallRequest,
    handler: Callable[[ToolCallRequest], ToolMessage],
) -> ToolMessage:
    try:
        return handler(request)
    except Exception as e:
        return ToolMessage(
            content=f"Tool error: {e}. Check the arguments and try again.",
            tool_call_id=request.tool_call["id"],
            status="error",
        )


agent = create_agent(model, tools=[flaky], middleware=[tool_errors_to_model])

With a scripted model that calls flaky with n=0 and then apologizes, the run produced a ToolMessage with status error (Tool error: integer division or modulo by zero. Check the arguments and try again.) followed by the final answer, instead of an exception. Retry, fallback and call-limit middleware are covered in Middleware; approval before a destructive tool runs is covered in Human-in-the-Loop & Time Travel and, framework-neutrally, in Human-in-the-Loop. See LangChain — Tools (error handling).

Returning artifacts

The string a tool returns goes into the model’s context window. Often the application also wants the full, structured result (the raw hits, scores and source IDs) without paying context tokens for it. @tool supports this with response_format="content_and_artifact": the function returns a two-element tuple, the first part is the content the model reads, and the second is an artifact attached to the ToolMessage but not sent to the model:

A search tool that returns content for the model and an artifact for the application
from langchain.tools import tool


@tool(response_format="content_and_artifact")
def search_docs_with_sources(query: str) -> tuple[str, list[dict]]:
    """Search the documentation and return a short summary for the model."""
    hits = retriever(query)
    summary = "\n".join(f"- {h['page']} ({h['section']})" for h in hits) or "No matches."
    return summary, hits        # (content for the model, artifact for the application)


call = {"name": "search_docs_with_sources", "args": {"query": "HNSW"}, "id": "call_1", "type": "tool_call"}
msg = search_docs_with_sources.invoke(call)
print(type(msg).__name__)
print("content :", msg.content.replace("\n", " | "))
print("artifact:", [h["section"] for h in msg.artifact])
print("plain invoke (no tool call):", type(search_docs_with_sources.invoke({"query": "HNSW"})).__name__)
Output
ToolMessage
content : - modules/ROOT/pages/database/vector-rag/ann-indexes.adoc (HNSW) | - modules/ROOT/pages/database/vector-rag/postgresql-pgvector.adoc (Indexes)
artifact: ['HNSW', 'Indexes']
plain invoke (no tool call): str

The last line matters: the artifact exists only when the tool is invoked with a tool call, because the ToolMessage is what carries it. Called with a plain argument dictionary, the tool returns just the content string. DocsAssistant uses this to show citations in its UI (page paths and scores from the artifact) while the model sees only the compact list, which is the concise-versus-detailed trade-off from Tool Design taken one step further. The attribute names above were confirmed in the installed langchain-core, and the parameter is described in the reference for the tool decorator; the retrieval side of this pattern is in Retrieval & RAG Bridge.

A tool can also return a Command to change state (shown earlier), and tools served by an MCP server return their results through the adapter described in MCP Adapters; the protocol itself is covered in the Model Context Protocol (MCP) sub-section.

Destructive tools and secrets

A tool that writes to the outside world needs an approval gate and a narrowly scoped credential. The credential comes from the environment or a secret store at call time, never from the source or the model’s arguments. Here is DocsAssistant’s issue-opening tool behind HumanInTheLoopMiddleware, followed by a rejected decision:

An approval-gated write tool
import os

from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langchain.tools import tool
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command

REPO = "albertoirurueta/docs"          # the only repository this tool may write to


@tool
def open_github_issue(title: str, body: str) -> str:
    """Open an issue in the documentation repository. Destructive: creates public content."""
    token = os.environ["DOCS_ISSUES_TOKEN"]   # fine-grained token, Issues: write on REPO only
    # ... POST https://api.github.com/repos/{REPO}/issues with the token ...
    return f"Opened issue in {REPO}: {title}"


agent = create_agent(
    model,
    tools=[open_github_issue],
    middleware=[HumanInTheLoopMiddleware(interrupt_on={"open_github_issue": True})],
    checkpointer=InMemorySaver(),
)
cfg = {"configurable": {"thread_id": "t"}}
out = agent.invoke({"messages": [{"role": "user", "content": "report the broken link"}]}, cfg)
print("interrupted:", "__interrupt__" in out, [i.value["action_requests"][0]["name"] for i in out["__interrupt__"]])
out = agent.invoke(Command(resume={"decisions": [{"type": "reject", "message": "not now"}]}), cfg)
print(out["messages"][-2].content)
Output
interrupted: True ['open_github_issue']
User rejected the tool call for `open_github_issue` with reason: not now

The run paused before the tool executed, and the rejection came back to the model as the tool result, so the token was never read. The environment variable holds a fine-grained token limited to issue writes on one repository; the snippet was run with a placeholder value set in the shell, and no real credential appears anywhere on this page. See LangChain — Human-in-the-loop.

Summary

Table 2. Where each tool concern lives
Concern LangChain 1.x mechanism Checked above

Define a tool

@tool from langchain.tools

search_docs

Description and argument docs

Docstring, parse_docstring=True, args_schema

Schema printed, ValidationError on bad enum

Offer tools to a model

model.bind_tools([…​], tool_choice=…​)

ChatOllama binding, no server needed

Hide identity and connections from the model

runtime: ToolRuntime[Context, State]

scoped_search, remember_topic

Handle failures

ToolException + handle_tool_error, ToolNode(handle_tool_errors=…​), wrap_tool_call

Observed behaviors in the table

Keep bulky data out of the prompt

response_format="content_and_artifact"

search_docs_with_sources

Guard destructive tools

HumanInTheLoopMiddleware, env-var credentials

Rejection run