Skip to main content

Command Palette

Search for a command to run...

Building Tools for Google ADK Agents: Complete Tutorial

Published
9 min read
Building Tools for Google ADK Agents: Complete Tutorial
J

Senior AI Engineer | Building & Telling Stories about AI/ML Systems | Software Engineer

Have you ever watched your AI agent struggle — locked in endless loops of “I don’t know how to help” while users grow impatient and annoyed?

For many developers, building intelligent agents becomes an exercise in frustration, wasted hours, and groaning at error logs when rudimentary bots fail at even simple tasks.

Every moment lost is burning cash, chewing up developer time, and driving up support tickets as agents fall short.

The real problem isn’t just technical; it’s a war of efficiency, cost, and sheer exhaustion as bots hit their limits and resources disappear into debugging rabbit holes.

This guide exists to break that cycle.

By mastering how to build powerful, purposeful tools for Google’s Agent Development Kit (ADK), you unlock faster development, lower support costs, and bots that can actually solve real problems.

Read on to discover step-by-step patterns, code, and design advice that will transform your agents from frustrating conversational dead-ends into action-taking superpowers.

Introduction: Why ADK Tools Matter

Modern AI agents can do much more than answer questions.

Tools let them calculate, research, manage data, and perform actions on your behalf.

In Google ADK, every meaningful capability comes from well-designed tools that act as bridges between the model and external systems.

Whether you’re building a mortgage advisor, a customer support bot, or an automation workhorse, understanding tool development is critical.

This tutorial presents the full blueprint for building reliable, flexible, and robust ADK tools.

What Are Tools in Google ADK?

Google ADK defines tools as call-ready Python functions that agents leverage during conversations.
Each tool is a callable extension of the language model, offering access to calculations, APIs, files, and more.

The agent chooses when to trigger a tool, based on user input or context.

Tools open new dimensions — agents move from passive responders to action-taking assistants.

Common use cases include:

  • Data processing and calculations

  • External service/API access

  • Conversation state management

  • Database and filesystem operations

  • Control over agent behavior within sessions

Anatomy of a Well-Built Tool

Every ADK tool should adhere to clear standards for easy integration and reliability.
Key aspects include:

  • A function signature with typed parameters

  • An informative docstring describing arguments and usage

  • Structured return values with status reporting

  • Consistent error handling for exceptions and edge cases

Writing Your First Tool: The Simple Calculator

Start with the basics — a tool that adds two numbers.

This hands-on example sets the pattern for building robust ADK tools.

def add_numbers(a: float, b: float) -> dict:
    """Adds two numbers together.

    Use this tool for adding two numeric values.
    Args:
        a: The first number.
        b: The second number.
    Returns:
        dict: Status, result, and operation string.
    """
    try:
        result = a + b
        return {
            "status": "success",
            "result": result,
            "operation": f"{a} + {b} = {result}"
        }
    except Exception as e:
        return {
            "status": "error",
            "error_message": f"Failed to add numbers: {str(e)}"
        }

The tool validates inputs and returns a structured dictionary, including an easy-to-read operation summary.

This predictable format makes it simple for your agent to parse the result and respond to the user appropriately.

Advanced Calculations with Mortgage Payments

Increase complexity by adding business logic and input validation.

Here’s an expanded example: a mortgage calculator for financial applications.

def calculate_mortgage_payment(principal: float, annual_interest_rate: float, years: int) -> dict:
    """Calculates monthly mortgage payments.

    Args:
        principal: Initial loan amount.
        annual_interest_rate: Annual interest (percent).
        years: Loan term (years).
    Returns:
        dict: Status, payment breakdown, and error (if any).
    """
    try:
        monthly_rate = (annual_interest_rate / 100) / 12
        num_payments = years * 12

        if monthly_rate < 0 or principal <= 0 or num_payments <= 0:
            return {
                "status": "error",
                "error_message": "Inputs must be positive. Interest rate cannot be negative."
            }
        if monthly_rate == 0:
            monthly_payment = principal / num_payments
        else:
            monthly_payment = principal * (monthly_rate * (1 + monthly_rate) ** num_payments) / ((1 + monthly_rate) ** num_payments - 1)

        return {
            "status": "success",
            "monthly_payment": round(monthly_payment, 2),
            "total_payments": round(monthly_payment * num_payments, 2),
            "total_interest": round((monthly_payment * num_payments) - principal, 2),
            "principal": principal,
            "interest_rate": annual_interest_rate,
            "loan_term_years": years
        }
    except Exception as e:
        return {
            "status": "error",
            "error_message": f"Failed to calculate mortgage payment: {str(e)}"
        }

Short paragraphs in the tool’s docstring clarify intended usage and argument expectations, while error handling returns clear feedback for invalid scenarios.

