Skip to main content
>_ supraj.dev
Building AI Agents with Google ADK: The Complete Production Guide
// PUBLISHED ON JUNE 11, 2026 UPDATED JUNE 11, 2026 5 min read

Building AI Agents with Google ADK: The Complete Production Guide

A deep, hands-on guide to Google's Agent Development Kit (ADK 2.0). Real Python code for tools, sequential/parallel/loop pipelines, sub-agent delegation, callbacks, evaluation, and Cloud Run deployment — plus an honest comparison with LangGraph, CrewAI, and AutoGen.

#ai #mlops #gcp #llm

Building AI Agents with Google ADK

Building AI Agents with Google ADK: The Complete Production Guide

TL;DR: Google’s Agent Development Kit (ADK) is an open-source, code-first framework for building AI agents that feel like real software instead of prompt spaghetti. You define agents as Python objects, hand them plain functions as tools, compose them into deterministic multi-agent pipelines (sequential, parallel, loop), guard them with callbacks, test them with built-in evaluation, and ship to Cloud Run with one command. This guide is the full walkthrough with copy-paste-ready code for every piece.


What is Google ADK?

Agent Development Kit (ADK) is an open-source framework from Google for building, evaluating, and deploying AI agents. Its founding principle is simple: agent development should feel like software development — modular, testable, version-controlled — not a pile of glue code wrapped around a single LLM call.

ADK was released in April 2025 and reached its 2.0 GA milestone with graph workflows and collaborative agents. It is available in Python, TypeScript, Go, and Java. This guide uses the Python SDK, which requires Python 3.11+.

Three terms you’ll see throughout:

  • Agent — the unit that reasons and acts. The default is the LlmAgent (an LLM-backed brain).
  • Tool — anything an agent can do: a Python function, a built-in like Google Search, an OpenAPI spec, or an MCP server.
  • Workflow agent — a deterministic orchestrator (SequentialAgent, ParallelAgent, LoopAgent) that controls how multiple agents run, without an LLM deciding the flow.

While ADK is optimized for Gemini and Google Cloud, it is model-agnostic (run GPT, Claude, or a local Llama/Mistral behind the same abstraction) and deployment-agnostic (Cloud Run, GKE, Vertex AI Agent Engine, or anywhere Python runs).


Why Does It Matter?

A bare LLM is a brilliant assistant locked in a room with no phone. It can reason, but it can’t check today’s weather, query your database, or call your internal API. The moment you need a multi-step, repeatable process that runs hands-off, you’ve crossed from “chatbot” into “agent” territory — and that’s where you need a framework.

ADK gives you the plumbing you’d otherwise hand-roll: tool calling, session and state management, multi-agent orchestration, retries, parallel execution, callbacks for guardrails, and built-in evaluation. The differentiator versus its competitors is that you write agent logic once and inherit managed infrastructure, authentication, and Cloud Trace observability the moment you deploy — without changing a line of agent code.

One more strategic point: ADK natively speaks the A2A (Agent-to-Agent) protocol, now a Linux Foundation project backed by 150+ organizations. An ADK agent can discover and invoke an agent built in LangGraph or CrewAI through a standardized task interface. You’re not locked into one framework’s island.


How It Works

ADK uses a hierarchical agent tree: a root agent delegates to sub-agents, which can have their own sub-agents. Two things drive execution at runtime — the Runner (which drives the agent loop) and the SessionService (which holds conversation state and history).

    ┌────────────────────────────────────────────────────────┐
    │                      ADK Runtime                        │
    │                                                         │
    │   User ──► Runner ──► root_agent (LlmAgent)             │
    │             │              │                            │
    │      SessionService    reason + decide                  │
    │     (state, history)       │                            │
    │             ▲      ┌───────┼────────┐                   │
    │             │      ▼       ▼        ▼                   │
    │       output_key  Tool   Sub-agent  Built-in tool       │
    │             │   (Python   (transfer_  (google_search)   │
    │             └──── fn)      to_agent)                     │
    └────────────────────────────────────────────────────────┘

The agent has three ways to act: call a function tool (your Python code), delegate to a sub-agent (ADK emits a typed transfer_to_agent call), or invoke a built-in tool. Each agent can write its result into shared session state via an output_key; downstream agents read it. That shared state is how data flows through a pipeline.

ADK ships three deterministic workflow primitives:

