How to Add a Custom Tool to a LangGraph Agent Workflow: Complete Implementation Guide
Adding a custom tool to a LangGraph agent workflow requires defining an async function with the @tool decorator, registering it in the ToolNode during graph construction in AgenticRAGService._build_graph, and emitting a matching tool_calls entry from any agent node that triggers the capability.
The production-grade RAG implementation in the jamwithai/production-agentic-rag-course repository demonstrates a scalable pattern for extending LangGraph agents with external capabilities. Adding a custom tool to LangGraph agent workflows follows a consistent three-step architecture that maintains type safety and clear separation of concerns.
Step 1: Define the Tool with @tool Decorator
Custom tool definitions reside in src/services/agents/tools.py, using a factory pattern that accepts service dependencies and returns an async function decorated with @tool from langchain_core.tools.
# src/services/agents/tools.py
# Lines 12-28 demonstrate the factory pattern implementation
def create_retriever_tool(
opensearch_client: OpenSearchClient,
embeddings_client: JinaEmbeddingsClient,
top_k: int = 3,
use_hybrid: bool = True,
):
@tool
async def retrieve_papers(query: str) -> list[Document]:
"""Search and return relevant arXiv research papers."""
# Embedding, OpenSearch call, and Document conversion logic
return documents
return retrieve_papers
Critical implementation requirements:
- Async signature: LangGraph executes nodes asynchronously, so tools must use
async def. - @tool decorator: Registers the function with LangChain's tool registry, enabling the graph to discover and invoke it by name.
- Dependency injection: The factory receives concrete service instances (OpenSearch, embeddings) rather than importing them directly, keeping the tool pure and testable.
- Return types: Functions should return serializable objects like
list[Document]that LangGraph can store in state.
Step 2: Register the Tool in the Graph Workflow
The workflow construction happens inside src/services/agents/agentic_rag.py within the AgenticRAGService._build_graph method. Here you instantiate the tool and add it to the ToolNode.
# src/services/agents/agentic_rag.py
# Lines 88-96 instantiate the tool
retriever_tool = create_retriever_tool(
opensearch_client=self.opensearch,
embeddings_client=self.embeddings,
top_k=self.graph_config.top_k,
use_hybrid=self.graph_config.use_hybrid,
)
tools = [retriever_tool]
# Lines 101-103 add the ToolNode to the graph
workflow.add_node("tool_retrieve", ToolNode(tools))
Configure Conditional Routing with tools_condition
After adding the node, establish edges that route execution to the tool when tool_calls are detected. The tools_condition function from LangGraph inspects the state messages for tool call requests.
# src/services/agents/agentic_rag.py
# Lines 26-34 configure conditional routing
workflow.add_conditional_edges(
"retrieve",
tools_condition, # Inspects state["messages"] for tool_calls
{
"tools": "tool_retrieve", # Routes to ToolNode when tools are requested
END: END,
},
)
tools_condition automatically detects when a node emits an AIMessage containing tool_calls and routes to the configured tool node. When the tool completes, execution returns to the calling node or proceeds to the next step in the workflow.
Step 3: Trigger the Tool from Agent Nodes
Agent nodes initiate tool execution by emitting an AIMessage with a populated tool_calls list. In src/services/agents/nodes/retrieve_node.py, the retrieve node constructs this message to trigger the retriever tool.
# src/services/agents/nodes/retrieve_node.py
# Lines 92-101 emit the tool call
updates["messages"] = [
AIMessage(
content="",
tool_calls=[
{
"id": f"retrieve_{new_attempt_count}",
"name": "retrieve_papers", # Must match the @tool function name
"args": {"query": question},
}
],
)
]
When LangGraph processes this state update, tools_condition detects the tool_calls entry, routes execution to the tool_retrieve node, and invokes retrieve_papers with the provided arguments. The tool's return value flows back into the agent state through extract_tool_artefacts in nodes/utils.py, making the results available for downstream grading and answer generation.
Practical Example: Adding a Calculator Tool
To add a math calculator alongside the existing retriever, follow the same three-step pattern.
1. Define the tool in tools.py:
# src/services/agents/tools.py
from langchain_core.tools import tool
def create_calculator_tool():
@tool
async def calc(expression: str) -> str:
"""Evaluate a simple arithmetic expression safely."""
allowed = "0123456789.+-*/() "
if any(ch not in allowed for ch in expression):
raise ValueError("Invalid characters in expression")
try:
result = eval(expression, {"__builtins__": {}})
except Exception as e:
raise ValueError(f"Failed to evaluate: {e}")
return str(result)
return calc
2. Register in _build_graph:
# src/services/agents/agentic_rag.py
calculator_tool = create_calculator_tool()
tools = [retriever_tool, calculator_tool] # Extend the list
workflow.add_node("tool_retrieve", ToolNode(tools))
3. Trigger from a node:
# Example payload emitted by any agent node
AIMessage(
content="",
tool_calls=[
{
"id": "calc_1",
"name": "calc", # Matches the @tool function name
"args": {"expression": "12 * (7 - 3)"},
}
],
)
The calculator executes within the same ToolNode, and its string result stores in relevant_tool_artefacts for later use in answer generation or reasoning steps.
Managing Tool Results in Agent State
The workflow persists tool outputs in src/services/agents/state.py, specifically within the relevant_tool_artefacts field defined at lines 41-50.
# src/services/agents/state.py
relevant_tool_artefacts: Optional[List[ToolArtefact]]
The ToolArtefact model in src/services/agents/models.py captures the tool name, call ID, content, and metadata, enabling downstream nodes to correlate results with specific tool invocations during multi-step reasoning.
Summary
- Define tools using the
@tooldecorator fromlangchain_core.toolsinsrc/services/agents/tools.py, using async factories for dependency injection. - Register tools in
AgenticRAGService._build_graphby adding them to theToolNodeand configuringtools_conditionfor routing. - Trigger tools by emitting
tool_callsentries inAIMessageobjects from any agent node, ensuring the"name"matches the decorated function name. - Access results through the
relevant_tool_artefactsfield inAgentState, which stores serialized tool outputs for downstream processing.
Frequently Asked Questions
Can I add synchronous tools to a LangGraph agent workflow?
While LangGraph supports synchronous functions, the jamwithai/production-agentic-rag-course implementation uses async patterns throughout for non-blocking I/O. If you must use a synchronous tool, wrap it in asyncio.to_thread or define it as def instead of async def, but prefer async for consistency with opensearch and embedding clients.
How does LangGraph route execution to the correct tool when multiple tools are registered?
The tools_condition function inspects the tool_calls list in the state messages and routes to the single ToolNode containing all registered tools. The ToolNode then dispatches to the specific tool based on the "name" field in each tool_calls entry. You do not need separate routing logic per tool.
What happens if a tool raises an exception during execution?
LangGraph's ToolNode catches exceptions and returns them as ToolMessage objects with error status, which flow back into the agent state. Your agent nodes should inspect relevant_tool_artefacts or the message history to detect failures and implement retry logic or error handling as demonstrated in the retrieve node's retry patterns.
Where should I initialize expensive resources like database connections for custom tools?
Follow the factory pattern shown in create_retriever_tool: accept initialized clients (OpenSearch, embeddings) as factory arguments rather than constructing them inside the tool function. Instantiate these clients in AgenticRAGService.__init__ and pass them to the factory during _build_graph, ensuring connections are reused across tool invocations and properly managed with the service lifecycle.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →