1. Overview
Fulfilling a multi-constraint travel request (such as planning a $100 day trip in Kyoto with historic temples, local ramen, and zero modern art museums) requires extracting structured preferences, researching venues concurrently, synthesizing an itemized draft, and auditing the total cost before responding. In ADK 1.x, orchestrating those stages required nesting SequentialAgent, ParallelAgent, and LoopAgent wrappers, saving every intermediate output into session state, and invoking an extra LLM tool call just to exit a refinement loop.
In this codelab, you build a multi-agent travel planner on Agent Platform using Agent Development Kit (ADK) 2 graph workflows (Workflow). Across three progressive stages, you connect specialist Agent nodes (powered by gemini-3.8-flash) in a sequential edge chain (node_input), fan out concurrent google_search researchers synchronized with JoinNode, and enforce budget and exclusion rules with a deterministic Python router (Event(route=...)) that skips refinement whenever the initial draft passes audit.
Prerequisites
- Basic proficiency in Python programming
- Familiarity with Generative AI concepts and AI agent fundamentals
What you learn
- How to declare a multi-agent pipeline in ADK 2 using
Workflowedge tuples - How ADK 2 passes outputs directly along edges as
node_inputand binds session state keys as function parameters - How to fan out concurrent research branches and merge their outputs with
JoinNode - How to route bounded maker-checker cycles deterministically in Python with
Event(route=...) - How to inspect compiled workflow graphs, parallel execution timelines, and state deltas in the ADK Web UI
What you need
- A Google account (use a personal
@gmail.comaccount if claiming workshop credits) - A workshop credit link from your instructor (plus a phone camera to scan the QR code) or an active Cloud Billing account
- Google Chrome (an Incognito window is recommended to avoid mixing personal and work accounts)
2. Claim Google Cloud credits (Optional)
If you are attending a workshop with an event credit link, claim your Google Developer Program credit to provision a billing account without a credit card.
Claim your event credit
- Open the workshop credit link in an Incognito window and sign in with your
@gmail.comaccount. - On Claim Your Benefit, click Claim Credit:

- If prompted to create a Google Developer Program profile, complete the form.
- When Scan to Verify You're Human appears, scan the QR code with your phone and complete verification (refresh and click Claim Credit again if the QR code does not appear):

- Confirm Successfully claimed your event credit! appears, then click Open Billing Account:

🔍 Check your work
Billing account setup takes up to 3 minutes:
- Open Google Cloud Billing (or copy the URL into your Incognito window) and select Your billing accounts:
https://console.cloud.google.com/billing
- Refresh until your
GDP Creditaccount shows status Active:

3. Create a Google Cloud project (Optional)
Create a dedicated Google Cloud project so all resources from this codelab stay isolated and easy to clean up.
- Open Create a Project (or copy the URL into your Incognito window):
https://console.cloud.google.com/projectcreate
- Enter your project details:
- Project name: For example,
google-cloud-workshop - Billing account (if shown): Select
GDP Credit(or your active billing account) - Parent resource: No organization (or select your organization if required)
- Project name: For example,
- Note the generated Project ID below the project name, then click Create:

🔍 Check your work
Click Notifications in the top-right toolbar and click Select Project once project creation finishes:

4. Set up your environment
Configure Google Cloud Shell, enable the Agent Platform API, and create an isolated Python 3.12 virtual environment with uv.
Configure Cloud Shell
- Click Activate Cloud Shell (
>_) in the Google Cloud console toolbar, or open Google Cloud Shell directly (or copy the URL into your Incognito window):
https://shell.cloud.google.com/?show=ide,terminal

- Click Authorize if prompted:

- Select your active Google Cloud Project ID from the numbered menu in your terminal:
unset CLOUDSDK_CORE_PROJECT
PS3="Select your project (e.g. 1): "
select PROJECT_ID in $(gcloud projects list --format="value(projectId)"); do
[ -n "$PROJECT_ID" ] && gcloud config set project "$PROJECT_ID" && break
done
You should see output similar to (untagged projects also print an informational [environment: untagged] line):
1) google-cloud-workshop-4210 2) my-cloud-project-8812 Select your project (e.g. 1): 1 Updated property [core/project].
Verify your selected Project ID appears highlighted in yellow in the Cloud Shell prompt:

