Level: Intermediate Env: Google Cloud Shell
Four small multi-agent apps, one per orchestration pattern, then deploy one of them to Cloud Run:
| Pattern | Folder | What it teaches |
|---|---|---|
| Router / Coordinator | router_agent_demo/ | An LLM dynamically picks which sub-agent should answer |
| Sequential | research_assistant/ | A fixed pipeline: step 1 always runs before step 2 |
| Parallel | parallel_agent_demo/ | Independent sub-agents run concurrently, then get merged |
| Loop | research_assistant/ (the refine_loop part) | Repeat a critique/revise cycle until a condition is met |
Finish Workshop 1 first (or at least the one-time environment setup). You should have adk --version working and your Gemini backend exported.
ADK gives you a small set of workflow agents you compose like building blocks. Each one just controls how its sub_agents run:
Router / Coordinator - LLM picks a specialist
Root Agent -.-> Billing Agent
Root Agent -.-> Tech Support Agent
Root Agent -.-> General Agent
Sequential - fixed pipeline, order guaranteed
Step 1 --> Step 2 --> Step 3
Parallel - fan-out, then fan-in
Fan-out --> Agent A -\
Fan-out --> Agent B --+--> Fan-in / Merge
Fan-out --> Agent C -/
Loop - repeat until approved or max_iterations
Critic --> Reviser --(not approved)--> Critic
Reviser --(exit_loop called)--> Done
SequentialAgent - runs sub-agents one after another, in order. Use when step B needs step A's output.
ParallelAgent - runs sub-agents at the same time. Use when steps are independent and you want speed.
LoopAgent - repeats its sub-agents until something calls escalate (or it hits max_iterations). Use for iterative refinement.
Router / Coordinator - this isn't a special class, it's a plain Agent with sub_agents=[...] and no tools of its own. ADK auto-wires a transfer_to_agent tool, and the LLM decides at runtime which specialist gets the message. Use when the right handler depends on user intent.
All four can nest inside each other - a SequentialAgent step can itself be a ParallelAgent, which is exactly what parallel_agent_demo does.
Open router_agent_demo/agent.py. Three specialists (billing_agent, tech_support_agent, general_agent), and a root agent that has no tools - only sub_agents and an instruction telling it to always delegate.
router_agent_demo/agent.py
root_agent = Agent(
name="support_router",
model=MODEL,
description="Routes a user's message to the right support specialist.",
instruction=(
"You are a routing coordinator for a support desk. Read the user's "
"message and immediately delegate to exactly one specialist:\n"
"- billing_agent: charges, payments, refunds, invoices, subscriptions\n"
"- tech_support_agent: bugs, errors, crashes, 'how do I' technical asks\n"
"- general_agent: everything else\n"
"Do not attempt to answer the question yourself - always delegate."
),
sub_agents=[billing_agent, tech_support_agent, general_agent],
)
Run it:
adk web
Pick router_agent_demo and try, in order:
"I was charged twice for my subscription this month" -> should transfer to billing_agent
"My app keeps crashing with a NullPointerException" -> should transfer to tech_support_agent
"Hey, how's it going?" -> should transfer to general_agent
Open the trace panel: the very first event from the root agent is a transfer_to_agent function call - it never generates a text answer itself.
Exercise: each specialist's description field is what the router reads to decide where to send things. Try weakening tech_support_agent's description to something vague and watch routing accuracy degrade - good lesson that description quality is routing quality.
Open research_assistant/agent.py and look at the bottom:
root_agent = SequentialAgent(
name="research_assistant",
sub_agents=[researcher, writer, refine_loop],
)
researcher writes to state["research"], writer reads {research} and writes state["draft"], and refine_loop (Part D, below) reads/rewrites {draft}. Order is guaranteed - writer never runs before researcher has finished.
Run it:
adk web
Pick research_assistant and ask: "Summarize the latest on Google ADK". Watch researcher -> writer -> the loop fire strictly in order in the trace panel.
Open parallel_agent_demo/agent.py:
gather = ParallelAgent(
name="gather",
sub_agents=[weather_agent, clock_agent, trivia_agent],
)
root_agent = SequentialAgent(
name="city_briefing",
sub_agents=[gather, synthesizer],
)
weather_agent, clock_agent, and trivia_agent don't depend on each other, so they run concurrently inside gather, each writing to its own output_key. Then synthesizer (a plain sequential step) reads all three and merges them into one answer.
Run it:
adk web
Pick parallel_agent_demo and ask: "Give me a briefing on Tokyo". In the trace panel, compare the timestamps on weather_agent, clock_agent, and trivia_agent - they overlap, unlike the strictly sequential steps in Part B. That overlap is the performance win ParallelAgent buys you.
Exercise: add a fourth parallel branch (e.g. a news_agent using google_search) and wire its output_key into the synthesizer instruction template.
Back in research_assistant/agent.py, look at the refine_loop:
def exit_loop(tool_context: ToolContext) -> dict:
"""Signal that the draft is approved and the refinement loop should stop."""
tool_context.actions.escalate = True
return {"status": "approved"}
critic = Agent(..., tools=[exit_loop], output_key="critique")
reviser = Agent(..., output_key="draft")
refine_loop = LoopAgent(
name="refine_loop",
max_iterations=3,
sub_agents=[critic, reviser],
)
Every pass: critic reads {draft} and either calls exit_loop (draft is good) or writes actionable feedback to {critique}; reviser applies that feedback and overwrites {draft}. The loop keeps going until exit_loop fires or max_iterations is hit - that cap is your safety net against an agent that never agrees its own work is good enough.
Run it: same research_assistant session from Part B - watch the trace panel count critic -> reviser passes and stop the moment exit_loop is called (often before hitting the 3-pass cap).
Combine two patterns: make router_agent_demo's tech_support_agent a SequentialAgent of its own (e.g. a "diagnose" step then a "suggest fix" step), or give it its own small LoopAgent for iterative troubleshooting. Nesting is the whole point - a workflow agent is just another Agent as far as its parent is concerned.
We'll deploy research_assistant since it already ships with .env-style docs. Same command shape as Workshop 1, just a different folder - use whichever backend column matches your setup.
Option A - Gemini Developer API:
adk deploy cloud_run research_assistant -- \
--region=us-central1 \
--set-env-vars=GOOGLE_API_KEY=YOUR_GEMINI_API_KEY,GOOGLE_GENAI_USE_VERTEXAI=FALSE
Option B - Vertex AI:
# One-time per project (skip if you already did this in Workshop 1):
PROJECT_NUMBER=$(gcloud projects describe $(gcloud config get-value project) --format='value(projectNumber)')
gcloud projects add-iam-policy-binding $(gcloud config get-value project) \
--member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
--role="roles/aiplatform.user"
adk deploy cloud_run research_assistant -- \
--region=us-central1 \
--set-env-vars=GOOGLE_CLOUD_PROJECT=YOUR_PROJECT_ID,GOOGLE_CLOUD_LOCATION=us-central1,GOOGLE_GENAI_USE_VERTEXAI=TRUE
Note the service URL, then create a session and invoke it exactly like Workshop 1:
export SERVICE_URL="https://adk-default-service-name-XXXXXXXXXX.us-central1.run.app"
curl -X POST "$SERVICE_URL/apps/research_assistant/users/u_123/sessions/s_1" \
-H "Content-Type: application/json" -d '{}'
curl -X POST "$SERVICE_URL/run" \
-H "Content-Type: application/json" \
-d '{
"appName": "research_assistant",
"userId": "u_123",
"sessionId": "s_1",
"newMessage": {
"role": "user",
"parts": [{"text": "Help me research Google ADK."}]
}
}'
The response event stream will show the full pipeline: researcher -> writer -> critic/reviser loop passes -> final draft.
gcloud run services delete adk-default-service-name --region=us-central1
Router: Agent + sub_agents, no tools of its own -> LLM picks the specialist at runtime.
Sequential: SequentialAgent -> guaranteed order, data flows via state/output_key.
Parallel: ParallelAgent -> concurrent independent work, fanned back in by a following step.
Loop: LoopAgent + an exit_loop tool that sets tool_context.actions.escalate = True, capped by max_iterations.
All four nest inside each other - mix and match to fit the shape of the real problem.
Deployment story is identical across all patterns: adk deploy cloud_run <folder>.