Tool Decorator
In shortLearn how to use the @tool decorator to attach metadata, tags, and capabilities to Python tool functions, plus filter and inspect tools at runtime.
- 5 min read
- 16 sections
- Updated
- v0.10.0
- Markdown
This example demonstrates how to use the @tool decorator to enrich tool functions with runtime-queryable metadata: names, descriptions, tags for filtering, capabilities, and arbitrary application data. You’ll build a set of decorated tools, sync, async, stateful, and learn to filter them by tag and introspect their metadata from your graph code.
Source example: agentflow/examples/tool-decorator/basic_decorator_usage.py
Prerequisites
- Python 3.12 or later
- 10xGraph installed:
pip install 10xgraph
How to run the example
Clone the repository and navigate to the example:
git clone https://github.com/10xGraph/10xGraph.git
cd 10xGraph/examples/tool-decorator
python basic_decorator_usage.pyYou should see output for basic tool schemas, tag filtering, metadata inspection, and an async tool.
Why use @tool?
Without the decorator, a function registered in a ToolNode exposes only its name, docstring, and parameter types to the LLM. The @tool decorator lets you attach rich metadata, explicit names, descriptions, tags for runtime filtering, provider hints, capabilities, and arbitrary key-value metadata, that your application code can query at runtime for tool selection, routing, auditing, or access control.
flowchart LR
A[Python function] -->|@tool decorator| B[Decorated function]
B -->|ToolNode| C[LLM tool schema]
B -->|get_tool_metadata| D[Runtime metadata]
style A fill:#4A90D9,color:#fff
style B fill:#7B68EE,color:#fff
style C fill:#50C878,color:#fff
style D fill:#F5A623,color:#fff
Imports
from tenxgraph.utils import (
tool,
get_tool_metadata,
has_tool_decorator,
END,
START,
)
from tenxgraph.core.graph.tool_node import ToolNode
from tenxgraph.core.state import AgentStateExample 1, Basic tool with explicit name
@tool(name="add_numbers")
def add(a: int, b: int) -> int:
"""Add two numbers together."""
return a + bThe name parameter overrides the Python function name in the LLM schema. Everything else (description, parameter types) is derived from the docstring and annotations.
Example 2, Decorator with no arguments (uses function name)
@tool
def multiply(x: int, y: int) -> int:
"""Multiply two numbers together."""
return x * yWhen no arguments are provided, @tool uses the function name (multiply) and docstring as-is.
Example 3, Full metadata
@tool(
name="web_search",
description="Search the web for information using a search engine",
tags=["search", "web", "external"],
provider="custom",
capabilities=["network_access"],
metadata={"rate_limit": 100, "timeout": 30},
)
def search_web(query: str, max_results: int = 10) -> list[str]:
"""Simulate a web search."""
return [f"Result {i + 1} for '{query}'" for i in range(max_results)]Metadata fields
| Field | Type | Description |
|---|---|---|
name |
str |
Name exposed to the LLM in the tool schema |
description |
str |
Description shown to the LLM (defaults to docstring) |
tags |
list[str] |
Arbitrary labels for filtering at runtime |
provider |
str |
Hint about which system provides this tool |
capabilities |
list[str] |
Capability strings (e.g. "network_access", "database_write") |
metadata |
dict |
Arbitrary key-value data for application use |
Example 4, Async tool
import asyncio
@tool(
name="fetch_data",
description="Asynchronously fetch data from a remote API",
tags=["async", "api", "fetch"],
)
async def fetch_data_async(endpoint: str, timeout: int = 5) -> dict:
"""Fetch data asynchronously from an API endpoint."""
await asyncio.sleep(0.1) # simulated I/O
return {"endpoint": endpoint, "data": "sample data"}ToolNode handles async functions automatically. You do not need to change any other part of your graph.
Example 5, Injectable parameters
Parameters with reserved names (such as state, config and tool_call_id) are injected automatically by the framework, matched by parameter name, and do not appear in the LLM schema. This lets tools access conversation state without the LLM needing to pass it.
@tool(
name="stateful_calculator",
description="Calculator that can access agent state",
tags=["calculator", "stateful"],
)
def stateful_add(a: int, b: int, state: AgentState | None = None) -> int:
"""The 'state' parameter is injected, it won't appear in the LLM's tool schema."""
result = a + b
if state:
# Access conversation history, custom fields, etc.
pass
return resultOther injectable names include tool_call_id (the call ID from the LLM), config, emit, generated_id, context_manager, publisher, checkpointer, store and task_manager.
Registering tools with ToolNode
tool_node = ToolNode([add, multiply, search_web, fetch_data_async, stateful_add])
tools = tool_node.get_local_tool() # returns list of tool schema dicts
for schema in tools:
fn = schema["function"]
print(fn["name"], "-", fn["description"])Tag-based filtering
At runtime you can request only tools that match a set of tags. A tool with no tags is never filtered out, so untagged tools (such as add and multiply above) are returned for every tag filter.
# Database-tagged tools, plus any untagged tools
db_tools = tool_node.get_local_tool(tags={"database"})
# Read-tagged tools, plus any untagged tools
read_tools = tool_node.get_local_tool(tags={"read"})
# External-tagged tools, plus any untagged tools
network_tools = tool_node.get_local_tool(tags={"external"})flowchart TD
A[ToolNode\nadd, multiply, search_web\nread_from_db, write_to_db] -->|tags=database| B[read_from_db\nwrite_to_db]
A -->|tags=search| C[search_web]
A -->|tags=read| D[read_from_db]
style A fill:#7B68EE,color:#fff
style B fill:#50C878,color:#fff
style C fill:#F5A623,color:#fff
style D fill:#4A90D9,color:#fff
Inspecting metadata at runtime
from tenxgraph.utils import has_tool_decorator, get_tool_metadata
# Check if a function was decorated
print(has_tool_decorator(add)) # True
print(has_tool_decorator(lambda x: x)) # False
# Read full metadata
meta = get_tool_metadata(search_web)
print(meta["name"]) # "web_search"
print(meta["tags"]) # {"search", "web", "external"} (a set)
print(meta["capabilities"]) # ["network_access"]
print(meta["metadata"]) # {"rate_limit": 100, "timeout": 30}Complete source
import asyncio
from tenxgraph.core.graph.tool_node import ToolNode
from tenxgraph.core.state import AgentState
from tenxgraph.utils import END, START, get_tool_metadata, has_tool_decorator, tool
@tool(name="add_numbers")
def add(a: int, b: int) -> int:
"""Add two numbers together."""
return a + b
@tool
def multiply(x: int, y: int) -> int:
"""Multiply two numbers."""
return x * y
@tool(
name="web_search",
description="Search the web for information",
tags=["search", "web", "external"],
metadata={"rate_limit": 100},
)
def search_web(query: str, max_results: int = 10) -> list[str]:
return [f"Result {i + 1} for '{query}'" for i in range(max_results)]
@tool(name="fetch_data", tags=["async", "api"])
async def fetch_data_async(endpoint: str) -> dict:
await asyncio.sleep(0.1)
return {"endpoint": endpoint}
@tool(name="database_read", tags=["database", "read"])
def read_from_db(table: str, record_id: int) -> dict:
return {"table": table, "id": record_id}
@tool(name="database_write", tags=["database", "write"])
def write_to_db(table: str, data: dict) -> bool:
return True
if __name__ == "__main__":
tool_node = ToolNode([add, multiply, search_web, read_from_db, write_to_db])
# All tools
print("All tools:", [t["function"]["name"] for t in tool_node.get_local_tool()])
# Filtered by tag
print("Database tools:", [t["function"]["name"] for t in tool_node.get_local_tool(tags={"database"})])
# Metadata inspection
print("search_web tags:", get_tool_metadata(search_web)["tags"])Key concepts
| Concept | Details |
|---|---|
@tool |
Decorator that attaches metadata to a Python function for use in ToolNode |
tags |
Arbitrary string labels, stored as a set; filter with get_local_tool(tags={...}) (untagged tools always pass) |
| Injectable params | Reserved names such as state, config, tool_call_id, supplied by the runtime, hidden from LLM schema |
has_tool_decorator |
Returns True if a function was wrapped with @tool |
get_tool_metadata |
Returns the full metadata dict from a decorated function |
What you learned
- How to use
@toolwith no arguments, with just a name, and with full metadata. - How to add async tools transparently.
- How to use injectable parameters to give tools access to conversation state.
- How to filter tools by tag at runtime.
- How to introspect tool metadata programmatically.
Next step
→ ReAct Agent, build a full ReAct loop with a checkpointer for persistent conversation history.