Enable required APIs
Enable the Agent Platform API so your ADK agents can invoke Gemini models:
gcloud services enable aiplatform.googleapis.com
You should see output similar to:
Operation "operations/acat.p2-..." finished successfully.
Create a Python virtual environment and install ADK
Create the ~/adk-graph-agents workspace and activate a Python 3.12 virtual environment:
cd ~
mkdir -p adk-graph-agents && cd ~/adk-graph-agents
which uv >/dev/null 2>&1 || curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
uv venv .venv --python 3.12
source .venv/bin/activate
Confirm the (.venv) prefix appears at the start of your terminal prompt:

Install ADK 2 inside the active virtual environment:
uv pip install --no-cache "google-adk==2.14.0"
🔍 Check your work
Verify the installed adk CLI version:
adk --version
You should see output similar to:
adk, version 2.14.0
5. Scaffold the travel planner
Generate the starter travel_planner package with adk create and test the default single-agent baseline in adk web.
Generate the starter package
Run adk create from ~/adk-graph-agents:
cd ~/adk-graph-agents
adk create travel_planner
Select the following options when prompted (if prompted with Enable telemetry? [Y/n]: on first run, press Enter or type n and press Enter):
- Choose a model for the root agent: Type
2to selectgemini-3.8-flash(option1selectsgemini-3.5-flash) and press Enter. - Choose a backend: Type
2to select the Google Cloud Agent Platform backend (option2in the CLI menu below) and press Enter. - Enter Google Cloud project ID: Press Enter to accept your detected Project ID in brackets.
- Enter Google Cloud region: Type
global(to use the global Gemini endpoint) and press Enter.
adk create writes GOOGLE_GENAI_USE_ENTERPRISE=1, your project ID, and GOOGLE_CLOUD_LOCATION=global into travel_planner/.env, so your credentials remain intact when you replace agent.py later.
You should see output similar to:
Choose a model for the root agent: 1. gemini-3.5-flash 2. gemini-3.8-flash 3. Other models (fill later) Choose model (1, 2, 3): 2 1. Google AI 2. Vertex AI 3. Login with Google Choose a backend (1, 2, 3): 2 You need an existing Google Cloud account and project, check out this link for details: https://google.github.io/adk-docs/get-started/quickstart/#gemini---google-cloud-vertex-ai Enter Google Cloud project ID [google-cloud-workshop-4210]: Enter Google Cloud region [us-central1]: global Agent created in /home/your_username/adk-graph-agents/travel_planner: - .env - .gitignore - __init__.py - agent.py ⚠️ WARNING: Secrets (like GOOGLE_API_KEY) are stored in .env. Next steps: - Chat with your agent in the terminal: adk run travel_planner - Or in the browser: adk web . - Have your coding agent evaluate and deploy it: uvx google-agents-cli setup (other install options: https://adk.dev/get-started/agents-cli/)
Open ~/adk-graph-agents in the Cloud Shell code editor:
which cloudshell >/dev/null 2>&1 && cloudshell workspace ~/adk-graph-agents || true
Reactivate your virtual environment in the reloaded terminal:
cd ~/adk-graph-agents
source .venv/bin/activate
Test the starter single agent
Start the ADK Web UI to observe how a single unorchestrated starter agent answers a travel prompt:
cd ~/adk-graph-agents
adk web --allow_origins "*"
Click the Web Preview icon in the top-right Cloud Shell toolbar, select Change Port, enter 8000, and click Change and Preview:

🔍 Check your work
Select travel_planner in the top-left dropdown of adk web and send "Help me plan a trip to Japan":

