Custom Tools
Extend Agentmd with custom Python tools that add domain-specific capabilities beyond the built-in tools. Custom tools are Python files with @tool-decorated functions that agents can call during execution.
Quick Start
1. Create a Tool File
Create a Python file in workspace/agents/tools/:
# workspace/agents/tools/sentiment_analyzer.py
from langchain_core.tools import tool
@tool
def analyze_sentiment(text: str) -> str:
"""Analyze the sentiment of the given text.
Args:
text: The text to analyze.
Returns:
Sentiment classification: positive, negative, or neutral.
"""
positive_words = ["good", "great", "excellent", "happy", "love"]
negative_words = ["bad", "terrible", "awful", "hate", "sad"]
text_lower = text.lower()
pos_count = sum(word in text_lower for word in positive_words)
neg_count = sum(word in text_lower for word in negative_words)
if pos_count > neg_count:
return "positive"
elif neg_count > pos_count:
return "negative"
else:
return "neutral"
2. Declare the Tool in Agent Frontmatter
Reference the tool file (without .py extension) in the custom_tools field:
---
name: review-analyzer
custom_tools:
- sentiment_analyzer
---
You are a product review analyzer.
Read reviews from `reviews.txt` and use the `analyze_sentiment` tool to classify each review.
Write a summary to `sentiment-report.txt`.
3. Run the Agent
The agent now has access to analyze_sentiment alongside the built-in tools.
The @tool Decorator
The @tool decorator from LangChain converts a regular Python function into a tool that LLMs can call.
Requirements
- Docstring is required: The first line becomes the tool description shown to the LLM
- Type hints are required: Parameters must have type annotations (
str,int,float,bool,dict,list) - Return type: Must return a string (or be converted to string)
- Args documentation: Use Google-style docstrings to document parameters
from langchain_core.tools import tool
@tool
def my_tool(param: str, count: int) -> str:
"""Tool description that the LLM sees.
Args:
param: Parameter description.
count: Numeric parameter.
Returns:
What the tool returns.
"""
return f"Processed: {param} x {count}"
Supported Parameter Types
@tool
def example_tool(
text: str,
count: int,
threshold: float,
enabled: bool,
options: dict,
items: list,
) -> str:
"""Example showing all supported parameter types."""
return f"Received: {text}, {count}, {threshold}"
File Location & Naming
All custom tools must be in:
Each Python file can contain one or more @tool-decorated functions.
Naming Conventions
- File names:
snake_case.py(e.g.,sentiment_analyzer.py) - Function names:
snake_case(e.g.,analyze_sentiment) - Tool names in frontmatter: Match the file name without
.py
Single-Tool Files
Most common pattern: one tool per file.
# workspace/agents/tools/word_counter.py
from langchain_core.tools import tool
@tool
def count_words(text: str) -> str:
"""Count the number of words in the given text.
Args:
text: The text to analyze.
Returns:
Word count as a string.
"""
return f"Word count: {len(text.split())}"
Declare in frontmatter: custom_tools: [word_counter]
Multi-Tool Files
One file can export multiple related tools. All @tool-decorated functions are loaded automatically:
# workspace/agents/tools/text_utils.py
from langchain_core.tools import tool
@tool
def count_words(text: str) -> str:
"""Count words in text."""
return str(len(text.split()))
@tool
def to_uppercase(text: str) -> str:
"""Convert text to uppercase."""
return text.upper()
Declare in frontmatter: custom_tools: [text_utils]
Examples
Example 1: API Integration
# workspace/agents/tools/weather_api.py
from langchain_core.tools import tool
import httpx
import os
@tool
def get_weather(city: str) -> str:
"""Fetch current weather for a city using OpenWeatherMap API.
Args:
city: City name (e.g., "London", "New York").
Returns:
Weather description and temperature.
"""
api_key = os.environ.get("OPENWEATHER_API_KEY")
if not api_key:
return "ERROR: OPENWEATHER_API_KEY not set"
url = f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={api_key}&units=metric"
try:
response = httpx.get(url, timeout=10)
response.raise_for_status()
data = response.json()
temp = data["main"]["temp"]
description = data["weather"][0]["description"]
return f"{city}: {description}, {temp}°C"
except Exception as e:
return f"ERROR: {e}"
Usage in agent:
Example 2: Data Processing
# workspace/agents/tools/csv_parser.py
from langchain_core.tools import tool
import csv
from io import StringIO
@tool
def parse_csv(csv_content: str) -> str:
"""Parse CSV content and return row count and column names.
Args:
csv_content: CSV data as a string.
Returns:
Summary of the CSV structure.
"""
try:
reader = csv.DictReader(StringIO(csv_content))
rows = list(reader)
if not rows:
return "Empty CSV"
columns = list(rows[0].keys())
return f"Columns: {', '.join(columns)}\nRows: {len(rows)}"
except Exception as e:
return f"ERROR parsing CSV: {e}"
Usage in agent:
Example 3: Text Transformation
# workspace/agents/tools/markdown_converter.py
from langchain_core.tools import tool
@tool
def markdown_to_html(markdown: str) -> str:
"""Convert Markdown to HTML.
Args:
markdown: Markdown-formatted text.
Returns:
HTML string.
"""
try:
import markdown
return markdown.markdown(markdown)
except ImportError:
return "ERROR: markdown package not installed. Run: pip install markdown"
except Exception as e:
return f"ERROR: {e}"
Usage in agent:
Dependencies
Custom tools can use any Python package available in your environment.
Installing Packages
Handling Missing Dependencies
Always check for import errors and return helpful messages:
@tool
def advanced_analysis(data: str) -> str:
"""Analyze data using pandas."""
try:
import pandas as pd
except ImportError:
return "ERROR: pandas not installed. Run: pip install pandas"
# Tool logic here
return "Analysis complete"
Declaring Tools in Frontmatter
Use the custom_tools field (or tools alias) to declare which tools the agent can access.
Built-in tools (file_read, file_write, file_edit, file_glob, http_request) are always available. Custom tools are added on top.
SDK — Path Resolution for Custom Tools
Custom tools that need to read or write files should use agent_md.sdk for sandbox-safe path resolution. This gives your tool the same security guarantees as built-in tools.
Available Functions
| Function | Returns | Description |
|---|---|---|
resolve_path(path) |
(Path \| None, str \| None) |
Resolve aliases + validate sandbox |
workspace_root() |
Path |
Absolute path to workspace root |
agent_name() |
str |
Display name of the executing agent |
agent_id() |
str |
Canonical id (filename stem) of the executing agent |
agent_paths() |
dict[str, Path] |
Agent's declared path aliases, resolved |
execution_id() |
int \| None |
Id of the execution currently running |
All functions are available during agent execution. Calling them outside of execution raises RuntimeError.
A custom tool writing into a watched directory retriggers the agent
The built-in write tools record what they write so a trigger: watch agent does not fire on its own changes. A custom tool writing with plain write_text leaves no such record, so the agent will trigger itself in a loop. Write through file_write when the target is inside a path the agent watches, or keep the two apart. See Known limits.
For pausing execution to ask the user a question, see SDK — Human-in-the-Loop Primitives below.
Example: File Processing Tool
# workspace/agents/_config/tools/word_count.py
from langchain_core.tools import tool
from agent_md.sdk import resolve_path
@tool
def word_count(path: str) -> str:
"""Count words in a file, with sandbox validation.
Args:
path: Path to the file (supports aliases like {data}/file.txt).
Returns:
Word count or error message.
"""
resolved, error = resolve_path(path)
if error:
return f"ERROR: {error}"
try:
text = resolved.read_text()
return f"{len(text.split())} words in {resolved.name}"
except Exception as e:
return f"ERROR: {e}"
Example: Context-Aware Tool
# workspace/agents/_config/tools/file_info.py
from langchain_core.tools import tool
from agent_md.sdk import agent_name, workspace_root, agent_paths
@tool
def show_context() -> str:
"""Show the current agent's workspace context.
Returns:
Agent name, workspace root, and declared paths.
"""
paths = agent_paths()
path_list = "\n".join(f" {alias}: {path}" for alias, path in paths.items())
return (
f"Agent: {agent_name()}\n"
f"Workspace: {workspace_root()}\n"
f"Paths:\n{path_list or ' (none)'}"
)
When to Use the SDK
- Use
resolve_pathwhen your tool reads or writes files — it validates sandbox rules and resolves aliases - Use
workspace_rootwhen you need the base directory for relative operations - Use
agent_namefor logging, naming output files, or conditional behavior per agent - Use
agent_pathsto discover which directories the agent has access to
When NOT to Use the SDK
If your tool doesn't touch the filesystem (API calls, text processing, calculations), you don't need the SDK. A plain @tool function works fine.
SDK — Human-in-the-Loop Primitives
Custom tools can pause execution and ask the user a question using three functions from agent_md.sdk. The execution suspends until a response arrives — surviving a backend restart — then resumes from where it stopped. See Human-in-the-Loop for the full guide.
Available Functions
| Function | Returns | Description |
|---|---|---|
request_confirmation(message, *, tool_name=None, tool_args=None) |
bool |
Ask yes/no; returns True if approved |
request_input(message) |
str |
Ask for free text; returns what the user typed |
request_choice(message, options, *, multi=False) |
str \| list[str] |
Ask the user to pick from a list; returns the selection |
Example: Confirm before a destructive action
from langchain_core.tools import tool
from agent_md.sdk import request_confirmation, request_choice
@tool
def cleanup(folder: str) -> str:
"""Delete temp files in a folder."""
if not request_confirmation(f"Clean {folder}?", tool_name="cleanup", tool_args={"folder": folder}):
return "User declined."
# ... do the cleanup ...
return "Cleaned."
Example: Ask for free text
from langchain_core.tools import tool
from agent_md.sdk import request_input
@tool
def create_report(topic: str) -> str:
"""Generate a report on a topic."""
title = request_input(f"Enter a title for the '{topic}' report:")
# ... generate with title ...
return f"Report '{title}' created."
Example: Pick from options
from langchain_core.tools import tool
from agent_md.sdk import request_choice
@tool
def deploy(artifact: str) -> str:
"""Deploy an artifact to an environment."""
env = request_choice("Deploy to which environment?", ["staging", "production"])
# ... deploy to env ...
return f"Deployed {artifact} to {env}."
Note: These primitives are not affected by
auto_approve. They are for dynamic, in-code questions — distinct from the guarded-tool mechanism, which guards a whole tool by name. You can use both in the same agent.
Troubleshooting
Tool file not found
Error:
FileNotFoundError: Custom tool 'my_tool' not found: expected file at workspace/agents/tools/my_tool.py
Solution:
1. Check that the file exists at workspace/agents/tools/my_tool.py
2. Verify the file name matches the custom_tools declaration
3. File names are case-sensitive on Linux/macOS
No @tool definitions found
Error:
Solution:
from langchain_core.tools import tool
@tool
def my_function(input: str) -> str:
"""Description."""
return input
Import from langchain_core.tools, not langchain.tools.
Missing docstring or type hints
Error:
ValueError: Tool function must have a docstring
ValidationError: Field 'param' requires type annotation
Solution:
@tool
def my_tool(param: str, count: int) -> str:
"""This is the tool description.
Args:
param: Parameter description.
count: Parameter description.
Returns:
What the tool returns.
"""
return f"{param} x {count}"
Tool not loaded
Problem: Agent says "I don't have access to that tool"
Solution:
1. Check that the tool is declared in custom_tools
2. Verify the tool file is in workspace/agents/tools/
3. Check logs: agentmd run agent -vv
Import errors in tool
Problem: Tool returns "ERROR: module 'X' not found"
Solution:
And add error handling in the tool.
Best Practices
1. Keep Tools Focused
Each tool should do one thing well:
# Good: separate focused tools
@tool
def to_uppercase(text: str) -> str:
"""Convert text to uppercase."""
return text.upper()
@tool
def count_chars(text: str) -> str:
"""Count characters in text."""
return str(len(text))
2. Return Strings
Tools should always return strings. For structured data, use JSON:
import json
@tool
def get_user_info(user_id: int) -> str:
"""Get user information."""
user = {"id": user_id, "name": "Alice", "email": "alice@example.com"}
return json.dumps(user, indent=2)
3. Handle Errors Gracefully
Always catch exceptions and return error messages:
@tool
def risky_operation(input: str) -> str:
"""Perform a risky operation."""
try:
result = do_something(input)
return f"Success: {result}"
except Exception as e:
return f"ERROR: {e}"
4. Use Environment Variables for Secrets
Never hardcode API keys:
import os
@tool
def call_api(endpoint: str) -> str:
"""Call external API."""
api_key = os.environ.get("API_KEY")
if not api_key:
return "ERROR: API_KEY environment variable not set"
# Use api_key in request
return "API response"
5. Document Clearly
Write clear docstrings that help the LLM understand when to use the tool:
@tool
def analyze_sentiment(text: str) -> str:
"""Analyze the sentiment of text (positive, negative, or neutral).
Use this tool to classify the emotional tone of reviews, comments, or feedback.
Args:
text: The text to analyze (sentences or paragraphs).
Returns:
Sentiment classification: "positive", "negative", or "neutral".
"""
# Implementation
6. Reuse Tools Across Agents
Create generic, reusable tools:
workspace/agents/tools/
├── text_utils.py # Used by many agents
├── weather_api.py # Weather agents
├── csv_parser.py # Data processing agents
└── markdown_converter.py