Agent with Tools Example

After building tools, attach them to your agent using Google ADK’s APIs.

from google.adk.agents import Agent

mortgage_advisor = Agent(
    name="mortgage_advisor",
    model="gemini-2.5-flash",
    description="Calculates and explains mortgage payments.",
    instruction="Use calculate_mortgage_payment for payment queries, then explain the results.",
    tools=[calculate_mortgage_payment]
)

A concise setup gives your agent instant calculation power, ready for deployment.

State Management. Leveraging ToolContext for Advanced Features

The ToolContext parameter brings state management and more into your tool’s scope.
It offers:

  • Access to cross-session data (persistent state)

  • Control over agent behavior and conversation flow

  • Detailed user context information

A Customer Profile Manager

Tools can update or fetch structured user data, providing continuity across sessions.

from google.adk.tools.tool_context import ToolContext
import datetime

def update_customer_profile(field: str, value: str, tool_context: ToolContext) -> dict:
    """Updates a customer's profile information.

    Args:
        field: Profile field to update.
        value: New value.
        tool_context: Provided by ADK.
    Returns:
        dict: Success status and summary.
    """
    try:
        profile_key = "user:customer_profile"
        profile = tool_context.state.get(profile_key, {})
        if "update_history" not in profile:
            profile["update_history"] = []
        old_value = profile.get(field)
        profile[field] = value
        profile["update_history"].append({
            "field": field,
            "old_value": old_value,
            "new_value": value,
            "timestamp": datetime.datetime.now().isoformat()
        })
        tool_context.state[profile_key] = profile
        return {
            "status": "success",
            "message": f"Updated {field} to '{value}'",
            "profile_summary": {k: v for k, v in profile.items() if k != "update_history"}
        }
    except Exception as e:
        return {
            "status": "error",
            "error_message": f"Failed to update profile: {str(e)}"
        }

Another tool fetches customer info if needed:

def get_customer_profile(tool_context: ToolContext) -> dict:
    """Retrieves current customer profile.

    Args:
        tool_context: Provided by ADK.
    Returns:
        dict: Profile details or error.
    """
    try:
        profile_key = "user:customer_profile"
        profile = tool_context.state.get(profile_key, {})
        if not profile:
            return {
                "status": "success",
                "message": "No customer profile found.",
                "profile": {}
            }
        clean_profile = {k: v for k, v in profile.items() if k != "update_history"}
        return {
            "status": "success",
            "message": "Customer profile retrieved.",
            "profile": clean_profile,
            "last_updated": profile.get("update_history", [{}])[-1].get("timestamp", "Never")
        }
    except Exception as e:
        return {
            "status": "error",
            "error_message": f"Failed to retrieve profile: {str(e)}"
        }

Stateful patterns allow your agent to persist context and manage long-running tasks.

Controlling Agent Flow. Escalation and Transfer: Agent Handoffs

Sometimes your bot needs to escalate or hand off conversations to humans.
Tools can automate this, ensuring seamless transitions.

def escalate_to_support(issue_type: str, severity: int, description: str, tool_context: ToolContext) -> dict:
    """Escalates issues to human support.

    Args:
        issue_type: Issue category.
        severity: 1-5 scale.
        description: Details.
        tool_context: Provided by ADK.
    Returns:
        dict: Escalation status and next steps.
    """
    try:
        escalation_data = {
            "issue_type": issue_type,
            "severity": severity,
            "description": description,
            "timestamp": datetime.datetime.now().isoformat(),
            "escalation_id": f"ESC-{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
        }
        tool_context.state["current_escalation"] = escalation_data
        if severity >= 4:
            tool_context.actions.transfer_to_agent = "human_support_agent"
            return {
                "status": "success",
                "action": "immediate_transfer",
                "message": "High severity. Transferring to human support.",
                "escalation_id": escalation_data["escalation_id"],
                "estimated_wait_time": "2-5 minutes"
            }
        elif severity == 3:
            return {
                "status": "success",
                "action": "callback_scheduled",
                "message": f"{issue_type} issue logged. Callback in 2 hours.",
                "escalation_id": escalation_data["escalation_id"],
                "next_steps": ["Ticket created", "Callback scheduled", "Email confirmation"]
            }
        else:
            return {
                "status": "success",
                "action": "ticket_created",
                "message": f"{issue_type} issue logged. Response in 24 hours.",
                "escalation_id": escalation_data["escalation_id"],
                "next_steps": ["Ticket created", "Expect email in 24 hours"]
            }
    except Exception as e:
        return {
            "status": "error",
            "error_message": f"Escalation failed: {str(e)}"
        }

Check for escalation status anytime:

