Skip to content
Blog

Google ADK 2.0: Graph Workflows for Deterministic Agent Orchestration

Stop asking the LLM to do routing. ADK 2.0 graph workflows turn agent orchestration into deterministic code—cutting latency, token costs, and hallucinated execution paths.

Published on August 20, 2026

AI Assistant

LLMs are bad at routing. Ask a model to decide “what happens next” in a business process and it will occasionally skip a step, invent a path, or loop forever—not because it’s dumb, but because orchestration is exactly the kind of deterministic work that traditional code excels at. Every LLM-routed decision costs tokens, adds latency, and injects variance.

ADK 2.0’s answer is the Workflow Runtime: your agents, tools, and functions become nodes in an execution graph, and routing happens programmatically in code. The LLM is reserved for nodes that genuinely require reasoning.

In this tutorial, you will learn how to build graph-based workflows with Google ADK 2.0—mixing agent nodes and deterministic code nodes, adding conditional routing, and knowing when to reach for the other ADK 2.0 patterns.

Prerequisites

  • Python 3.10+ (ADK 2.0 also ships for Go)
  • pip install google-adk
  • An API key for your model provider (Google AI / Vertex AI / OpenAI / etc.)

The Problem: LLMs Shouldn’t Orchestrate

Consider a refund process. In a naive agent, the model does everything: read the complaint, look up policy, decide eligibility, trigger a Stripe refund, draft an email, update the CRM. That’s a huge context window, high token costs, and a real risk the model hallucinates an execution path or skips a compliance step.

The ADK 2.0 philosophy: if you can map the workflow, use determinism. If B always follows A, there is no reason to wait for the LLM to infer the next step. Those are tokens and seconds you could be saving.

The ADK 2.0 Workflow Approach

Map the refund process as a directed graph:

  • Node A (Tool): Fetch purchase history via database query.
  • Node B (LLM Agent): Analyze the complaint against policy exceptions.
  • Node C (Tool): Issue the refund programmatically via Stripe.
  • Node D (LLM Agent): Draft a customized confirmation email.
  • Node E (Tool): Update the support ticket status in the CRM.

The LLM only runs at nodes B and D. Everything between them is programmatic execution speed.

Building Your First Graph

In ADK 2.0, a plain Python function and an LLM agent are the same kind of node in the edges list:

from google.adk import Agent
from google.adk.workflow import Workflow, START

# Deterministic code nodes
def fetch_purchase_history(node_input: str) -> dict:
    return query_db(purchase_id=node_input)  # fast, deterministic

def issue_refund(node_input: dict) -> dict:
    return stripe.refund(order_id=node_input["order_id"])  # side effect

def close_ticket(node_input: dict) -> dict:
    return crm.close(node_input["ticket_id"])

# LLM reasoning nodes
analyze_complaint_agent = Agent(
    name="analyze_complaint",
    tools=[get_policy],
    instruction=(
        "Check the complaint details against company policy using get_policy. "
        "Decide if the customer is eligible. Output exactly 'true' or 'false'."
    ),
    mode="single_turn",
)

draft_email_agent = Agent(
    name="draft_email",
    tools=[send_email],
    instruction=(
        "Draft a customer confirmation email summarizing the action "
        "and send it using send_email."
    ),
    mode="single_turn",
)

def route_complaint(node_input: str, ctx) -> bool:
    ctx.route = "true" in str(node_input).lower()
    return node_input

workflow = Workflow(
    name="Refund_Workflow",
    edges=[
        (START, fetch_purchase_history, analyze_complaint_agent),
        (analyze_complaint_agent, route_complaint, {True: issue_refund, False: close_ticket}),
        (issue_refund, draft_email_agent, close_ticket),
    ],
)

Two key design wins:

  • Programmatic routing: transitions are evaluated in code. The route_complaint function’s boolean decides the branch—no LLM decision, no hallucination.
  • Strict state boundaries: each node receives only the necessary subset of data. The email agent never sees policy documents or API history, keeping its context minimal and focused.

Conditional Routing and Fan-Out

Graphs support the full toolkit of control flow.

Conditional branches via a router function and dict dispatch:

def classify(node_input: str, ctx) -> str:
    category = classify_topic(node_input)  # BUG | CUSTOMER_SUPPORT | LOGISTICS
    ctx.route = category
    return category

edges = [
    (START, process_message, classify),
    (classify, bug_handler, {"BUG": bug_handler}),
    (classify, support_handler, {"CUSTOMER_SUPPORT": support_handler}),
    (classify, logistics_handler, {"LOGISTICS": logistics_handler}),
]

Parallel fan-out with a join—a JoinNode waits for all upstream tasks, then passes the collection of outputs to the next node:

from google.adk.workflow import JoinNode

my_join_node = JoinNode(name="join")

edges = [
    ("START", research_task_A, my_join_node),
    ("START", research_task_B, my_join_node),
    ("START", research_task_C, my_join_node),
    (my_join_node, final_task_D),
]

Loops via back-edges—the graph routes back to an earlier node until a condition is met, the ADK 2.0 home for critic→refine→critic loops.

The Three ADK 2.0 Patterns

Graphs are one pillar. ADK 2.0 gives you three orchestration patterns:

PillarWho decides what runs nextBest for
Graph workflowsThe graph you drewDeterministic, structured processes you can draw before input arrives
Collaborative workflowsThe LLM (coordinator + subagents)A known team, but the request picks the subset per turn
Dynamic workflowsYour code at runtimeWork whose shape depends on the input (runtime fan-out, recursion)

Collaborative agents declare a team with sub_agents and a mode:

  • chat — full conversation, specialist owns it.
  • task — clarifying questions only, then finish_task with a validated object.
  • single_turn — no human, runs in parallel, returns its result automatically.
coordinator = Agent(
    name="coordinator",
    sub_agents=[medical, weather, pacing, gear, nutrition, mental],
    mode="single_turn",  # each specialist runs as a parallel tool-like call
)

Dynamic workflows hide the shape inside your code with @node(parallel_worker=True) and ctx.run_node(...)—runtime-sized fan-out and recursion that a static graph can’t express.

The One-Question Decision Tree

  • Would a prebuilt SequentialAgent / ParallelAgent / LoopAgent do? → use it, stop.
  • No — I need routing, a join, or non-agent nodes:
    • Can you draw the workflow before input arrives? → Graph.
    • Known team, request picks the subset? → Collaborative.
    • Does the shape depend on the input? → Dynamic.

The three patterns compose. A graph node can call a collaborative coordinator; a specialist can launch a dynamic workflow.

Putting It All Together

For a complete, runnable guided walkthrough of all three patterns—graph, collaborative, and dynamic—see the official codelab:

Conclusion & Next Steps

You now know how to build deterministic orchestration with ADK 2.0 graph workflows: mixing agent and function nodes, routing in code, and choosing the right pattern for the problem.

Next steps:

  • Try the Marathon Race Day Coach codelab to run all three pillars live.
  • Measure token savings: graph workflows reserve the LLM for reasoning nodes only.
  • Migrate an ADK 1.x agent and review the Workflow Runtime breaking changes.

Production-grade AI doesn’t require choosing between pure code and pure agents. The most reliable architectures combine both—isolating the probabilistic behavior of LLMs to nodes that need reasoning, and letting the graph handle everything else.

References