Give your AI agent superpowers with custom tools

Series Navigation: This is Post 3 of the Google ADK Tutorial Series. ← Previous: LiteLLM Integration: Making Your ADK Agents Model-Agnostic


What You’ll Learn

By the end of this post, you’ll understand the three types of tools in ADK:

  1. Built-in Tools - Pre-built tools like Google Search that provide instant capabilities
  2. Custom Function Tools - Your own Python functions that the agent can call
  3. Agent as a Tool - Using other agents as callable tools for complex workflows

We’ll build a travel assistant agent that demonstrates all three tool types working together.

Code on GitHub: The complete working example from this post is available at google-adk-samples/travel_agent


Why Tools Matter

An LLM without tools is like a travel agent who can only talk about destinations but can’t book anything. Tools transform your agent from a chatbot into an assistant that can:

  • Search the web for real-time information about destinations, weather, and events
  • Call APIs to check flight status or exchange rates
  • Execute business logic to search hotels and create bookings
  • Delegate to specialists by calling other agents with specific expertise

In ADK, tools are just Python functions that the agent can call when needed. The LLM decides when to call a tool based on the user’s request and your tool’s description.


How ADK Tools Work

When you define a tool, ADK automatically:

  1. Extracts the function signature - parameter names and types
  2. Parses the docstring - tool description and parameter descriptions
  3. Generates a schema - tells the LLM what the tool does and how to call it
  4. Handles execution - calls your function when the LLM requests it
def search_hotels(city: str, check_in: str, check_out: str, guests: int = 2) -> dict:
    """Search for available hotels in a city.

    Args:
        city: Destination city (e.g., "Paris", "Tokyo")
        check_in: Check-in date in YYYY-MM-DD format
        check_out: Check-out date in YYYY-MM-DD format
        guests: Number of guests (default: 2)

    Returns:
        List of available hotels with prices and ratings
    """
    # Your implementation here
    pass

From this function, ADK generates a tool schema that tells the LLM:

  • Name: search_hotels
  • Description: “Search for available hotels in a city”
  • Parameters: city (required string), check_in (required string), check_out (required string), guests (optional integer, default 2)

Setting Up

# Create project directory
mkdir agents
cd agents

# Set up virtual environment
python -m venv .venv
source .venv/bin/activate

# Install dependencies
pip install google-adk python-dotenv

# Create agent folder
mkdir travel_agent
cd travel_agent

Create a .env file:

GOOGLE_API_KEY=your-google-api-key

ADK provides built-in tools that give your agent instant capabilities without writing any code. The most powerful one is Google Search, which allows your agent to search the web for real-time information.

from google.adk.agents import Agent
from google.adk.tools import google_search

search_agent = Agent(
    model='gemini-2.0-flash',
    name='search_agent',
    description='Search for real-time travel information',
    instruction="""Use Google Search to answer questions about:
    - Current weather conditions at destinations
    - Local events and festivals
    - Travel advisories and requirements
    - Restaurant and attraction recommendations""",
    tools=[google_search],
)

Important Limitation

Google Search has a critical constraint: it can only be used alone in an agent. You cannot combine google_search with other tools in the same agent’s tool list.

# This will NOT work - google_search cannot be combined with other tools
agent = Agent(
    model='gemini-2.0-flash',
    name='broken_agent',
    tools=[google_search, my_custom_tool],  # Error!
)

We’ll solve this limitation later using the Agent as a Tool pattern.

Model Requirement

Google Search requires Gemini 2.0 or later models. It won’t work with older model versions.


Custom Function Tools

Custom function tools are just Python functions that your agent can call. They’re the workhorses of most ADK applications - handling business logic, API calls, database queries, and more.

Tool 1: Get Flight Status

A tool to check the current status of a flight:

# Sample flight data (in production, this would call an airline API)
FLIGHTS = {
    "AA123": {
        "airline": "American Airlines",
        "origin": "JFK",
        "destination": "LAX",
        "departure": "2026-03-15 08:00",
        "arrival": "2026-03-15 11:30",
        "status": "On Time",
        "gate": "B22"
    },
    "UA456": {
        "airline": "United Airlines",
        "origin": "SFO",
        "destination": "ORD",
        "departure": "2026-03-15 14:00",
        "arrival": "2026-03-15 20:15",
        "status": "Delayed",
        "delay_minutes": 45,
        "gate": "C17"
    },
    "DL789": {
        "airline": "Delta Airlines",
        "origin": "ATL",
        "destination": "MIA",
        "departure": "2026-03-15 10:30",
        "arrival": "2026-03-15 12:45",
        "status": "Boarding",
        "gate": "A5"
    },
}


def get_flight_status(flight_number: str) -> dict:
    """Get the current status of a flight.

    Use this tool when a user asks about their flight status, departure time,
    arrival time, gate information, or delays.

    Args:
        flight_number: The flight number (e.g., "AA123", "UA456")

    Returns:
        Flight details including status, times, and gate information
    """
    flight_number = flight_number.upper().strip()

    if flight_number not in FLIGHTS:
        return {
            "found": False,
            "error": f"Flight '{flight_number}' not found. Please check the flight number.",
            "hint": "Flight numbers typically look like: AA123, UA456, DL789"
        }

    flight = FLIGHTS[flight_number]
    result = {
        "found": True,
        "flight_number": flight_number,
        "airline": flight["airline"],
        "route": f"{flight['origin']}{flight['destination']}",
        "departure": flight["departure"],
        "arrival": flight["arrival"],
        "status": flight["status"],
        "gate": flight["gate"]
    }

    if "delay_minutes" in flight:
        result["delay"] = f"{flight['delay_minutes']} minutes"

    return result

Tool 2: Search Hotels

A tool to search for available hotels in a destination:

# Sample hotel data
HOTELS = {
    "paris": [
        {"id": "H001", "name": "Hotel Le Marais", "stars": 4, "price_per_night": 180, "available": True, "amenities": ["WiFi", "Breakfast", "Pool"]},
        {"id": "H002", "name": "Boutique Saint-Germain", "stars": 5, "price_per_night": 320, "available": True, "amenities": ["WiFi", "Spa", "Restaurant", "Gym"]},
        {"id": "H003", "name": "Paris Budget Inn", "stars": 2, "price_per_night": 75, "available": True, "amenities": ["WiFi"]},
    ],
    "tokyo": [
        {"id": "H004", "name": "Shinjuku Grand Hotel", "stars": 4, "price_per_night": 150, "available": True, "amenities": ["WiFi", "Restaurant", "Onsen"]},
        {"id": "H005", "name": "Shibuya Capsule Hotel", "stars": 2, "price_per_night": 45, "available": True, "amenities": ["WiFi", "Locker"]},
        {"id": "H006", "name": "Imperial Tokyo", "stars": 5, "price_per_night": 450, "available": False, "amenities": ["WiFi", "Spa", "Pool", "Restaurant", "Gym"]},
    ],
    "london": [
        {"id": "H007", "name": "The Westminster", "stars": 4, "price_per_night": 200, "available": True, "amenities": ["WiFi", "Breakfast", "Bar"]},
        {"id": "H008", "name": "Covent Garden Suites", "stars": 5, "price_per_night": 380, "available": True, "amenities": ["WiFi", "Spa", "Restaurant", "Theater tickets"]},
    ],
}