PrimitiveBehavior
SequentialAgentRuns sub-agents in strict order, passing state forward.
ParallelAgentRuns sub-agents concurrently, then merges results.
LoopAgentRe-invokes sub-agents until a stop condition holds.

Hands-On: Step-by-Step

1. Install ADK and scaffold a project

ADK ships as the google-adk package. Always use a virtual environment.

python -m venv .venv
source .venv/bin/activate
pip install google-adk

Scaffold a new agent with the CLI:

adk create my_agent

This generates the minimal structure ADK expects:

my_agent/
├── agent.py      # main agent code — must define root_agent
├── __init__.py   # marks the dir as a Python package
└── .env          # API keys / project IDs

The __init__.py is not optional: ADK loads the agent folder as a Python module, so discovery and relative imports depend on it.

2. Configure credentials

If you use the Gemini API directly, create a key in Google AI Studio and write it into .env:

GOOGLE_GENAI_USE_VERTEXAI=FALSE
GOOGLE_API_KEY=your_api_key_here

If you’re on Vertex AI instead, point ADK at your project:

GOOGLE_GENAI_USE_VERTEXAI=TRUE
GOOGLE_CLOUD_PROJECT=your-project-id
GOOGLE_CLOUD_LOCATION=us-central1

3. Build a single agent with one tool

A function tool in ADK is just a Python function with a docstring and type hints. This is the most important detail to internalize: ADK feeds the docstring and type hints to the model so it knows when and how to call the tool. A vague docstring means the model never calls it, or calls it wrong. Treat the docstring as the tool’s API contract.

# my_agent/agent.py
from google.adk.agents import Agent


def get_weather(city: str) -> dict:
    """Returns the current weather for a given city.

    Args:
        city: The name of the city, e.g. "Pune".

    Returns:
        A dict with a status field and a human-readable report.
    """
    # Replace with a real API call in production.
    data = {
        "pune": "32C, clear skies",
        "hyderabad": "29C, light rain",
    }
    report = data.get(city.lower())
    if report:
        return {"status": "success", "city": city, "report": report}
    return {"status": "error", "message": f"No data for {city}."}


root_agent = Agent(
    model="gemini-2.5-flash",
    name="weather_agent",
    description="Answers questions about the weather in a city.",
    instruction=(
        "You are a helpful weather assistant. When a user asks about the "
        "weather, call the get_weather tool. If the tool returns an error, "
        "apologize and ask the user to try another city."
    ),
    tools=[get_weather],
)

That is a complete, runnable agent. You pass the function directly into tools — ADK wraps it as a function tool automatically. Return a dict with a status field; the model uses that to decide its next move.

4. Run it locally

ADK gives you two ways to test before you deploy anything. The terminal REPL:

adk run my_agent

Or the web UI, which renders the full trace of reasoning and tool calls:

adk web

Open the printed localhost URL, pick my_agent, and ask “What’s the weather in Pune?” You’ll watch the model decide to call get_weather, see the tool return, and read the final answer. The trace view is the single fastest way to debug why an agent did or did not call a tool — use it constantly.

ADK ships built-in tools that are first-class citizens, not user code. Google Search gives the agent live web access in one import:

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

root_agent = Agent(
    model="gemini-2.5-flash",
    name="research_agent",
    description="Answers questions using live web search.",
    instruction=(
        "Answer the user's question. Use the google_search tool to find "
        "current information before answering. Cite what you found."
    ),
    tools=[google_search],
)

6. Compose a Sequential pipeline

Real solutions chain specialists. SequentialAgent runs sub-agents in a fixed order and passes data between them through session state. Each agent writes to its output_key; the next agent reads that value with {key} templating directly in its instruction string.

Here’s a three-stage code pipeline — write, review, refactor:

# pipeline/agent.py
from google.adk.agents import LlmAgent, SequentialAgent

MODEL = "gemini-2.5-flash"

code_writer = LlmAgent(
    name="code_writer",
    model=MODEL,
    description="Writes Python code from a spec.",
    instruction=(
        "You are a Python developer. Write code that satisfies the user's "
        "request. Output only the code block, no explanation."
    ),
    output_key="generated_code",     # saved to state["generated_code"]
)

code_reviewer = LlmAgent(
    name="code_reviewer",
    model=MODEL,
    description="Reviews Python code for bugs and style.",
    instruction=(
        "Review the following code for bugs, edge cases, and style issues. "
        "Be specific and concise.\n\n"
        "Code to review:\n{generated_code}"   # reads previous output
    ),
    output_key="review_comments",
)