You should see a single-pass conversational reply in the chat pane (with a single root_agent invocation in Events):
root_agent -> responds in one pass without structured preference extraction, live search, or budget verification
Press Ctrl + C in your Cloud Shell terminal to stop adk web before editing files in the next step.
6. Build a linear graph workflow
Build your first ADK 2 Workflow: a sequential chain connecting the reserved "START" entry point to user_profiler_agent and itinerary_synthesizer_agent (any node with no outgoing edges acts as a terminal node). Every edge (a, b) automatically runs Agent (an alias for LlmAgent) nodes in isolated single_turn mode and delivers node a's output as node_input (the incoming user message) to node b, while {user_preferences} in an instruction string interpolates the matching session state key saved via output_key.

Create the initial specialists
Ensure adk web is stopped (Ctrl + C), then open a new file named travel_planner/specialists.py:
cloudshell edit ~/adk-graph-agents/travel_planner/specialists.py
Paste the following code into travel_planner/specialists.py to define user_profiler_agent and itinerary_synthesizer_agent (importing google_search upfront for the next step):
from google.adk import Agent
from google.adk.tools import google_search
MODEL = "gemini-3.8-flash"
# --- Specialist 1: The User Profiler ---
user_profiler_agent = Agent(
model=MODEL,
name="user_profiler_agent",
description="Extracts travel preferences and constraints from user input.",
instruction="""
Extract destination, duration (default 1 day), budget, interests, and exclusions.
Return a single JSON object with keys: "destination", "duration", "budget", "interests", "exclusions".
""",
output_key="user_preferences", # Saved for multi-hop constraints downstream
)
# --- Specialist 2: The Synthesizer ---
itinerary_synthesizer_agent = Agent(
model=MODEL,
name="itinerary_synthesizer_agent",
description="Compiles preferences and research into a final Markdown itinerary.",
instruction="""
You are an elite Travel Concierge. Using the incoming input and
User Preferences ({user_preferences}), write a Markdown itinerary
with an itemized USD and local currency cost breakdown.
""",
output_key="final_itinerary",
)
Define the sequential edge chain
Open a new file named travel_planner/workflows.py:
cloudshell edit ~/adk-graph-agents/travel_planner/workflows.py
Paste the following Workflow definition (where a tuple of 3 or more nodes compiles into a sequential chain from "START"):
from google.adk import Workflow
from .specialists import (
itinerary_synthesizer_agent,
user_profiler_agent,
)
# 1. SEQUENTIAL GRAPH: START -> user_profiler_agent -> itinerary_synthesizer_agent
travel_workflow = Workflow(
name="travel_workflow",
description="Orchestrates the end-to-end travel planning process as a graph workflow.",
edges=[
("START", user_profiler_agent, itinerary_synthesizer_agent),
],
)
Expose the workflow as root_agent
Open travel_planner/agent.py:
cloudshell edit ~/adk-graph-agents/travel_planner/agent.py
Replace the entire contents of travel_planner/agent.py with these two lines:
from .workflows import travel_workflow
root_agent = travel_workflow
🔍 Check your work
Start adk web from ~/adk-graph-agents:
cd ~/adk-graph-agents
adk web --allow_origins "*"
Open Web Preview on port 8000, click + New Session, and send:
Help me plan a trip to Kyoto
In the Events view, you should see an event sequence similar to:
1. user_profiler_agent -> outputs JSON {"destination": "Kyoto", "duration": "1 day", ...} and writes state["user_preferences"]
2. itinerary_synthesizer_agent -> receives preferences via node_input and outputs the Markdown itinerary

Click any event in Events and click the Graph icon (account_tree in the event detail toolbar) to inspect the compiled START -> user_profiler_agent -> itinerary_synthesizer_agent topology, then check the State tab to verify user_preferences and final_itinerary are populated.
Press Ctrl + C in your terminal to stop adk web before continuing.
7. Fan out and join with JoinNode
Ground the itinerary in live web data by adding two concurrent google_search specialists (activity_finder_agent and restaurant_finder_agent) and synchronizing them with JoinNode:
- Fan-out tuple: Placing
(activity_finder_agent, restaurant_finder_agent)inside the edge tuple deliversuser_profiler_agent's output asnode_inputto both researchers concurrently. - Fan-in barrier (
JoinNode): PlacingJoinNode(name="research_join")right after the tuple waits until every incoming branch completes and passes one mergeddict({"activity_finder_agent": ..., "restaurant_finder_agent": ...}) asnode_inputtoitinerary_synthesizer_agentinstead of triggering synthesis twice.