def search_hotels(city: str, check_in: str, check_out: str, guests: int = 2) -> dict:
    """Search for available hotels in a city.

    Use this tool when a user wants to find hotels, accommodation, or places to stay
    in a specific destination.

    Args:
        city: Destination city (e.g., "Paris", "Tokyo", "London")
        check_in: Check-in date in YYYY-MM-DD format
        check_out: Check-out date in YYYY-MM-DD format
        guests: Number of guests (default: 2)

    Returns:
        List of available hotels with prices, ratings, and amenities
    """
    city_lower = city.lower().strip()

    if city_lower not in HOTELS:
        return {
            "found": 0,
            "error": f"No hotels found in '{city}'. We currently support: Paris, Tokyo, London",
            "supported_cities": list(HOTELS.keys())
        }

    # Filter for available hotels only
    available_hotels = [h for h in HOTELS[city_lower] if h["available"]]

    if not available_hotels:
        return {
            "found": 0,
            "message": f"Sorry, no hotels available in {city} for those dates.",
            "suggestion": "Try different dates or check nearby cities."
        }

    # Calculate total price for the stay
    from datetime import datetime
    try:
        check_in_date = datetime.strptime(check_in, "%Y-%m-%d")
        check_out_date = datetime.strptime(check_out, "%Y-%m-%d")
        nights = (check_out_date - check_in_date).days
        if nights <= 0:
            nights = 1
    except ValueError:
        nights = 1  # Default if date parsing fails

    results = []
    for hotel in available_hotels:
        results.append({
            "id": hotel["id"],
            "name": hotel["name"],
            "stars": "⭐" * hotel["stars"],
            "price_per_night": f"${hotel['price_per_night']}",
            "total_price": f"${hotel['price_per_night'] * nights}",
            "amenities": hotel["amenities"]
        })

    return {
        "found": len(results),
        "city": city,
        "dates": f"{check_in} to {check_out}",
        "nights": nights,
        "guests": guests,
        "hotels": results
    }

Agent as a Tool

The Agent as a Tool pattern is one of ADK’s most powerful features. It lets you wrap an entire agent as a callable tool, allowing a parent agent to delegate tasks to specialist sub-agents.

This pattern solves the Google Search limitation we discussed earlier: since Google Search can’t be combined with other tools in the same agent, we can wrap a search-only agent and use that agent as a tool.

Creating an Agent Tool

from google.adk.agents import Agent
from google.adk.tools import google_search
from google.adk.tools.agent_tool import AgentTool

# Create a search specialist agent (uses only google_search)
search_agent = Agent(
    name='web_search',
    model='gemini-2.0-flash',
    description='Search the web for real-time travel information',
    instruction="""You are a travel research specialist. Use Google Search to find:
    - Current weather conditions and forecasts
    - Local events, festivals, and holidays
    - Travel advisories and entry requirements
    - Popular attractions and restaurants
    - Transportation options and tips

    Provide concise, relevant information from your searches.""",
    tools=[google_search],
)

# Wrap the search agent as a tool
search_tool = AgentTool(agent=search_agent)

Why Use Agent as a Tool?

Agent as a Tool can be thought of as a specific skill provided to the agent that it can invoke when needed. It is similar in concept to skills in Claude Code.

  1. Specialization - Create expert sub-agents for specific domains
  2. Reusability - The same agent can be used as a tool by multiple parent agents
  3. Cleaner architecture - Break complex agents into focused, manageable pieces
  4. Dynamic delegation - The parent agent decides which specialist to call based on the query
  5. Overcomes tool limitations - Bypass restrictions like Google Search’s single-tool requirement

Complete Example: Travel Assistant Agent

Now let’s combine everything into a working travel assistant that uses all three tool types.

Create agent.py:

"""
Travel Assistant Agent
An agent that helps users plan travel with flights, hotels, currency, and web search.
"""

from dotenv import load_dotenv
from datetime import datetime
from google.adk.agents import Agent
from google.adk.tools import google_search
from google.adk.tools.agent_tool import AgentTool
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types


# ============ Sample Data ============