code_refactorer = LlmAgent(
    name="code_refactorer",
    model=MODEL,
    description="Refactors code based on review feedback.",
    instruction=(
        "Refactor the original code to address the review comments. "
        "Output only the final code block.\n\n"
        "Original code:\n{generated_code}\n\n"
        "Review comments:\n{review_comments}"
    ),
    output_key="refactored_code",
)

# The orchestrator: runs the three agents in strict order.
root_agent = SequentialAgent(
    name="code_pipeline",
    sub_agents=[code_writer, code_reviewer, code_refactorer],
)

Because all sub-agents share the same InvocationContext, they share session state automatically. The writer’s output is visible to the reviewer with zero extra wiring. This is the heart of the ADK philosophy: deterministic orchestration on the outside, LLM reasoning on the inside.

7. Fan out with a Parallel pipeline

When stages are independent, run them concurrently. ParallelAgent executes sub-agents at the same time and merges their state, which cuts latency on I/O-bound work like multiple research calls.

from google.adk.agents import LlmAgent, ParallelAgent, SequentialAgent

MODEL = "gemini-2.5-flash"

market_research = LlmAgent(
    name="market_research",
    model=MODEL,
    instruction="Research the market size for the user's topic.",
    output_key="market",
)

competitor_research = LlmAgent(
    name="competitor_research",
    model=MODEL,
    instruction="Identify the top competitors for the user's topic.",
    output_key="competitors",
)

# Both run at the same time.
gather = ParallelAgent(
    name="parallel_research",
    sub_agents=[market_research, competitor_research],
)

synthesize = LlmAgent(
    name="synthesizer",
    model=MODEL,
    instruction=(
        "Write a brief using these findings.\n\n"
        "Market:\n{market}\n\nCompetitors:\n{competitors}"
    ),
    output_key="brief",
)

# Parallel gather, then a sequential synthesis step.
root_agent = SequentialAgent(
    name="research_pipeline",
    sub_agents=[gather, synthesize],
)

8. Iterate with a Loop pipeline

LoopAgent re-runs its sub-agents until a condition is met — ideal for refine-until-good-enough workflows.

from google.adk.agents import LlmAgent, LoopAgent

writer = LlmAgent(
    name="writer",
    model="gemini-2.5-flash",
    instruction="Improve the draft based on the latest critique: {critique}",
    output_key="draft",
)

critic = LlmAgent(
    name="critic",
    model="gemini-2.5-flash",
    instruction=(
        "Critique this draft: {draft}. If it is excellent, reply exactly "
        "'APPROVED'. Otherwise give one concrete improvement."
    ),
    output_key="critique",
)

refine_loop = LoopAgent(
    name="refine_loop",
    sub_agents=[writer, critic],
    max_iterations=3,   # hard cap so it always terminates
)

Always set max_iterations. A loop with no ceiling is a runaway token bill waiting to happen.

9. Delegate with sub-agents (dynamic routing)

The workflow agents above are deterministic. For model-decided routing, give an LlmAgent a list of sub_agents and let it delegate. ADK turns this into a typed transfer_to_agent call under the hood.

from google.adk.agents import LlmAgent

billing_agent = LlmAgent(
    name="billing_agent",
    model="gemini-2.5-flash",
    description="Handles billing, invoices, and refund questions.",
    instruction="You resolve billing issues for customers.",
)

tech_agent = LlmAgent(
    name="tech_agent",
    model="gemini-2.5-flash",
    description="Handles technical troubleshooting and bug reports.",
    instruction="You help customers debug technical problems.",
)

# The root agent routes to the right specialist based on the query.
root_agent = LlmAgent(
    name="support_router",
    model="gemini-2.5-flash",
    description="Front-line support that routes to specialists.",
    instruction=(
        "Read the user's request and delegate to the correct specialist. "
        "Use billing_agent for payment topics and tech_agent for technical "
        "issues. Do not answer specialist questions yourself."
    ),
    sub_agents=[billing_agent, tech_agent],
)

The description field on each sub-agent is what the router reads to decide. Write descriptions for the router’s benefit, not for humans.

10. Add a guardrail with a callback

Callbacks are hooks that fire at fixed points in the agent lifecycle: before_agent, before_model, after_model, before_tool, and so on. They’re available on any agent (they all inherit from BaseAgent). The classic use is a guardrail that inspects or blocks a request before it hits the model.