Append the research specialists
Confirm adk web is stopped (Ctrl + C), then open travel_planner/specialists.py:
cloudshell edit ~/adk-graph-agents/travel_planner/specialists.py
Append activity_finder_agent and restaurant_finder_agent to the bottom of travel_planner/specialists.py (neither needs output_key because research_join forwards both outputs directly via node_input):
# --- Specialist 3: The Activity Finder ---
activity_finder_agent = Agent(
model=MODEL,
name="activity_finder_agent",
description="Finds 3 attractions matching the user's destination and interests.",
instruction="""
Using the travel preferences in the input, call google_search to find 3 specific attractions
(including budget-friendly options).
Return a JSON list with keys: "attraction_name", "estimated_cost_local", "estimated_cost_usd", "notes".
""",
tools=[google_search],
)
# --- Specialist 4: The Restaurant Finder ---
restaurant_finder_agent = Agent(
model=MODEL,
name="restaurant_finder_agent",
description="Finds 3 restaurants matching the destination and budget.",
instruction="""
Using the travel preferences in the input, call google_search to find 3 highly-rated restaurants
across price points.
Return a JSON list with keys: "restaurant_name", "cuisine", "estimated_cost_local", "estimated_cost_usd".
""",
tools=[google_search],
)
Add fan-out and JoinNode to workflows.py
Open travel_planner/workflows.py:
cloudshell edit ~/adk-graph-agents/travel_planner/workflows.py
Replace the entire contents of travel_planner/workflows.py with the fan-out and JoinNode graph:
from google.adk import Workflow
from google.adk.workflow import JoinNode
from .specialists import (
activity_finder_agent,
itinerary_synthesizer_agent,
restaurant_finder_agent,
user_profiler_agent,
)
research_join = JoinNode(name="research_join")
# 2. FAN-OUT / FAN-IN GRAPH: Profile -> (Activities || Restaurants) -> Join -> Synthesize
travel_workflow = Workflow(
name="travel_workflow",
description="Orchestrates the end-to-end travel planning process as a graph workflow.",
edges=[
(
"START",
user_profiler_agent,
(activity_finder_agent, restaurant_finder_agent),
research_join,
itinerary_synthesizer_agent,
),
],
)
🔍 Check your work
Start adk web to test concurrent search and JoinNode synchronization:
cd ~/adk-graph-agents
adk web --allow_origins "*"
Open Web Preview on port 8000, click + New Session, and send:
Help me plan a trip to Kyoto focused on historic temples and ramen
In the Events view, you should see an event sequence similar to:
1. user_profiler_agent -> emits JSON preferences via node_input
2. activity_finder_agent & restaurant_finder_agent -> invoke google_search concurrently
3. research_join -> merges both researcher outputs into one dict node_input
4. itinerary_synthesizer_agent -> drafts final_itinerary from the merged dict + {user_preferences}
Press Ctrl + C in your terminal to stop adk web before continuing.
8. Route revision cycles
Enforce budget and negative constraints by adding an itinerary_critic_agent, an itinerary_refiner_agent, and two deterministic Python function nodes (route_critique and finalize_itinerary) that return ADK Event objects:
route: Selects which named branch key in a dict edge ({"revise": ..., "approved": ...}) runs next.output: Passes a typed Python value or string along the edge asnode_inputto the next node without rendering a chat message.messageandstate:messageemits user-visible text in the chat transcript, whilestatecommits key-value updates (stateDelta) to session state.