FLIGHTS = {
    "AA123": {"airline": "American Airlines", "origin": "JFK", "destination": "LAX", "departure": "2026-03-15 08:00", "arrival": "2026-03-15 11:30", "status": "On Time", "gate": "B22"},
    "UA456": {"airline": "United Airlines", "origin": "SFO", "destination": "ORD", "departure": "2026-03-15 14:00", "arrival": "2026-03-15 20:15", "status": "Delayed", "delay_minutes": 45, "gate": "C17"},
    "DL789": {"airline": "Delta Airlines", "origin": "ATL", "destination": "MIA", "departure": "2026-03-15 10:30", "arrival": "2026-03-15 12:45", "status": "Boarding", "gate": "A5"},
}

HOTELS = {
    "paris": [
        {"id": "H001", "name": "Hotel Le Marais", "stars": 4, "price_per_night": 180, "available": True, "amenities": ["WiFi", "Breakfast", "Pool"]},
        {"id": "H002", "name": "Boutique Saint-Germain", "stars": 5, "price_per_night": 320, "available": True, "amenities": ["WiFi", "Spa", "Restaurant", "Gym"]},
        {"id": "H003", "name": "Paris Budget Inn", "stars": 2, "price_per_night": 75, "available": True, "amenities": ["WiFi"]},
    ],
    "tokyo": [
        {"id": "H004", "name": "Shinjuku Grand Hotel", "stars": 4, "price_per_night": 150, "available": True, "amenities": ["WiFi", "Restaurant", "Onsen"]},
        {"id": "H005", "name": "Shibuya Capsule Hotel", "stars": 2, "price_per_night": 45, "available": True, "amenities": ["WiFi", "Locker"]},
        {"id": "H006", "name": "Imperial Tokyo", "stars": 5, "price_per_night": 450, "available": False, "amenities": ["WiFi", "Spa", "Pool", "Restaurant", "Gym"]},
    ],
    "london": [
        {"id": "H007", "name": "The Westminster", "stars": 4, "price_per_night": 200, "available": True, "amenities": ["WiFi", "Breakfast", "Bar"]},
        {"id": "H008", "name": "Covent Garden Suites", "stars": 5, "price_per_night": 380, "available": True, "amenities": ["WiFi", "Spa", "Restaurant", "Theater tickets"]},
    ],
}


# ============ Custom Function Tools ============

def get_flight_status(flight_number: str) -> dict:
    """Get the current status of a flight.

    Use this tool when a user asks about their flight status, departure time,
    arrival time, gate information, or delays.

    Args:
        flight_number: The flight number (e.g., "AA123", "UA456")

    Returns:
        Flight details including status, times, and gate information
    """
    flight_number = flight_number.upper().strip()

    if flight_number not in FLIGHTS:
        return {
            "found": False,
            "error": f"Flight '{flight_number}' not found.",
            "hint": "Try: AA123, UA456, or DL789"
        }

    flight = FLIGHTS[flight_number]
    result = {
        "found": True,
        "flight_number": flight_number,
        "airline": flight["airline"],
        "route": f"{flight['origin']}{flight['destination']}",
        "departure": flight["departure"],
        "arrival": flight["arrival"],
        "status": flight["status"],
        "gate": flight["gate"]
    }

    if "delay_minutes" in flight:
        result["delay"] = f"{flight['delay_minutes']} minutes"

    return result


def search_hotels(city: str, check_in: str, check_out: str, guests: int = 2) -> dict:
    """Search for available hotels in a city.

    Use this tool when a user wants to find hotels, accommodation, or places to stay.

    Args:
        city: Destination city (e.g., "Paris", "Tokyo", "London")
        check_in: Check-in date in YYYY-MM-DD format
        check_out: Check-out date in YYYY-MM-DD format
        guests: Number of guests (default: 2)

    Returns:
        List of available hotels with prices and amenities
    """
    city_lower = city.lower().strip()

    if city_lower not in HOTELS:
        return {
            "found": 0,
            "error": f"No hotels in '{city}'. Available: Paris, Tokyo, London"
        }

    available = [h for h in HOTELS[city_lower] if h["available"]]

    if not available:
        return {"found": 0, "message": f"No availability in {city} for those dates."}

    try:
        d1 = datetime.strptime(check_in, "%Y-%m-%d")
        d2 = datetime.strptime(check_out, "%Y-%m-%d")
        nights = max((d2 - d1).days, 1)
    except ValueError:
        nights = 1

    results = []
    for hotel in available:
        results.append({
            "id": hotel["id"],
            "name": hotel["name"],
            "stars": "⭐" * hotel["stars"],
            "price_per_night": f"${hotel['price_per_night']}",
            "total": f"${hotel['price_per_night'] * nights}",
            "amenities": hotel["amenities"]
        })

    return {"found": len(results), "city": city, "nights": nights, "hotels": results}