from google.adk.agents import LlmAgent
from google.adk.agents.callback_context import CallbackContext
from google.adk.models import LlmRequest, LlmResponse
from google.genai import types


def block_forbidden_topics(
    callback_context: CallbackContext,
    llm_request: LlmRequest,
) -> LlmResponse | None:
    """Runs before every model call. Block disallowed input."""
    last = ""
    for content in reversed(llm_request.contents):
        if content.role == "user" and content.parts:
            last = content.parts[0].text or ""
            break

    if "password" in last.lower():
        # Returning an LlmResponse short-circuits the model call.
        return LlmResponse(
            content=types.Content(
                role="model",
                parts=[types.Part(text="I can't help with credentials.")],
            )
        )
    return None  # None = proceed to the model normally


agent = LlmAgent(
    name="guarded_agent",
    model="gemini-2.5-flash",
    instruction="You are a helpful assistant.",
    before_model_callback=block_forbidden_topics,
)

Two hard rules for Python callbacks: the parameter names must match the documented names exactly (callback_context, llm_request) because ADK passes them by keyword — renaming to ctx throws a TypeError. And keep callbacks fast and observational; don’t bury expensive LLM logic in them. If you need to rewrite a prompt with another model, use a sub-agent, not a callback.

11. Drive it programmatically with a Runner

adk web is for development. In production you call agents from code with a Runner and a SessionService:

import asyncio
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types

from pipeline.agent import root_agent

APP_NAME = "code_pipeline_app"
USER_ID = "supraj"
SESSION_ID = "session_001"


async def main():
    session_service = InMemorySessionService()
    # NOTE: create_session is async in current ADK — you must await it.
    await session_service.create_session(
        app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID
    )

    runner = Runner(
        agent=root_agent,
        app_name=APP_NAME,
        session_service=session_service,
    )

    message = types.Content(
        role="user",
        parts=[types.Part(text="Write a palindrome checker in Python.")],
    )

    async for event in runner.run_async(
        user_id=USER_ID, session_id=SESSION_ID, new_message=message
    ):
        if event.is_final_response():
            print(event.content.parts[0].text)


asyncio.run(main())

Swap InMemorySessionService for DatabaseSessionService or VertexAiSessionService when state must survive a restart. The in-memory one loses everything on process exit — fine for tests, fatal for production.

12. Use a non-Gemini model

ADK is model-agnostic through LiteLLM. To run an agent on Claude or a local Ollama model, wrap the model string in LiteLlm:

from google.adk.agents import LlmAgent
from google.adk.models.lite_llm import LiteLlm

local_agent = LlmAgent(
    name="local_agent",
    model=LiteLlm(model="ollama_chat/llama3.1"),
    description="Runs on a local Llama model.",
    instruction="You are a helpful assistant.",
)

This is the hook that lets you keep agent logic identical while swapping the brain — invaluable when cost, latency, or data-residency rules out a hosted model.

13. Evaluate before you ship

ADK has built-in evaluation. You define test cases as an eval set, then score the agent’s tool trajectory (did it call the right tools in the right order?) and response quality from the CLI:

adk eval my_agent path/to/my_agent.evalset.json

Because it runs from the terminal, it drops straight into CI/CD — gate your merges on agent eval the same way you gate on unit tests. ADK’s eval is built for the development inner loop; for production quality drift you’ll still want trace-based observability (Cloud Trace, or an OpenTelemetry-based tool) on top.

14. Deploy to Cloud Run

Add a requirements.txt next to your agent folder containing google-adk, then deploy from source. Cloud Run builds the container for you:

gcloud run deploy weather-agent \
  --source . \
  --region us-central1 \
  --allow-unauthenticated

Accept the defaults when prompted, and let it enable Artifact Registry if asked. Your agent inherits managed scaling, auth, and Cloud Trace observability with no code changes.


Common Pitfalls