Append the critic and refiner specialists
Confirm adk web is stopped (Ctrl + C), then open travel_planner/specialists.py:
cloudshell edit ~/adk-graph-agents/travel_planner/specialists.py
Append itinerary_critic_agent and itinerary_refiner_agent to the bottom of travel_planner/specialists.py (the refiner needs no google_search or exit_loop tool because it swaps items already gathered in {final_itinerary} and only runs when route_critique selects "revise"):
# --- Specialist 5: The Budget & Constraint Critic ---
itinerary_critic_agent = Agent(
model=MODEL,
name="itinerary_critic_agent",
description="Audits the itinerary draft against budget and negative constraints.",
instruction="""
Audit the itinerary in the input against User Preferences ({user_preferences}).
Verify that the USD total stays within the user's budget and no excluded venue types appear.
If all checks pass, respond EXACTLY with the single word "APPROVED".
Otherwise, output 1-2 concise revision instructions explaining what to swap or adjust.
""",
)
# --- Specialist 6: The Itinerary Refiner ---
itinerary_refiner_agent = Agent(
model=MODEL,
name="itinerary_refiner_agent",
description="Revises the itinerary draft using the critic's feedback.",
instruction="""
Revise Current Itinerary ({final_itinerary}) using the auditor critique in the input
so all USD budget and exclusion rules in {user_preferences} pass.
Output ONLY the updated Markdown itinerary.
""",
output_key="final_itinerary",
)
Add route_critique and the cycle
Open travel_planner/workflows.py:
cloudshell edit ~/adk-graph-agents/travel_planner/workflows.py
Replace the entire contents of travel_planner/workflows.py with the complete graph workflow:
from google.adk import Event, Workflow
from google.adk.workflow import JoinNode
from .specialists import (
activity_finder_agent, itinerary_critic_agent, itinerary_refiner_agent,
itinerary_synthesizer_agent, restaurant_finder_agent, user_profiler_agent,
)
MAX_REVISIONS = 2
research_join = JoinNode(name="research_join")
def route_critique(node_input: str, revision_count: int = 0):
"""Routes to 'approved' when constraints pass, or 'revise' up to MAX_REVISIONS."""
approved = node_input.strip().rstrip(".").upper() == "APPROVED"
if approved or revision_count >= MAX_REVISIONS:
return Event(route="approved", output=approved)
return Event(
route="revise", output=node_input, state={"revision_count": revision_count + 1}
)
def finalize_itinerary(node_input: bool, final_itinerary: str, revision_count: int = 0):
"""Emits the verified itinerary and resets revision_count for the next turn."""
status = (
f"Itinerary approved after {revision_count} revision(s)."
if node_input
else f"Max revisions ({revision_count}) reached; returning best-effort draft."
)
return Event(
message=f"{status}\n\n{final_itinerary}",
output=final_itinerary,
state={"revision_count": 0},
)
travel_workflow = Workflow(
name="travel_workflow",
description="Orchestrates the end-to-end travel planning process as a graph workflow.",
edges=[
(
"START",
user_profiler_agent,
(activity_finder_agent, restaurant_finder_agent),
research_join,
itinerary_synthesizer_agent,
itinerary_critic_agent,
route_critique,
),
(route_critique, {"revise": itinerary_refiner_agent, "approved": finalize_itinerary}),
(itinerary_refiner_agent, itinerary_critic_agent),
],
)
🔍 Check your work
Verify that travel_planner/workflows.py compiles cleanly and prints the directed graph edges:
cd ~/adk-graph-agents && source .venv/bin/activate
python3 -c "from travel_planner.agent import root_agent; print('\n'.join(f'{e.from_node.name} -> {e.to_node.name} (route={e.route})' for e in root_agent.graph.edges))"
You should see output similar to:
__START__ -> user_profiler_agent (route=None) user_profiler_agent -> activity_finder_agent (route=None) user_profiler_agent -> restaurant_finder_agent (route=None) activity_finder_agent -> research_join (route=None) restaurant_finder_agent -> research_join (route=None) research_join -> itinerary_synthesizer_agent (route=None) itinerary_synthesizer_agent -> itinerary_critic_agent (route=None) itinerary_critic_agent -> route_critique (route=None) route_critique -> itinerary_refiner_agent (route=revise) route_critique -> finalize_itinerary (route=approved) itinerary_refiner_agent -> itinerary_critic_agent (route=None)
Next, start adk web to test both immediate approval and budget-triggered revisions:
adk web --allow_origins "*"
Open Web Preview on port 8000, click + New Session, and send the multi-constraint Kyoto prompt:
Design a full-day itinerary for Kyoto with a total budget of $100. My main interests are historical landmarks, temples, and authentic local cuisine. Please do not include any modern art museums or galleries.
To watch the route="revise" loop fire in action, send a follow-up prompt with a tight $15 budget:
Now design a full-day Kyoto itinerary with paid temple admissions, lunch, and dinner on a strict total budget of $15 USD.
In the Events view, you should see an execution sequence similar to:
1. user_profiler_agent -> writes JSON constraints to state["user_preferences"] 2. (activity_finder_agent, restaurant_finder_agent) -> run google_search concurrently -> research_join 3. itinerary_synthesizer_agent -> writes initial draft to state["final_itinerary"] 4. itinerary_critic_agent -> outputs "APPROVED" (or revision instructions when over budget) 5. route_critique -> emits route="approved" (skipping refiner) or route="revise" -> itinerary_refiner_agent -> itinerary_critic_agent 6. finalize_itinerary -> emits "Itinerary approved after N revision(s)." (or max-revisions notice) and resets revision_count=0
Press Ctrl + C in your terminal to stop adk web.
9. Troubleshooting
The agent dropdown is empty in adk web
- Cause:
adk webwas started insidetravel_planner/instead of the parent~/adk-graph-agentsdirectory, or the Cloud Shell session restarted. - Fix: Press
Ctrl + C, return to the parent directory, and restart withcd ~/adk-graph-agents && source .venv/bin/activate && adk web --allow_origins "*".
404 NOT_FOUND model error
- Cause: You pressed Enter to accept the default
us-central1region duringadk createinstead of typingglobal. - Fix: Open
~/adk-graph-agents/travel_planner/.envand ensureGOOGLE_CLOUD_LOCATION=globalis set.
ERROR: [Errno 98] address already in use on port 8000
- Cause: A previous
adk webserver is still running in Cloud Shell. - Fix: Press
Ctrl + Cin the active terminal or stop the existing process withpkill -f "adk web"before startingadk webagain.
10. Clean up
Remove the local workspace and optionally disable the API or delete a dedicated workshop project.
Delete the local project files
Remove the ~/adk-graph-agents directory and virtual environment:
cd ~
rm -rf ~/adk-graph-agents
Disable the Agent Platform API (Optional)
Disable the Agent Platform API if you no longer need it in this project:
gcloud services disable aiplatform.googleapis.com --quiet
🔍 Check your work
Confirm that ~/adk-graph-agents has been removed:
test ! -d ~/adk-graph-agents && echo "Workspace ~/adk-graph-agents removed"
You should see output similar to:
Workspace ~/adk-graph-agents removed
11. Congratulations
You built a multi-agent travel planner using ADK 2 Workflow, unifying sequential chains, concurrent google_search fan-out with JoinNode, and conditional maker-checker cycles in a single declarative edge graph.
What you accomplished
- Declared a multi-agent pipeline using ADK 2
Workflow(edges=[...]) - Passed data directly between
Agent,JoinNode, and Python function nodes vianode_inputand state-bound parameters while keeping session state minimal - Fanned out concurrent
google_searchbranches and synchronized them withJoinNode - Replaced
LoopAgentand tool-based loop escalation with a deterministicEvent(route=...)router that skips unnecessary LLM calls on approved drafts
Next steps
- Explore Multi-Agent Systems in ADK
- Join the GEAR (Gemini Enterprise Agent Ready) program and Get Certified program for hands-on workshops and learning pathways