Custom Tools
AdaL comes with built-in tools (file read/write, bash, search, etc.), but you can extend it with your own project-specific tools. Custom tools are Python functions that the agent can call just like built-in ones — with full type-checked parameters, docstring-derived descriptions, and approval flow integration.
How It Works
Place a tools.py file in .adal/ at the root of your workspace:
my-project/
├── .adal/
│ └── tools.py ← your custom tools
├── src/
├── tests/
└── ...
The runtime auto-loads this file when it starts. No extra configuration needed.
Basic Example
from adalflow.core.types import ToolOutput
def create_jira_ticket(title: str, description: str, priority: str = "Medium") -> ToolOutput:
"""Create a Jira ticket in the current sprint.
Args:
title: Short summary of the ticket.
description: Detailed description of the work.
priority: Priority level — Low, Medium, High, or Critical.
"""
# Your integration logic here
import requests
resp = requests.post(
"https://your-org.atlassian.net/rest/api/3/issue",
json={"fields": {"summary": title, "description": description, "priority": {"name": priority}}},
auth=("email", "JIRA_API_TOKEN"),
)
ticket_key = resp.json().get("key", "UNKNOWN")
return ToolOutput(
output={"key": ticket_key, "url": f"https://your-org.atlassian.net/browse/{ticket_key}"},
observation=f"Created ticket {ticket_key}: {title}",
display=f"🎫 {ticket_key}",
status="success",
)
# This list is what the runtime reads — every tool you want exposed must be here.
CUSTOM_TOOLS = [create_jira_ticket]
The CUSTOM_TOOLS List
The runtime looks for a module-level CUSTOM_TOOLS variable. It must be a list or tuple containing:
Simple form — just functions
def deploy_staging(service: str) -> ToolOutput:
"""Deploy a service to the staging environment."""
...
def run_migration(name: str, dry_run: bool = True) -> ToolOutput:
"""Run a database migration."""
...
CUSTOM_TOOLS = [deploy_staging, run_migration]
The tool name comes from the function name, and the description from the docstring.
Dict form — override name, description, or approval
CUSTOM_TOOLS = [
{
"function": deploy_staging,
"name": "deploy", # override the tool name
"description": "Ship to staging", # override the description
"require_approval": True, # default is True
},
{
"function": run_migration,
"require_approval": False, # skip approval (use with caution!)
},
]
Return Values
Custom tools should return a ToolOutput:
from adalflow.core.types import ToolOutput
def my_tool() -> ToolOutput:
return ToolOutput(
output={"result": "data"}, # structured data (stored internally)
observation="Human-readable result", # what the agent sees
display="Short UI display text", # what the user sees in the tool card
status="success", # "success" or "error"
)
If your function returns a plain value (string, dict, etc.) instead of ToolOutput, the runtime wraps it automatically — but explicit ToolOutput gives you control over what the agent vs. user sees.
Tool Approval
By default, custom tools require approval (require_approval=True). When the agent wants to call your tool, it will ask for confirmation before executing — just like built-in tools in default permission mode.
To auto-approve all tools (including custom ones), use --yolo:
adal -q "Deploy the auth service to staging" --yolo
To skip approval for a specific tool (it runs immediately when the agent calls it):
CUSTOM_TOOLS = [
{"function": safe_read_only_tool, "require_approval": False},
dangerous_write_tool, # keeps default require_approval=True
]
Or use --yolo to auto-approve everything (including custom tools).
Async Tools
Custom tools can be async:
import httpx
from adalflow.core.types import ToolOutput
async def check_service_health(service_url: str) -> ToolOutput:
"""Check if a service is healthy by hitting its /health endpoint."""
async with httpx.AsyncClient() as client:
resp = await client.get(f"{service_url}/health", timeout=5)
healthy = resp.status_code == 200
return ToolOutput(
output={"healthy": healthy, "status_code": resp.status_code},
observation=f"{'✅ Healthy' if healthy else '❌ Unhealthy'} — {service_url} returned {resp.status_code}",
display=f"{'✅' if healthy else '❌'} {service_url}",
status="success",
)
CUSTOM_TOOLS = [check_service_health]
Error Handling
If your tool raises an exception, the runtime catches it and converts it to an error ToolOutput — the agent sees the error message and can decide how to proceed. Your tool won't crash the session:
def risky_operation(target: str) -> ToolOutput:
"""Do something that might fail."""
if not valid(target):
# Option 1: Return an error ToolOutput (preferred — gives you control)
return ToolOutput(
output=None,
observation=f"Invalid target: {target}",
display="❌ Invalid target",
status="error",
)
# Option 2: Raise an exception (runtime wraps it as error ToolOutput)
raise ValueError(f"Something went wrong with {target}")
Usage
Just run AdaL in a directory that contains .adal/tools.py — the agent discovers your tools automatically:
cd /path/to/my-project # contains .adal/tools.py
adal
Or in headless mode:
adal -q "Create a Jira ticket for the login page bug we discussed."
The agent can now call create_jira_ticket, deploy_staging, etc. alongside built-in tools.
Controlling Built-in Tools
Custom tools from .adal/tools.py are always available to the agent. To disable built-in tools so the agent cannot see or call them, use --disabled-default-tools (CLI) or disabled_default_tools (SDK):
# Agent cannot use bash, edits, or web
adal --disabled-default-tools "Bash,Edit,Web"
options = AdalAgentOptions(disabled_default_tools=["Bash", "Edit", "Web"])
Disabled tools are invisible and unexecutable — the agent cannot see or call them.
For the full list of tool groups, permission modes, and recipes, see the Built-in Tools guide.
Naming Rules
Tool names must be:
- Valid Python identifiers (
create_ticket✓,create-ticket✗) - Not Python keywords (
return✗,class✗) - Unique within the project (no duplicates in
CUSTOM_TOOLS) - Not conflicting with built-in tool names (e.g., don't name a tool
bashorread_file)
Tips
- Keep tools focused — one tool = one action. The agent composes multiple tools itself.
- Write good docstrings — the agent reads your docstring to decide when and how to call the tool. Be specific about parameters and behavior.
- Use type hints — parameter types are extracted for the tool schema.
str,int,bool,float, andOptional[...]work well. - Test independently — your custom tools are regular Python functions. Test them with pytest before exposing to the agent.
- Sensitive operations — keep
require_approval=True(the default) for anything that modifies external systems. - Dependencies —
.adal/tools.pyruns in the same Python environment as the AdaL runtime. If your tools need packages, ensure they're installed in that environment.