# ============ Search Agent (Built-in Tool) ============

search_agent = Agent(
    name='web_search',
    model='gemini-3-flash-preview',
    description='Search the web for real-time travel information',
    instruction="""You are a travel research specialist. Search for:
    - Weather conditions and forecasts
    - Local events and festivals
    - Travel advisories and visa requirements
    - Popular attractions and restaurants

    Provide concise, helpful information.""",
    tools=[google_search],
)


# ============ Main Travel Assistant (Agent as Tool + Custom Tools) ============

root_agent = Agent(
    name="travel_assistant",
    model="gemini-3-flash-preview",
    instruction="""You are a helpful travel assistant that helps users plan trips.

You can help with:
1. **Flight Status** - Check if flights are on time, delayed, or boarding
2. **Hotel Search** - Find available hotels in Paris, Tokyo, or London
3. **Travel Research** - Search the web for weather, events, attractions, and travel tips

Guidelines:
- Always use tools to look up information - don't make up data
- For flight status, ask for the flight number if not provided
- When showing hotels, highlight price, rating, and key amenities
- Use web search for current weather, events, or destination tips

Sample flight numbers for testing: AA123, UA456, DL789
Sample cities with hotels: Paris, Tokyo, London""",
    tools=[
        get_flight_status,
        search_hotels,
        AgentTool(agent=search_agent),  # Agent as a tool!
    ]
)


async def main():
    """Run the travel assistant with ADK Runner."""

    # Load environment variables from .env file for Google API key
    load_dotenv()

    session_service = InMemorySessionService()

    runner = Runner(
        agent=root_agent,
        app_name="travel_assistant",
        session_service=session_service,
    )

    print("Travel Assistant Ready!")
    print("Try these prompts:")
    print("- What's the status of flight AA123?")
    print("- Find hotels in Paris for March 15-20, 2026")
    print("- What's the weather like in Tokyo?")

    user_id = "demo_user"
    session_id = "demo_session"

    # Create session before running - run_async requires existing session
    await session_service.create_session(
        app_name="travel_assistant",
        user_id=user_id,
        session_id=session_id
    )

    while True:
        user_input = input("\nYou: ").strip()
        if not user_input:
            continue
        if user_input.lower() in ['quit', 'exit', 'q']:
            break

        content = types.Content(
            role='user',
            parts=[types.Part.from_text(text=user_input)]
        )

        print("\nAssistant: ", end="", flush=True)

        try:
            async for event in runner.run_async(
                session_id=session_id,
                user_id=user_id,
                new_message=content
            ):
                if hasattr(event, 'content') and event.content:
                    if hasattr(event.content, 'parts'):
                        for part in event.content.parts:
                            if hasattr(part, 'text') and part.text:
                                print(part.text, end="", flush=True)
        except Exception as e:
            print("Error in run_async", {"error": str(e)})
            raise e
        print()


if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

Running the Agent

You can run this agent in three different ways:

Option 1: Using the ADK Web UI

# From the agents/ directory (parent of travel_agent/)
adk web

ADK Web Sample Output

Option 2: Running the script directly

python agent.py

Python Script Execution Sample Output

Option 3: Using the ADK Run CLI command

# From the agents/ directory (parent of travel_agent/)
# Agent name `travel_agent` below should match the the directory name
adk run travel_agent 