def check_escalation_status(tool_context: ToolContext) -> dict:
    """Checks current escalation status.
    Args:
        tool_context: Provided by ADK.
    Returns:
        dict: Escalation details.
    """
    try:
        escalation = tool_context.state.get("current_escalation")
        if not escalation:
            return {
                "status": "success",
                "message": "No active escalations.",
                "has_escalation": False
            }
        return {
            "status": "success",
            "has_escalation": True,
            "escalation": {
                "id": escalation["escalation_id"],
                "type": escalation["issue_type"],
                "severity": escalation["severity"],
                "created": escalation["timestamp"],
                "description": escalation["description"]
            },
            "message": f"Active escalation: {escalation['escalation_id']}"
        }
    except Exception as e:
        return {
            "status": "error",
            "error_message": f"Check escalation failed: {str(e)}"
        }

Building agent flow tools helps bridge gaps between automation and human support.

Using Built-In ADK Tools

Google Search Integration

Agents often need up-to-date external data.
ADK provides tools like google_search so agents can research live web content without leaving their workflow.

from google.adk.tools import google_search

def create_research_agent():
    return Agent(
        name="research_assistant",
        model="gemini-2.0-flash-exp",
        instruction="""
        Use google_search to find current information.
        Always cite sources and explain recency.
        """,
        tools=[google_search]
    )

This pattern lets agents respond with the latest information, crucial for data-driven applications.

Code Execution Tool

Programmatic agents must often run and validate code snippets.
ADK's code_interpreter tool enables on-the-fly code testing, analysis, and demonstration.

from google.adk.tools import code_interpreter

def create_coding_assistant():
    return Agent(
        name="coding_assistant",
        model="gemini-2.0-flash-exp",
        instruction="""Execute Python code with code_interpreter. Explain results before and after execution.""",
        tools=[code_interpreter]
    )

Run code, check results, analyze data — all inside the agent’s workflow.

Best Practices and Design Patterns

Consistent Return Structures

Uniformity matters for both agent parsing and reliability.

Success Pattern

{
    "status": "success",
    "data": {},  # Actual response data
    "message": "Human-readable message"
}

Error Pattern

{
    "status": "error",
    "error_message": "Clear error explanation",
    "error_code": "OPTIONAL_ERROR_CODE"
}

Input Validation

Never trust incoming data blindly.
Validate early; inform the agent clearly on rejection.

import re

def validate_email_tool(email: str) -> dict:
    """Validates email address format."""
    if not email or not isinstance(email, str):
        return {
            "status": "error",
            "error_message": "Email must be a non-empty string"
        }
    email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    if not re.match(email_pattern, email.strip()):
        return {
            "status": "error",
            "error_message": "Invalid email format"
        }
    return {
        "status": "success",
        "email": email.strip().lower(),
        "message": "Email format is valid"
    }

Testing for business rule compliance leads to lower error rates in production.

Logging and Debugging

Monitor for issues with clear logging.
Make debugging easy by capturing all relevant details.

import logging

def debug_tool_example(data: str, tool_context: ToolContext) -> dict:
    """Logging example."""
    logger = logging.getLogger(f"tool.{__name__}")
    try:
        logger.info(f"Processing data: {data[:50]}...")
        result = process_data(data)
        logger.info(f"Completed successfully, size: {len(str(result))}")
        return {
            "status": "success",
            "result": result
        }
    except Exception as e:
        logger.error(f"Tool failed: {str(e)}", exc_info=True)
        return {
            "status": "error",
            "error_message": f"Processing failed: {str(e)}"
        }

Short, targeted logs aid rapid identification of flaws and exceptions during development and production.

Conclusion

The power of ADK agents lies in the tools you build for them.

Every reliable, useful, and robust tool follows the practices outlined here:

  • Start with the simplest working unit; expand as needed

  • Treat errors as first-class citizens, reporting them cleanly to agents/users

  • Use ToolContext wherever persistent state or agent control is needed

  • Unit test every tool for correctness, edge cases, and robustness

  • Document functions with precise, comprehensive docstrings

  • Standardize return formats for predictability and interoperability

By mastering tool creation in ADK, you turn conversational agents into active digital teammates, capable of solving practical problems and running workflows at scale.

This foundation ensures your journey into ADK agent tool development yields apps that are powerful, maintainable, and deeply effective.

Now, unleash your imagination and build agents that truly act in the real world.

PS:

If you like this article, share it with others ♻️

Would help a lot ❤️

And feel free to follow me for more content like this.

More from this blog

Juan C Olamendy

122 posts

🤖 Talk about AI/ML · AI-preneur 🛠️ Build AI tools 🚀 Share my journey 𓀙 🔗 pixela.io · 🛍️ shoppingbot.ai