Google Agent Development Kit 2.0 (ADK 2.0) is the second major version of Google’s open-source framework for building AI agents. Its central change is architectural: the 1.x hierarchical agent executor is replaced by a graph-based execution engine, where every unit of work (an LLM agent, a plain Python function, a tool call, a human-input step, another workflow) is a node, and the edges between nodes say what runs next. The practical effect is a change in who makes routing decisions. In 1.x, a large share of control flow lived in prompts: you told a coordinator model to “hand billing questions to the billing agent” and hoped. In 2.0, control flow lives in code you can read, test and checkpoint, and the model is called only for the steps that need judgment. ADK 2.0 reached general availability for Python on May 19, 2026, Go on June 30, 2026 and TypeScript on August 21, 2026, and sits in the same family of graph-runtime agent SDKs as Microsoft Agent Framework.
The Problem 2.0 Is Solving
Most real agent processes are a mix. Some steps are fixed (fetch the purchase history, check the refund policy, send the email), some need reasoning (is this customer’s complaint legitimate?), and some need a person (approve anything over $500). A framework that routes everything through an LLM pays for a model call at every hop, introduces a chance of the wrong turn at every hop, and leaves an audit trail that amounts to a transcript. A framework that is purely a rigid pipeline cannot handle the open-ended parts.
ADK 2.0’s answer is to let you choose, step by step and node by node, between code and model, and to give code the same first-class status as an agent. The documentation’s phrase for the principle is that predictable work stays as functions, clear rules become explicit routing, and reasoning goes to the model. Nodes that are plain functions run without invoking a generative model at all, so they are free, instant and deterministic.
Three Ways to Compose: Who Decides What Runs Next?
ADK 2.0 offers three composition styles, and the cleanest way to choose among them is to ask who decides the next step.
| Style | Who decides what runs next | Use it when | Core API |
|---|---|---|---|
| Graph workflow | The diagram you drew: declared nodes, edges and routes | The shape of the process is known before any input arrives | Workflow(edges=[...]) |
| Collaborative workflow | The model, choosing from a team of sub-agents | You know the team, but the request determines which members take part | Agent(sub_agents=[...]) with mode= |
| Dynamic workflow | Your code at runtime: loops, conditionals, recursion | The shape depends on the data (decompose into N sub-tasks, retry until a check passes) | @node plus ctx.run_node() |
The three compose: a graph node can be a collaborative team, and a collaborative agent can launch a dynamic workflow. ADK also keeps the older template workflow agents (sequential, parallel and loop) for simple fixed structures.
%%{init: {'theme': 'base'}}%%
flowchart TD
classDef default fill:#ffffff,stroke:#4338CA,stroke-width:2px,color:#0F172A,rx:8px,ry:8px;
classDef data fill:#EEF0F7,stroke:#0D9488,stroke-width:2px,color:#0F172A,rx:8px,ry:8px;
classDef process fill:#F7F8FC,stroke:#6366F1,stroke-width:2px,color:#0F172A,rx:8px,ry:8px;
classDef output fill:#4338CA,stroke:#4338CA,stroke-width:2px,color:#ffffff,rx:8px,ry:8px;
Q{Is the shape of the work known up front?}:::process
Q -- "Yes, I can draw it" --> G[Graph workflow]:::output
Q -- "Known team, request decides who joins" --> C[Collaborative workflow]:::output
Q -- "Shape depends on the data" --> D[Dynamic workflow]:::output
G --> N1[Function nodes: no LLM cost]:::data
G --> N2[Agent nodes: reasoning]:::data
C --> M1[chat, task or single_turn sub-agents]:::data
D --> L1[Loops, recursion and parallel workers in plain code]:::data
Graph Workflows: Nodes, Edges and Routes
A graph workflow is a Workflow object with a list of edges. A node can be an Agent, a function, a tool or a nested workflow. The simplest edge is a chain that begins at the reserved START node, and each node’s return value becomes the next node’s node_input, so you do not have to thread data through session state by hand.
# Scenario: a three-step pipeline. The agent picks a city, a function looks up
# the time (no model call), and a second agent writes the report from the
# structured result. Output flows along the edges automatically.
from google.adk import Agent, Workflow
from pydantic import BaseModel
class CityTime(BaseModel):
time_info: str
city: str
city_agent = Agent(name="city_generator", model="gemini-2.5-flash",
instruction="Return the name of a random city. Return only the name.")
def lookup_time(node_input: str): # a function node
return CityTime(time_info="10:10 AM", city=node_input)
report_agent = Agent(name="city_report", model="gemini-2.5-flash",
instruction="Write one friendly sentence about the city and its local time.")
root_agent = Workflow(
name="root_agent",
edges=[("START", city_agent, lookup_time, report_agent)],
)
Conditional branching uses routes. A node returns an Event with a route field, and the edge for that node is a dictionary that maps route values to destinations. The route is independent of the payload, so a node can branch and forward its data at the same time. A route can also be a list, which fans out to several branches, and a default route catches anything that matched no other (the TypeScript docs show it as DEFAULT_ROUTE).
# Scenario: triaging an incoming message that may belong to more than one
# queue. The classifier agent replies with a comma-separated list; the router
# function turns that into routes; each route has its own handler.
from google.adk import Event
def router(node_input: str):
routes = [r.strip() for r in node_input.split(",")]
return Event(route=routes) # a list fans out to every matching branch
root_agent = Workflow(
name="routing_workflow",
edges=[
("START", process_message, router),
(router, {
"BUG": handle_bug,
"CUSTOMER_SUPPORT": handle_support,
"LOGISTICS": handle_logistics,
}),
],
)
Three other graph features come up quickly in real systems. State is data visible to the whole graph, written with Event(state={...}) or ctx.state[key], with scope prefixes for application-wide (app:), per-user (user:), current-invocation (temp:) or session-lifetime (no prefix) values. Loops are back-edges taken through a route, such as a reviewer that routes either back to the drafter or onward to send; the engine re-activates the earlier node with a fresh lifecycle on each pass. Parallel fan-out and join runs branches concurrently and converges them at a join node, which waits for every branch and hands the collected outputs to the next node.
# Scenario: a draft-review loop with a bounded exit, plus a parallel analysis
# stage. The reviewer's route decides whether to loop or finish; the three
# analyses run concurrently and a join node gathers their outputs.
from google.adk.workflow import Workflow, JoinNode
join = JoinNode(name="join_analyses")
review_loop = Workflow(
name="review_loop",
edges=[
("START", drafter),
(drafter, reviewer),
(reviewer, {"revise": drafter, "approve": send}), # back-edge taken only via a route
],
)
analysis = Workflow(
name="analysis",
edges=[
("START", (sentiment, entities, summary)), # fan-out to three nodes
((sentiment, entities, summary), join), # fan-in at the join
(join, write_report),
],
)
Graph workflows have limits. The documentation notes that some third-party integrations are not compatible with them, and that retry behavior needs care (see the migration section below).
Dynamic Workflows: When the Shape Depends on the Data
Some processes cannot be drawn in advance: how many sub-questions a research task splits into is decided by a model at runtime; a fix-and-recheck loop runs until a linter is satisfied. A dynamic workflow writes that logic as ordinary Python. A function decorated with @node becomes a node, and await ctx.run_node(child, node_input=...) runs another node and returns its output directly, so conditionals, while loops, recursion and asyncio.gather all work as they normally do.
# Scenario: a code-fixing loop. Write code, lint it, and keep fixing until the
# linter is clean. In a graph this is a routed back-edge; as ordinary code it
# is a while loop anyone can read.
from google.adk import Event
from google.adk.workflow import node
@node(rerun_on_resume=True)
async def code_workflow(ctx, user_request: str):
code = await ctx.run_node(coder_agent, user_request)
check = await ctx.run_node(compile_lint_check, code)
while check.findings:
yield Event(state={"code": code, "findings": check.findings})
code = await ctx.run_node(fixer_agent, {"code": code, "findings": check.findings})
check = await ctx.run_node(compile_lint_check, code)
yield Event(output=code)
The feature that makes dynamic workflows safe for long-running work is checkpointing. Parent nodes that call ctx.run_node set rerun_on_resume=True, which means that if the run is interrupted (a human-input pause, a crash, a restart), the parent’s body re-executes from the top on resume, but every child node that already succeeded is skipped and its recorded result reused. The loop above can be interrupted halfway through its third fix and resume without paying for the first two again. For reorderable work, you can give children stable identifiers (run_id=f"order-{order.order_id}") so a resumed run matches results to the right items.
# Scenario: process a batch of orders in parallel. Each child run gets a stable
# id derived from the order, so a resumed run reuses finished orders instead of
# redoing them.
import asyncio
@node(rerun_on_resume=True)
async def process_batch(ctx, orders: list):
tasks = [ctx.run_node(process_order, o, run_id=f"order-{o['id']}") for o in orders]
return await asyncio.gather(*tasks)
Reporting on ADK 2.0 notes that some capabilities of dynamic workflows were still marked alpha even after the Python release reached general availability, so check the status of the specific feature you depend on.
Collaborative Workflows and the Task API
When you know which specialists exist but not which ones a given request needs, you use a collaborative workflow: a coordinator agent with sub_agents. ADK 2.0 adds an explicit mode on each sub-agent that fixes how it talks to the user and when control returns to the coordinator, which is the part that was fuzzy in 1.x.
| Mode | User interaction | How control returns | Can run in parallel |
|---|---|---|---|
chat (default) | Full conversation | Manual: the parent takes the conversation back | No |
task | Clarifying questions allowed | Automatic, via finish_task, once the objective is met | No |
single_turn | None | Automatic, with a validated result | Yes |
The Task API is the task mode. A task-mode agent holds the conversation until it has collected the fields it needs, then finishes and hands a structured result back to the coordinator; the coordinator can delegate to it through an auto-generated delegation tool named after the sub-agent. Task-mode agents must be leaf agents and cannot have sub-agents of their own.
# Scenario: a travel planner. Weather lookups are quick and independent
# (single_turn, may run in parallel). Booking a flight needs a back-and-forth
# with the user until the required fields are filled (task), then returns a
# validated FlightResult to the planner.
from google.adk import Agent
weather_agent = Agent(
name="weather_checker",
mode="single_turn",
tools=[get_weather, geocode_address],
)
flight_agent = Agent(
name="flight_booker",
mode="task",
input_schema=FlightInput, # what the coordinator must supply
output_schema=FlightResult, # what it hands back via finish_task
tools=[search_flights, book_flight],
)
root_agent = Agent(
name="travel_planner",
sub_agents=[weather_agent, flight_agent],
)
This is the same underlying idea as subagent orchestration in a multi-agent system: isolate work into a specialist and let the coordinator see only the result. ADK 2.0’s contribution is making the hand-off contract (who talks to the user, when control comes back, what shape the result has) a typed, declared property instead of an emergent behavior of prompts.
Human Input: Pausing a Workflow Mid-Run
A node that yields a RequestInput pauses the workflow at that point and emits an event for the client to render. RequestInput takes a message for the person, an optional payload of structured data to display, and an optional response_schema the reply must conform to. When the human responds, the workflow resumes according to the node’s rerun_on_resume setting: with False, the usual choice for a leaf input node, the reply is routed to the node’s successor as its input; with True, the node body re-runs from the top with the reply available.
# Scenario: an agent that must not delete anything without approval. The
# approval node yields RequestInput and bypasses itself on resume; the parent
# re-runs from the top and finds the finished children already checkpointed.
from google.adk.events import RequestInput
from google.adk.workflow import node
@node(rerun_on_resume=False)
async def get_approval(ctx, node_input):
yield RequestInput(
message="Delete 214 stale records from production?",
response_schema=ApprovalDecision, # the reply must validate against this
)
@node(rerun_on_resume=True)
async def cleanup(ctx, node_input):
plan = await ctx.run_node(plan_agent, node_input) # skipped on resume
decision = await ctx.run_node(get_approval, plan)
if decision.approved:
return await ctx.run_node(delete_records, plan)
return "Cleanup cancelled."
Because the checkpointed state lives in the session, a paused workflow is durable: Google’s Go 2.0 announcement describes workflows as resumable across process restarts through session-state persistence, and adds that human-in-the-loop now also works for a plain LLM agent, not only inside a workflow. This is the production form of human-in-the-loop review: an approval that may arrive in minutes or days, survives a deploy, and cannot be skipped because the next node does not run without it.
Interactive: What Does Putting Code in Charge Save?
The strongest argument for a graph is arithmetic. An autonomous agent loop makes a model call at every step, and each call resends a context that has grown by every earlier tool result. A graph calls the model only at the nodes that need judgment, with a focused input. There is a second effect: every step the model must choose correctly is a chance for the wrong turn, while a graph’s edges are exact. Move the sliders to see both effects.
Start at the defaults and then push the process length to 15 steps while leaving the judgment share at 50%: the loop’s token count grows faster than linearly, because step 15 resends everything the first fourteen produced, while the graph’s grows linearly with the number of judgment nodes. Now pull the judgment share toward 100%: the saving shrinks because more steps call a model, but it does not vanish, since each graph node sees only the input it needs instead of the whole history. That is the model’s best-case assumption, and the honest limit of the argument: a step that genuinely needs the entire history pays for it in a graph too, and a process that is all reasoning gets mostly structure and checkpoints from a graph rather than savings. Finally drag the routing accuracy down to 90% and watch the loop’s chance of following the intended path collapse over a long process. The model ignores prompt caching, which lowers the cost of a repeated prefix (see prefix caching), and assumes the agent loop’s mistakes are independent, so treat it as a picture of the mechanism, not a benchmark.
What Google’s Own Numbers Show
Google published an illustrative comparison of the same customer-refund process built two ways: a single autonomous agent with five tools and numbered instructions in its system prompt, and an ADK 2.0 workflow with six nodes (tool calls plus two LLM nodes for analysis and drafting) joined by deterministic edges. Reported results were 5,152 tokens and 7.2 seconds per run for the autonomous agent against 2,265 tokens and 5.7 seconds for the workflow, a reduction of roughly 56% in tokens and 21% in latency. Google labels the results illustrative, produced with a Gemini Flash model and mock API responses, which means real payment-gateway latency is absent. The takeaway is directional: the savings come from not asking a model to make decisions that code can make.
The same analysis lists when graphs beat model loops: the execution sequence is predefined, failure states are explicit and predictable, compliance requires an audit trail, volume is high enough that orchestration tokens matter, and untrusted text reaches the model (a deterministic path leaves a prompt-injected document fewer chances to redirect the agent; see prompt injection). Autonomous agents remain the better choice while the process is still being discovered.
Migrating From 1.x
ADK 2.0 is a major version, and the migration guide is blunt about the breaking changes. Teams that postpone can stay on 1.x with pip install "google-adk~=1.0".
- The event schema grew. Events gain fields such as node information, output and routing metadata. If you store events in custom database columns, the schema must change; JSON-blob storage needs no change. Storage and event schemas differ between 1.x and 2.0, so do not mix the two against the same data.
- Agents are now nodes. Custom execution overrides (such as
_run_async_implin Python) are bypassed by the graph engine and silently ignored. Move that logic into before-agent and after-agent callbacks. - Events must be yielded, not appended. Appending directly to
session.eventsbreaks determinism and checkpointing; emit events from nodes instead. - Broad exception handlers now hide failures from retries. The engine retries failed nodes, but only if it sees the failure.
# Scenario: a tool that calls an inventory API. A catch-all handler that returns
# an error dict looks safe, but in 2.0 it means the engine never sees a failure,
# so automatic retry never fires. Catch only what you can actually handle.
import requests
def check_stock_bad(sku: str) -> dict:
try:
return requests.get(f"https://inventory.example.com/{sku}", timeout=5).json()
except Exception: # swallows timeouts and 5xx alike: nothing to retry
return {"error": "lookup failed"}
def check_stock(sku: str) -> dict:
resp = requests.get(f"https://inventory.example.com/{sku}", timeout=5)
if resp.status_code == 404: # an expected, handleable case
return {"in_stock": False}
resp.raise_for_status() # anything else propagates so the engine can retry
return resp.json()
ADK 2.0 and Microsoft Agent Framework
The two SDKs landed on the same architectural idea within weeks of each other, which is itself informative: when a framework needs to be production-grade, a declared graph beats an LLM-routed hierarchy.
| Google ADK 2.0 | Microsoft Agent Framework | |
|---|---|---|
| Core runtime | Graph workflow engine; agents are nodes | Graph-based Workflows engine over executors |
| Composition styles | Graph, dynamic, collaborative, plus sequential, parallel and loop templates | Sequential, concurrent, handoff, group chat and magentic patterns |
| Pausing for a person | RequestInput with checkpointed resume (rerun_on_resume) | See the linked entry |
| Languages at GA | Python, Go, TypeScript | Python, .NET |
| Model stance | Optimized for Gemini; model-agnostic through LiteLLM | Azure OpenAI, OpenAI directly, and MCP-compatible tool servers |
| Code-first dynamic mode | Yes: @node plus ctx.run_node() with automatic checkpoint skip | Workflows are declarative graphs plus the magentic manager pattern |
Limitations Worth Naming
- It is a real migration. Because agents became nodes and events changed shape, existing 1.x agents with custom execution logic need work, not a version bump.
- A graph is only as good as its diagram. Putting control flow in code removes model unpredictability but moves the risk to your own routing logic. A graph with many routes and back-edges can become as hard to reason about as the prompt-based orchestration it replaced.
- Features are at different maturity levels. Reporting on the 2.0 release describes some dynamic-workflow features as alpha even though the framework is generally available, and some third-party integrations do not work inside graph workflows.
- More structure means less improvisation. If you are still discovering what the process should be, a loose autonomous agent is the cheaper way to explore. Convert the parts that stabilize into graph nodes as you learn them.
- Durability depends on persistence. Pausing and resuming across restarts relies on the session store holding the checkpointed state; pick and test a persistent session backend before relying on long human approvals.
What’s New (2025-2026)
- General availability, language by language. Python 2.0 went GA on May 19, 2026, Go 2.0 on June 30, 2026 and TypeScript on August 21, 2026, each carrying the same graph model so a workflow design transfers between languages.
- Task mode and per-mode agent contracts. The
chat,taskandsingle_turnmodes, withfinish_taskand typed input and output schemas, turned delegation to a sub-agent into a declared contract. - Checkpointed dynamic workflows. Writing orchestration as ordinary async code, with finished children skipped on resume, brought the durability people associate with workflow engines to plain Python.
- Human-in-the-loop for plain agents. With the Go 2.0 release, Google noted that human approval is no longer limited to workflow nodes and also works on a single LLM agent.
- A published case for graphs over loops. The refund-process comparison from July 2026 (5,152 versus 2,265 tokens, with the caveats above) gave teams a concrete argument for moving fixed processes out of the prompt.
- Graph runtimes are converging across vendors. Alongside Microsoft Agent Framework and the broader move toward agent harnesses, explicit graphs with checkpoints are becoming a common answer to “how do we make agents reliable”.
Practical Guidance
| Situation | Recommendation |
|---|---|
| The process has a known shape (intake, check, decide, act) | Build a graph workflow; use function nodes for everything that does not need a model |
| Some steps are pure rules (thresholds, lookups, formatting) | Make them function nodes, not agent instructions: free, instant and testable |
| The next step depends on the data (split into N tasks, retry until a check passes) | Use a dynamic workflow with rerun_on_resume=True on the parent so resumed runs skip finished children |
| You have specialists and the request decides who joins | Use a coordinator with sub-agents; mark quick independent ones single_turn and form-filling ones task |
| An action is irreversible or regulated | Put a RequestInput node in front of it, with a response_schema for the reply |
| You are upgrading from 1.x | Move custom execution overrides into callbacks, yield events instead of appending, narrow your exception handlers, and migrate agent by agent; pin google-adk~=1.0 until ready |
| You are still exploring what the process should be | Start with a single autonomous agent, then convert the stable parts into graph nodes |
| Deciding between ADK and another SDK | Choose by language, model stack and deployment target; the graph runtime itself is no longer the differentiator |
The through-line of ADK 2.0 is a simple rule for agent design: spend model calls only where the model adds something code cannot, and make everything else an explicit, inspectable, resumable step. The graph is not a replacement for an agent. It is the frame that lets you decide, one node at a time, how much of the work to hand to one.
How to Use: A support-ticket Workflow where code routes, the model classifies, and a human approves refunds
# Install: pip install google-adk (Python 3.10+)
#
# Scenario: a support inbox. The model is used only where judgment is needed
# (classifying the ticket). Routing is plain code, so it is deterministic and
# costs no tokens, and a refund cannot go out without a human saying yes.
from google.adk import Agent, Workflow, Event
from google.adk.events import RequestInput
from pydantic import BaseModel
class Ticket(BaseModel):
category: str # BILLING or BUG
summary: str
classifier = Agent(
name="classifier",
model="gemini-2.5-flash",
instruction="Classify the ticket as BILLING or BUG and summarise it in one line.",
output_schema=Ticket,
)
def router(node_input: Ticket):
# A function node: no model call. The route value picks the outgoing edge.
return Event(output=node_input, route=node_input.category)
def approve_refund(node_input: Ticket):
# Yielding RequestInput pauses the whole workflow until a person replies.
yield RequestInput(message=f"Refund requested: {node_input.summary}. Approve? (yes/no)")
def issue_refund(node_input: str):
return "Refund issued." if node_input.strip().lower() == "yes" else "Refund declined."
bug_agent = Agent(
name="bug_triage",
model="gemini-2.5-flash",
instruction="Draft a bug report from the ticket summary you are given.",
)
root_agent = Workflow(
name="ticket_workflow",
edges=[
("START", classifier, router), # a chain: START -> classifier -> router
(router, {"BILLING": approve_refund, "BUG": bug_agent}), # route value -> next node
(approve_refund, issue_refund), # the human's reply becomes issue_refund's input
],
)
Ready to build?
Leverage AI technologies to build your product stack
Superteams can help you build, deploy and launch AI application stacks using open source technologies — from architecture through to production.
Talk to Superteams