ADK Run Sample Output

Test Prompts to Try

What's the status of flight AA123?
Is flight UA456 delayed?
Find hotels in Paris for 2 guests, March 15-20, 2026
Show me hotels in Tokyo
What's the weather like in Paris next week?
Are there any festivals in Tokyo in March?

Best Practices

1. Write Clear Docstrings

The docstring is how the LLM understands your tool. Be specific about when to use it:

# Vague - LLM doesn't know when to use it
def process(data: str) -> str:
    """Process the data."""
    pass


# Clear - LLM knows exactly when to use it
def get_flight_status(flight_number: str) -> dict:
    """Get the current status of a flight.

    Use this tool when a user asks about their flight status, departure time,
    arrival time, gate information, or delays.

    Args:
        flight_number: The flight number (e.g., "AA123", "UA456")

    Returns:
        Flight details including status, times, and gate information
    """

2. Use Type Hints

Type hints help ADK generate accurate schemas:

def search_hotels(
    city: str,              # String
    check_in: str,          # String (date in YYYY-MM-DD)
    check_out: str,         # String (date in YYYY-MM-DD)
    guests: int = 2,        # Integer with default
    amenities: list[str] = None,  # Optional list of strings
) -> dict:
    pass

3. Return Errors as Data, Not Exceptions

# Bad - exceptions disrupt the agent
def get_flight(flight_number: str) -> dict:
    if flight_number not in FLIGHTS:
        raise ValueError("Flight not found")  # Don't do this

# Good - return error information the LLM can use
def get_flight(flight_number: str) -> dict:
    if flight_number not in FLIGHTS:
        return {
            "found": False,
            "error": "Flight not found",
            "hint": "Try flight numbers like AA123, UA456"
        }
    return {"found": True, "data": FLIGHTS[flight_number]}

4. Keep Tools Focused

Each tool should do one thing well:

# Bad - tool does too many things
def manage_booking(action: str, ...) -> dict:
    if action == "search": ...
    elif action == "book": ...
    elif action == "cancel": ...

# Good - separate focused tools
def search_hotels(...) -> dict: ...
def create_booking(...) -> dict: ...
def cancel_booking(...) -> dict: ...

Troubleshooting

“Tool is not being called”

Problem: The LLM ignores your tool even when it should use it.

Solution: Improve your docstring with explicit “Use this tool when…” guidance:

def search_hotels(city: str, ...) -> dict:
    """Search for available hotels in a city.

    Use this tool when:
    - User asks about hotels or accommodation
    - User wants to find places to stay
    - User is planning a trip and needs lodging
    - User asks "where can I stay in [city]?"

    Args:
        city: Destination city name
    ...
    """

“Tool returns wrong type”

Problem: The LLM passes wrong parameter types.

Solution: Validate inputs and return helpful errors:

def get_flight_status(flight_number: str) -> dict:
    if not isinstance(flight_number, str):
        return {"error": f"Expected string, got {type(flight_number).__name__}"}

    flight_number = flight_number.upper().strip()
    # ... rest of implementation

“Google Search tool error”

Problem: Error when using google_search.

Solutions:

  1. Ensure you’re using gemini-2.0-flash or later
  2. Don’t combine google_search with other tools - use the Agent as Tool pattern instead
  3. Check that your API key has access to Gemini 2.0 features

Summary

In this post, we covered the three types of tools in ADK:

Tool TypeUse CaseExample
Built-in ToolsPre-built capabilitiesgoogle_search for web search
Custom FunctionsYour business logicget_flight_status(), search_hotels()
Agent as ToolDelegate to specialistsWrap search agent to use alongside custom tools

The Agent as Tool pattern is particularly powerful - it lets you overcome tool limitations and build modular, specialized agent architectures.


Resources


Series Navigation ← Previous: LiteLLM Integration: Making Your ADK Agents Model-Agnostic → Next: Building Multi-Agent Systems with ADK (coming soon)