ADK 2.0's Workflow Runtime: deterministic graphs for the steps agents shouldn't improvise
ADK 2.0 swaps the hierarchical agent executor for a graph-based Workflow Runtime: deterministic nodes, first-class human-in-the-loop, and a benchmarked case for using them.
- adk
- agents
- workflow-runtime
- google-cloud
Every agent framework eventually confronts the same question: when a business process says step B must follow step A, why is a language model deciding that at runtime? ADK 2.0 — stable for Python since May 19 and for Go since June 30 — answers it by inserting a graph between the agent and its own orchestration decisions. The Workflow Runtime is a structural change, not a tuning knob: ADK’s BaseAgent now subclasses BaseNode, and every agent, tool, and function in a workflow is evaluated as a node in an execution graph rather than a step an LLM infers from a long prompt. For the exact kind of failure this site’s eval harness exists to catch — the refund that should escalate but doesn’t, the handoff that never happens — this is the first mechanism that prevents the failure at the architecture level instead of just detecting it after the fact.
What actually shipped
ADK 2.0 reached general availability for Python on May 19, 2026 and for Go on June 30. Both releases carry the same architectural change: a move from ADK’s hierarchical agent executor to a graph-based Workflow Runtime, where agents, tools, and plain functions are all nodes, wired together by edges a developer defines explicitly instead of leaving to model inference. ADK now offers three complementary ways to compose multi-step work. Graph-based workflows are a declarative edges list with explicit routing. Dynamic workflows express the same kind of graph as ordinary async code — loops, conditionals, recursion — for control flow too irregular for a static list. And the older sequential/parallel/loop template agents — which predate 2.0 and run their own fixed orchestration logic without consulting a model — are, per the docs, now positioned as superseded by the graph and dynamic structures for anything beyond those three built-in patterns.
Nodes and edges, not prompts and hope
The minimal shape is a Workflow object whose edges parameter chains agents and functions:
from google.adk import Agent, Workflow, Event
from pydantic import BaseModel
city_generator_agent = Agent(
name="city_generator_agent",
model="gemini-flash-latest",
instruction="Return the name of a random city. Return only the name, nothing else.",
output_schema=str,
)
class CityTime(BaseModel):
time_info: str
city: str
def lookup_time_function(node_input: str):
return CityTime(time_info="10:10 AM", city=node_input)
city_report_agent = Agent(
name="city_report_agent",
model="gemini-flash-latest",
input_schema=CityTime,
instruction="Output: It is {CityTime.time_info} in {CityTime.city} right now.",
output_schema=str,
)
root_agent = Workflow(
name="root_agent",
edges=[
("START", city_generator_agent, lookup_time_function, city_report_agent)
],
)
Each node’s typed return value becomes the next node’s typed input automatically, via a new Event.output field — no session-state writes required to pass data between steps. That is one of two additions to the core Event schema; the other, node_info, tracks which node emitted the event. Anyone running a custom BaseSessionService on rigid SQL columns needs both fields added to the schema before writing 2.0 events, or the insert fails.
Branching without asking the model to route
The interesting case is not the linear chain but the decision point — routing that in a prompt-only agent would be an LLM inferring “if the classification is X, call tool Y.” In a workflow graph, a node emits an Event carrying a route, and a dict of routes replaces the inference:
def router(node_input: str):
routes = [r.strip() for r in node_input.split(",")]
return Event(route=routes)
root_agent = Workflow(
name="routing_workflow",
edges=[
("START", process_message, router),
(router, {
"BUG": response_1_bug,
"CUSTOMER_SUPPORT": response_2_support,
"LOGISTICS": response_3_logistics,
}),
],
)
Google’s own worked example, in the “Why we built ADK 2.0” explainer, applies exactly this pattern to a refund-eligibility check — the same domain this site’s eval harness uses for its worked case (refund-over-limit-001, escalate rather than auto-approve above a threshold). An analyze_complaint_agent node outputs a bare "true" or "false", a routing function converts that into ctx.route, and the graph — not the model — decides whether the next node is issue_refund or close_ticket. Google’s illustrative benchmark for that workflow, run against mocked API responses on gemini-3.5-flash: token usage per run drops from 5,152 to 2,265, roughly 50%, and latency from 7.2 to 5.7 seconds, roughly 20%, because the deterministic nodes execute at code speed instead of waiting on an LLM to parse a tool result and pick the next action. The more consequential property, for anything with a real approval threshold, is that the routing decision now lives in a dict a reviewer can read — not a probability distribution over tokens a crafted input can perturb.
Human input as a graph primitive
ADK 2.0 also makes human-in-the-loop a first-class node type rather than a side channel bolted onto a tool call. A node yields RequestInput, and the workflow pauses until a reply arrives:
from google.adk.events import RequestInput
from google.adk import Workflow
async def get_user_feedback(node_input: ActivitiesList):
yield RequestInput(
message=f"Here is your recommended itinerary:\n{node_input}\n\nWhich items appeal to you?",
payload=node_input,
response_schema=UserFeedback,
)
payload carries structured context for the client to render alongside the prompt; response_schema declares the shape the reply must satisfy — though the docs are explicit that ADK does not coerce a malformed human reply into that schema itself, so a UI or a downstream validating node still has to do that work. A related but distinct mechanism — tool confirmation, an experimental feature that predates the 2.0 graph engine (Python since v1.14.0) — adds a yes/no approval gate inside an LLM agent’s own tool call rather than as a standalone graph node: wrap the tool as FunctionTool(reimburse, require_confirmation=True) and the framework pauses for a boolean response before the call executes. It’s the right primitive when the approval belongs to one tool call rather than an entire workflow stage, though it isn’t compatible with DatabaseSessionService or VertexAiSessionService yet.
What breaks on the way in
ADK 2.0 is compatible with 1.x agents by default, but three breaking changes matter for anyone upgrading a production system rather than starting fresh. Custom overrides of _run_async_impl() are silently ignored now that BaseAgent subclasses BaseNode — that logic has to move into BeforeAgentCallback/AfterAgentCallback. Directly appending to context.session.events breaks graph determinism; events must be yielded so the runtime can route and persist them itself. And inside a tool, a broad except Exception: now masks the framework’s automatic retry mechanism (RetryConfig(max_attempts=3)); catching BaseException goes further and traps NodeInterruptedError itself, which breaks the runtime’s ability to pause for human input at all. None of these are theoretical — they are exactly the kind of defensive tool code every ADK 1.x deployment already has, written before the framework could retry anything on its own.
The practical consequence for evaluation: the eval harness this site published reads event.get_function_calls() and event.is_final_response() off the ADK event stream to score a run. Both still work against 2.0 events, but a harness that wants to assert on which node produced a call — as opposed to just which tool — should also read the new node_info field. A graph gives you an audit trail that a flat tool-call list never could.
The enterprise case: a workflow graph is a governance boundary
For a large, regulated organization, the more interesting property of a workflow graph isn’t cost or latency — it’s that it turns “which agent is allowed to do what” into something a compliance reviewer can read off a static edges list, instead of inferring it from prompt text and hoping the model respects it under adversarial input. That is the same boundary this site’s tool-estate governance piece argued the MCP protocol had just made gateway-enforceable at the transport layer; ADK 2.0 does the equivalent inside the agent’s own execution model. A RequestInput node in front of an irreversible action — a refund above a threshold, a cross-entity data transfer, anything the reference architecture’s governance plane would flag for human-in-the-loop review — is now a graph edge a security architect can point to in a design review, not a paragraph of instruction text an auditor has to trust the model followed. In a conglomerate structure where one agent platform serves multiple subsidiaries under different regulatory regimes, that distinction is the difference between “the agent was instructed not to” and “the agent had no edge that led there.”
What to try next
Start narrow: take one existing prompt-only agent with a hard business rule buried in its instruction text — an approval threshold, a mandatory step order — and re-express just that rule as a Workflow edge, leaving the rest of the agent’s reasoning untouched. Measure the token and latency delta against real traffic, not mocked responses. Then add one RequestInput node at the point in that process where a human already reviews the outcome manually, and see whether the graph’s payload field carries enough context to make that review faster rather than just formalizing it.
The deterministic-versus-autonomous choice was never really about capability — models have been able to route simple decisions correctly for a long time. ADK 2.0’s contribution is making the boundary between the two an artifact in the codebase instead of a hope embedded in a prompt.