PitfallWhy it bitesFix
Vague tool docstringsADK feeds the docstring to the model. Weak descriptions mean the tool is never called, or called wrong.Write a clear docstring with Args: and a one-line purpose. Treat it as the tool’s API contract.
Missing __init__.pyADK loads the agent folder as a module. Without it, adk run/adk web can’t discover root_agent.Keep __init__.py in the agent directory. adk create adds it for you.
Mismatched state keysReading {review_comments} before any agent set it yields empty context and silent garbage output.Confirm the upstream output_key exactly matches the {key} you read downstream.
InMemorySessionService in prodState vanishes on restart; multi-turn conversations and pipelines reset unexpectedly.Use DatabaseSessionService or VertexAiSessionService for anything persistent.
Forgetting create_session is asyncIn current ADK, create_session returns a coroutine; calling it without await leaves the session uncreated.await session_service.create_session(...) inside an async function.
Renaming callback paramsADK passes callback args by keyword; using ctx instead of callback_context throws a runtime TypeError.Use the exact documented names: callback_context, llm_request, tool_context.
Loops without a ceilingA LoopAgent with no stop condition can run indefinitely and burn tokens.Always set max_iterations and a clear approval signal.

ADK vs the Alternatives

The agent-framework landscape settled into a handful of real production choices in 2026. There’s no universal winner — each picks a different abstraction. Here’s the honest comparison.

FrameworkCore abstractionBest atWatch out for
Google ADKHierarchical agent tree + deterministic workflow agentsGoogle Cloud teams; fast deploy to Cloud Run / Vertex; built-in eval and memory; native A2AYoungest framework, so production maturity and ecosystem are still catching up
LangGraphDirected graph with conditional edges (state machine)Complex branching, explicit state control, auditability; most mature observability (LangSmith), checkpointing, time-travel debuggingSteepest learning curve of the group
CrewAIRole-based “crews” of agents with goals and backstoriesThe fastest path from idea to working prototype; ships genuine built-in memoryTeams often outgrow its simpler orchestration; limited checkpointing
AutoGen / Microsoft Agent FrameworkConversational GroupChatOffline, quality-sensitive workflows where thoroughness beats speedAutoGen went into maintenance mode in 2026, succeeded by Microsoft Agent Framework (v1.0 GA, April 2026); GroupChat is token-expensive
OpenAI Agents SDKExplicit handoffs between agentsTight integration with OpenAI models; built-in tracing and guardrailsMost natural inside the OpenAI ecosystem

A few cross-cutting truths worth knowing before you pick:

  • Orchestration vs. tools are separate layers. Frameworks like ADK own orchestration, state, and the agent loop. MCP (Model Context Protocol) owns access to tools and external services. ADK speaks MCP, so you can mix and match.
  • A2A is ADK’s quiet superpower. Because ADK speaks the A2A protocol natively, an ADK agent can call a LangGraph or CrewAI agent through a standard interface. You can adopt ADK without abandoning what you’ve already built.
  • Memory is a real differentiator. Among the major options, CrewAI and ADK ship genuine built-in memory; LangGraph gives you checkpointing (state persistence) rather than semantic memory.

When to Use / When NOT to Use

Use ADK whenConsider alternatives when
You need multi-step, repeatable agent workflows with tool callsA single prompt-and-response covers it — just call the model API directly
You’re on GCP/Vertex AI and want a clean path to Cloud Run or GKEYou’re deeply invested in another stack and migration cost outweighs the benefit
You want deterministic orchestration (sequential, parallel, loop) over LLM-only routingYou need a mature visual no-code builder rather than a code-first SDK
You want logic portable across Gemini, GPT, Claude, and local modelsYou need the deepest branching/observability story today — LangGraph leads there
You value built-in evaluation wired into CI/CD from day oneYou’re prototyping fast and CrewAI’s role abstraction gets you there quicker

Key Takeaways

  • ADK 2.0 (Python 3.11+) is a code-first framework that makes agent development feel like software engineering, not prompt wrangling.
  • A tool is just a Python function with a strong docstring and type hints — pass it straight into tools.
  • Use adk web to inspect the trace and debug tool-calling before you deploy anything.
  • The deterministic workflow agentsSequentialAgent, ParallelAgent, LoopAgent — plus output_key and {templating} are how you build real multi-agent pipelines.
  • For model-decided routing, give an LlmAgent a list of sub_agents; ADK delegates via a typed transfer_to_agent call.
  • Callbacks like before_model_callback are your guardrail layer — keep them fast, and match parameter names exactly.
  • Use a Runner with a persistent SessionService for production; never ship InMemorySessionService.
  • ADK is model-agnostic via LiteLLM, and A2A-native, so it interoperates with LangGraph and CrewAI rather than locking you in.
  • Run adk eval in CI to gate merges on agent quality, then deploy with gcloud run deploy --source ..

Further Reading