chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:25:13 +08:00
commit ec2b666284
2231 changed files with 491535 additions and 0 deletions
@@ -0,0 +1,83 @@
# Agents In Workflow
This sample demonstrates how to use both `task` mode and `single_turn` mode Agents as nodes within a `Workflow`.
## Overview
The workflow represents a medical lab intake process:
1. **`intake_agent`**: A `task` mode Agent that chats with the user to collect their `name` and `phone_number`. It handles a multi-turn conversation until its `PatientIdentity` output schema is fulfilled.
1. **`check_identity`**: A regular Python function node that receives the `PatientIdentity`. It mocks checking the database.
- If the name is anything other than "Jane Doe", it yields a `retry` route, sending the user back to the `intake_agent`.
- If the name is "Jane Doe", it routes to the `generate_instruction` agent.
1. **`generate_instruction`**: A `single_turn` mode Agent that uses the `find_orders` tool to look up orders. It requires tool confirmation before execution.
## Sample Inputs
- `Hi, I am Jane Doe, my phone number is 555-1234.`
*The system will process this and return the mock lab orders along with AI-generated instructions on how to prepare.*
- `I'm here for my blood work.`
*The system will ask for your name and phone number.*
- `My name is John Doe, and my number is 123-456-7890.`
*The system will fail to find John's orders and route back to the intake agent.*
## Graph
```text
[ START ]
|
v
[ intake_agent ] <----.
| |
v |
[ check_identity ] --- retry
|
| (DEFAULT_ROUTE)
v
[ generate_instruction ]
```
## How To
Within an ADK workflow, you can embed LLM agents directly as nodes. The ADK runner handles them according to their `mode`:
### 1. Task Mode Agents
A `task` agent (`mode="task"`) handles a multi-turn conversation on its own before passing control to the next node. It will continually interact with the user until its specified task is completed.
```python
class PatientIdentity(BaseModel):
name: str
phone_number: str
intake_agent = Agent(
name="intake_agent",
mode="task", # Stops and chats with the user until the schema is populated
output_schema=PatientIdentity,
instruction="...",
)
```
The parsed `output_schema` object is automatically forwarded as the `node_input` to the next node in the graph.
### 2. Single Turn Mode Agents
A `single_turn` agent (the default mode if omitted) executes a single LLM call. It is typically used for inline text generation, summarization, or classification without chatting with the user.
```python
generate_instruction = Agent(
name="generate_instruction",
tools=[FunctionTool(find_orders, require_confirmation=True)],
instruction="""
Use the find_orders tool to get the patient's orders.
List the orders found, and then generate a concise instruction about how to prepare based on those orders.
""",
)
```
In this sample, the `generate_instruction` agent uses the `find_orders` tool to retrieve the orders. It also demonstrates **tool confirmation**, requiring the user to approve the tool call before it executes.
@@ -0,0 +1,87 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from google.adk import Agent
from google.adk import Event
from google.adk import Workflow
from google.adk.tools.function_tool import FunctionTool
from google.adk.workflow import DEFAULT_ROUTE
from pydantic import BaseModel
from pydantic import Field
class PatientIdentity(BaseModel):
"""Output schema for the intake agent."""
name: str = Field(description="The patient's full name.")
phone_number: str = Field(description="The patient's phone number.")
intake_agent = Agent(
name="intake_agent",
mode="task",
output_schema=PatientIdentity,
instruction="""\
You are a medical lab intake assistant. Your job is to chat with
the user to get their full name and phone number. Do not make up
information. Once you have both, finish your task.
If identity check failed, ask for another name.
""",
)
def check_identity(node_input: PatientIdentity):
"""Mocks checking the database for the patient.
Routes back to intake_agent if the name is not Jane Doe.
"""
if node_input.name.lower() != "jane doe":
yield Event(
message=(
f"Could not find matching records for {node_input.name}. Let's"
" try again."
),
route="retry",
)
else:
yield Event(
message=f"""Hello {node_input.name}! Let me look up your orders."""
)
def find_orders() -> list[str]:
"""Finds orders for the patient."""
return ["CBC (Complete Blood Count)", "Lipid Panel"]
generate_instruction = Agent(
name="generate_instruction",
tools=[FunctionTool(find_orders, require_confirmation=True)],
instruction="""
Use the find_orders tool to get the patient's orders.
List the orders found, and then generate a concise instruction about how to prepare based on those orders.
""",
)
root_agent = Workflow(
name="task_in_workflow",
edges=[
("START", intake_agent, check_identity),
(
check_identity,
{"retry": intake_agent, DEFAULT_ROUTE: generate_instruction},
),
],
)
@@ -0,0 +1,282 @@
{
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "go"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"content": {
"parts": [
{
"text": "Hello! I'm your medical lab intake assistant. To start, may I please have your full name?"
}
],
"role": "model"
},
"author": "intake_agent",
"nodeInfo": {
"path": "task_in_workflow@1/intake_agent@1"
},
"isolationScope": "task_in_workflow@1/intake_agent@1"
},
{
"content": {
"parts": [
{
"text": "Jane Doe"
}
],
"role": "user"
},
"author": "user",
"nodeInfo": {
"path": ""
}
},
{
"content": {
"parts": [
{
"text": "Thank you, Jane Doe. May I please have your phone number now?"
}
],
"role": "model"
},
"author": "intake_agent",
"nodeInfo": {
"path": "task_in_workflow@1/intake_agent@1"
},
"isolationScope": "task_in_workflow@1/intake_agent@1"
},
{
"content": {
"parts": [
{
"text": "555-1234"
}
],
"role": "user"
},
"author": "user",
"nodeInfo": {
"path": ""
}
},
{
"content": {
"parts": [
{
"functionCall": {
"id": "fc-1",
"args": {
"name": "Jane Doe",
"phone_number": "555-1234"
},
"name": "finish_task"
}
}
],
"role": "model"
},
"author": "intake_agent",
"nodeInfo": {
"path": "task_in_workflow@1/intake_agent@1"
},
"isolationScope": "task_in_workflow@1/intake_agent@1"
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "finish_task",
"response": {
"result": "Task completed."
}
}
}
],
"role": "user"
},
"author": "intake_agent",
"output": {
"name": "Jane Doe",
"phone_number": "555-1234"
},
"nodeInfo": {
"path": "task_in_workflow@1/intake_agent@1",
"outputFor": [
"task_in_workflow@1/intake_agent@1"
]
},
"isolationScope": "task_in_workflow@1/intake_agent@1"
},
{
"content": {
"parts": [
{
"text": "Hello Jane Doe! Let me look up your orders."
}
],
"role": "user"
},
"author": "task_in_workflow",
"nodeInfo": {
"path": "task_in_workflow@1/check_identity@1"
}
},
{
"content": {
"parts": [
{
"functionCall": {
"id": "fc-2",
"args": {},
"name": "find_orders"
}
}
],
"role": "model"
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
}
},
{
"content": {
"parts": [
{
"functionCall": {
"id": "fc-3",
"args": {
"originalFunctionCall": {
"id": "fc-2",
"args": {},
"name": "find_orders"
},
"toolConfirmation": {
"hint": "Please approve or reject the tool call find_orders() by responding with a FunctionResponse with an expected ToolConfirmation payload.",
"confirmed": false
}
},
"name": "adk_request_confirmation"
}
}
]
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
},
"longRunningToolIds": [
"fc-3"
]
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-2",
"name": "find_orders",
"response": {
"error": "This tool call requires confirmation, please approve or reject."
}
}
}
],
"role": "user"
},
"author": "generate_instruction",
"actions": {
"skipSummarization": true,
"requestedToolConfirmations": {
"fc-2": {
"hint": "Please approve or reject the tool call find_orders() by responding with a FunctionResponse with an expected ToolConfirmation payload.",
"confirmed": false
}
}
},
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
}
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-3",
"name": "adk_request_confirmation",
"response": {
"confirmed": true,
"payload": {}
}
}
}
],
"role": "user"
},
"author": "user",
"nodeInfo": {
"path": ""
}
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-2",
"name": "find_orders",
"response": {
"result": [
"CBC (Complete Blood Count)",
"Lipid Panel"
]
}
}
}
],
"role": "user"
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
}
},
{
"content": {
"parts": [
{
"text": "The patient has the following orders: CBC (Complete Blood Count) and Lipid Panel. Please fast for 8-12 hours prior to your appointment for the Lipid Panel."
}
],
"role": "model"
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1",
"outputFor": [
"task_in_workflow@1/generate_instruction@1",
"task_in_workflow@1"
],
"messageAsOutput": true
}
}
]
}
@@ -0,0 +1,279 @@
{
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "go"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"content": {
"parts": [
{
"text": "Hello! I'm here to help you with your lab intake. Could I please get your full name and phone number?"
}
],
"role": "model"
},
"author": "intake_agent",
"nodeInfo": {
"path": "task_in_workflow@1/intake_agent@1"
},
"isolationScope": "task_in_workflow@1/intake_agent@1"
},
{
"content": {
"parts": [
{
"text": "Jane Doe"
}
],
"role": "user"
},
"author": "user",
"nodeInfo": {
"path": ""
}
},
{
"content": {
"parts": [
{
"text": "Thanks, Jane. Could I please get your phone number?"
}
],
"role": "model"
},
"author": "intake_agent",
"nodeInfo": {
"path": "task_in_workflow@1/intake_agent@1"
},
"isolationScope": "task_in_workflow@1/intake_agent@1"
},
{
"content": {
"parts": [
{
"text": "555-1234"
}
],
"role": "user"
},
"author": "user",
"nodeInfo": {
"path": ""
}
},
{
"content": {
"parts": [
{
"functionCall": {
"id": "fc-1",
"args": {
"name": "Jane Doe",
"phone_number": "555-1234"
},
"name": "finish_task"
}
}
],
"role": "model"
},
"author": "intake_agent",
"nodeInfo": {
"path": "task_in_workflow@1/intake_agent@1"
},
"isolationScope": "task_in_workflow@1/intake_agent@1"
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "finish_task",
"response": {
"result": "Task completed."
}
}
}
],
"role": "user"
},
"author": "intake_agent",
"output": {
"name": "Jane Doe",
"phone_number": "555-1234"
},
"nodeInfo": {
"path": "task_in_workflow@1/intake_agent@1",
"outputFor": [
"task_in_workflow@1/intake_agent@1"
]
},
"isolationScope": "task_in_workflow@1/intake_agent@1"
},
{
"content": {
"parts": [
{
"text": "Hello Jane Doe! Let me look up your orders."
}
],
"role": "user"
},
"author": "task_in_workflow",
"nodeInfo": {
"path": "task_in_workflow@1/check_identity@1"
}
},
{
"content": {
"parts": [
{
"functionCall": {
"id": "fc-2",
"args": {},
"name": "find_orders"
}
}
],
"role": "model"
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
}
},
{
"content": {
"parts": [
{
"functionCall": {
"id": "fc-3",
"args": {
"originalFunctionCall": {
"id": "fc-2",
"args": {},
"name": "find_orders"
},
"toolConfirmation": {
"hint": "Please approve or reject the tool call find_orders() by responding with a FunctionResponse with an expected ToolConfirmation payload.",
"confirmed": false
}
},
"name": "adk_request_confirmation"
}
}
]
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
},
"longRunningToolIds": [
"fc-3"
]
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-2",
"name": "find_orders",
"response": {
"error": "This tool call requires confirmation, please approve or reject."
}
}
}
],
"role": "user"
},
"author": "generate_instruction",
"actions": {
"skipSummarization": true,
"requestedToolConfirmations": {
"fc-2": {
"hint": "Please approve or reject the tool call find_orders() by responding with a FunctionResponse with an expected ToolConfirmation payload.",
"confirmed": false
}
}
},
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
}
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-3",
"name": "adk_request_confirmation",
"response": {
"confirmed": false,
"payload": {}
}
}
}
],
"role": "user"
},
"author": "user",
"nodeInfo": {
"path": ""
}
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-2",
"name": "find_orders",
"response": {
"error": "This tool call is rejected."
}
}
}
],
"role": "user"
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
}
},
{
"content": {
"parts": [
{
"text": "I'm sorry, but I was unable to retrieve the patient's orders. The tool call was rejected."
}
],
"role": "model"
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1",
"outputFor": [
"task_in_workflow@1/generate_instruction@1",
"task_in_workflow@1"
],
"messageAsOutput": true
}
}
]
}
@@ -0,0 +1,253 @@
{
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "Hi, my name is Jane Doe"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"content": {
"parts": [
{
"text": "Hi Jane, what is your phone number?"
}
],
"role": "model"
},
"author": "intake_agent",
"nodeInfo": {
"path": "task_in_workflow@1/intake_agent@1"
},
"isolationScope": "task_in_workflow@1/intake_agent@1"
},
{
"content": {
"parts": [
{
"text": "555-1234"
}
],
"role": "user"
},
"author": "user",
"nodeInfo": {
"path": ""
}
},
{
"content": {
"parts": [
{
"functionCall": {
"id": "fc-1",
"args": {
"name": "Jane Doe",
"phone_number": "555-1234"
},
"name": "finish_task"
}
}
],
"role": "model"
},
"author": "intake_agent",
"nodeInfo": {
"path": "task_in_workflow@1/intake_agent@1"
},
"isolationScope": "task_in_workflow@1/intake_agent@1"
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "finish_task",
"response": {
"result": "Task completed."
}
}
}
],
"role": "user"
},
"author": "intake_agent",
"output": {
"name": "Jane Doe",
"phone_number": "555-1234"
},
"nodeInfo": {
"path": "task_in_workflow@1/intake_agent@1",
"outputFor": [
"task_in_workflow@1/intake_agent@1"
]
},
"isolationScope": "task_in_workflow@1/intake_agent@1"
},
{
"content": {
"parts": [
{
"text": "Hello Jane Doe! Let me look up your orders."
}
],
"role": "user"
},
"author": "task_in_workflow",
"nodeInfo": {
"path": "task_in_workflow@1/check_identity@1"
}
},
{
"content": {
"parts": [
{
"functionCall": {
"id": "fc-2",
"args": {},
"name": "find_orders"
}
}
],
"role": "model"
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
}
},
{
"content": {
"parts": [
{
"functionCall": {
"id": "fc-3",
"args": {
"originalFunctionCall": {
"id": "fc-2",
"args": {},
"name": "find_orders"
},
"toolConfirmation": {
"hint": "Please approve or reject the tool call find_orders() by responding with a FunctionResponse with an expected ToolConfirmation payload.",
"confirmed": false
}
},
"name": "adk_request_confirmation"
}
}
]
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
},
"longRunningToolIds": [
"fc-3"
]
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-2",
"name": "find_orders",
"response": {
"error": "This tool call requires confirmation, please approve or reject."
}
}
}
],
"role": "user"
},
"author": "generate_instruction",
"actions": {
"skipSummarization": true,
"requestedToolConfirmations": {
"fc-2": {
"hint": "Please approve or reject the tool call find_orders() by responding with a FunctionResponse with an expected ToolConfirmation payload.",
"confirmed": false
}
}
},
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
}
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-3",
"name": "adk_request_confirmation",
"response": {
"confirmed": true,
"payload": {}
}
}
}
],
"role": "user"
},
"author": "user",
"nodeInfo": {
"path": ""
}
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-2",
"name": "find_orders",
"response": {
"result": [
"CBC (Complete Blood Count)",
"Lipid Panel"
]
}
}
}
],
"role": "user"
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
}
},
{
"content": {
"parts": [
{
"text": "Here are the orders found: CBC (Complete Blood Count) and Lipid Panel.\n\nPlease fast for 9-12 hours before your appointment for the Lipid Panel. You may drink water during this time."
}
],
"role": "model"
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1",
"outputFor": [
"task_in_workflow@1/generate_instruction@1",
"task_in_workflow@1"
],
"messageAsOutput": true
}
}
]
}
@@ -0,0 +1,224 @@
{
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "I am Jane Doe, my phone number is 555-1234"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"content": {
"parts": [
{
"functionCall": {
"id": "fc-1",
"args": {
"name": "Jane Doe",
"phone_number": "555-1234"
},
"name": "finish_task"
}
}
],
"role": "model"
},
"author": "intake_agent",
"nodeInfo": {
"path": "task_in_workflow@1/intake_agent@1"
},
"isolationScope": "task_in_workflow@1/intake_agent@1"
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "finish_task",
"response": {
"result": "Task completed."
}
}
}
],
"role": "user"
},
"author": "intake_agent",
"output": {
"name": "Jane Doe",
"phone_number": "555-1234"
},
"nodeInfo": {
"path": "task_in_workflow@1/intake_agent@1",
"outputFor": [
"task_in_workflow@1/intake_agent@1"
]
},
"isolationScope": "task_in_workflow@1/intake_agent@1"
},
{
"content": {
"parts": [
{
"text": "Hello Jane Doe! Let me look up your orders."
}
],
"role": "user"
},
"author": "task_in_workflow",
"nodeInfo": {
"path": "task_in_workflow@1/check_identity@1"
}
},
{
"content": {
"parts": [
{
"functionCall": {
"id": "fc-2",
"args": {},
"name": "find_orders"
}
}
],
"role": "model"
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
}
},
{
"content": {
"parts": [
{
"functionCall": {
"id": "fc-3",
"args": {
"originalFunctionCall": {
"id": "fc-2",
"args": {},
"name": "find_orders"
},
"toolConfirmation": {
"hint": "Please approve or reject the tool call find_orders() by responding with a FunctionResponse with an expected ToolConfirmation payload.",
"confirmed": false
}
},
"name": "adk_request_confirmation"
}
}
]
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
},
"longRunningToolIds": [
"fc-3"
]
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-2",
"name": "find_orders",
"response": {
"error": "This tool call requires confirmation, please approve or reject."
}
}
}
],
"role": "user"
},
"author": "generate_instruction",
"actions": {
"skipSummarization": true,
"requestedToolConfirmations": {
"fc-2": {
"hint": "Please approve or reject the tool call find_orders() by responding with a FunctionResponse with an expected ToolConfirmation payload.",
"confirmed": false
}
}
},
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
}
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-3",
"name": "adk_request_confirmation",
"response": {
"confirmed": true,
"payload": {}
}
}
}
],
"role": "user"
},
"author": "user",
"nodeInfo": {
"path": ""
}
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-2",
"name": "find_orders",
"response": {
"result": [
"CBC (Complete Blood Count)",
"Lipid Panel"
]
}
}
}
],
"role": "user"
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
}
},
{
"content": {
"parts": [
{
"text": "The patient has orders for a CBC (Complete Blood Count) and a Lipid Panel. Please fast for 9-12 hours prior to the tests."
}
],
"role": "model"
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1",
"outputFor": [
"task_in_workflow@1/generate_instruction@1",
"task_in_workflow@1"
],
"messageAsOutput": true
}
}
]
}
@@ -0,0 +1,253 @@
{
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "My phone number is 555-1234"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"content": {
"parts": [
{
"text": "Thanks! What is your full name?"
}
],
"role": "model"
},
"author": "intake_agent",
"nodeInfo": {
"path": "task_in_workflow@1/intake_agent@1"
},
"isolationScope": "task_in_workflow@1/intake_agent@1"
},
{
"content": {
"parts": [
{
"text": "Jane Doe"
}
],
"role": "user"
},
"author": "user",
"nodeInfo": {
"path": ""
}
},
{
"content": {
"parts": [
{
"functionCall": {
"id": "fc-1",
"args": {
"name": "Jane Doe",
"phone_number": "555-1234"
},
"name": "finish_task"
}
}
],
"role": "model"
},
"author": "intake_agent",
"nodeInfo": {
"path": "task_in_workflow@1/intake_agent@1"
},
"isolationScope": "task_in_workflow@1/intake_agent@1"
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "finish_task",
"response": {
"result": "Task completed."
}
}
}
],
"role": "user"
},
"author": "intake_agent",
"output": {
"name": "Jane Doe",
"phone_number": "555-1234"
},
"nodeInfo": {
"path": "task_in_workflow@1/intake_agent@1",
"outputFor": [
"task_in_workflow@1/intake_agent@1"
]
},
"isolationScope": "task_in_workflow@1/intake_agent@1"
},
{
"content": {
"parts": [
{
"text": "Hello Jane Doe! Let me look up your orders."
}
],
"role": "user"
},
"author": "task_in_workflow",
"nodeInfo": {
"path": "task_in_workflow@1/check_identity@1"
}
},
{
"content": {
"parts": [
{
"functionCall": {
"id": "fc-2",
"args": {},
"name": "find_orders"
}
}
],
"role": "model"
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
}
},
{
"content": {
"parts": [
{
"functionCall": {
"id": "fc-3",
"args": {
"originalFunctionCall": {
"id": "fc-2",
"args": {},
"name": "find_orders"
},
"toolConfirmation": {
"hint": "Please approve or reject the tool call find_orders() by responding with a FunctionResponse with an expected ToolConfirmation payload.",
"confirmed": false
}
},
"name": "adk_request_confirmation"
}
}
]
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
},
"longRunningToolIds": [
"fc-3"
]
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-2",
"name": "find_orders",
"response": {
"error": "This tool call requires confirmation, please approve or reject."
}
}
}
],
"role": "user"
},
"author": "generate_instruction",
"actions": {
"skipSummarization": true,
"requestedToolConfirmations": {
"fc-2": {
"hint": "Please approve or reject the tool call find_orders() by responding with a FunctionResponse with an expected ToolConfirmation payload.",
"confirmed": false
}
}
},
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
}
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-3",
"name": "adk_request_confirmation",
"response": {
"confirmed": true,
"payload": {}
}
}
}
],
"role": "user"
},
"author": "user",
"nodeInfo": {
"path": ""
}
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-2",
"name": "find_orders",
"response": {
"result": [
"CBC (Complete Blood Count)",
"Lipid Panel"
]
}
}
}
],
"role": "user"
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
}
},
{
"content": {
"parts": [
{
"text": "The patient has the following orders: CBC (Complete Blood Count) and Lipid Panel.\n\nPlease fast for 8-12 hours before your appointment for the Lipid Panel. You may drink water."
}
],
"role": "model"
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1",
"outputFor": [
"task_in_workflow@1/generate_instruction@1",
"task_in_workflow@1"
],
"messageAsOutput": true
}
}
]
}
@@ -0,0 +1,349 @@
{
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "My name is John Doe"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"content": {
"parts": [
{
"text": "Thanks, John. What's your phone number?"
}
],
"role": "model"
},
"author": "intake_agent",
"nodeInfo": {
"path": "task_in_workflow@1/intake_agent@1"
},
"isolationScope": "task_in_workflow@1/intake_agent@1"
},
{
"content": {
"parts": [
{
"text": "555-1234"
}
],
"role": "user"
},
"author": "user",
"nodeInfo": {
"path": ""
}
},
{
"content": {
"parts": [
{
"functionCall": {
"id": "fc-1",
"args": {
"name": "John Doe",
"phone_number": "555-1234"
},
"name": "finish_task"
}
}
],
"role": "model"
},
"author": "intake_agent",
"nodeInfo": {
"path": "task_in_workflow@1/intake_agent@1"
},
"isolationScope": "task_in_workflow@1/intake_agent@1"
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "finish_task",
"response": {
"result": "Task completed."
}
}
}
],
"role": "user"
},
"author": "intake_agent",
"output": {
"name": "John Doe",
"phone_number": "555-1234"
},
"nodeInfo": {
"path": "task_in_workflow@1/intake_agent@1",
"outputFor": [
"task_in_workflow@1/intake_agent@1"
]
},
"isolationScope": "task_in_workflow@1/intake_agent@1"
},
{
"content": {
"parts": [
{
"text": "Could not find matching records for John Doe. Let's try again."
}
],
"role": "user"
},
"author": "task_in_workflow",
"actions": {
"route": "retry"
},
"nodeInfo": {
"path": "task_in_workflow@1/check_identity@1"
}
},
{
"content": {
"parts": [
{
"text": "Could not find matching records for John Doe. Let's try again. What other name can I use?"
}
],
"role": "model"
},
"author": "intake_agent",
"nodeInfo": {
"path": "task_in_workflow@1/intake_agent@2"
},
"isolationScope": "task_in_workflow@1/intake_agent@2"
},
{
"content": {
"parts": [
{
"text": "Jane Doe, 555-1234"
}
],
"role": "user"
},
"author": "user",
"nodeInfo": {
"path": ""
}
},
{
"content": {
"parts": [
{
"functionCall": {
"id": "fc-2",
"args": {
"name": "Jane Doe",
"phone_number": "555-1234"
},
"name": "finish_task"
}
}
],
"role": "model"
},
"author": "intake_agent",
"nodeInfo": {
"path": "task_in_workflow@1/intake_agent@2"
},
"isolationScope": "task_in_workflow@1/intake_agent@2"
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-2",
"name": "finish_task",
"response": {
"result": "Task completed."
}
}
}
],
"role": "user"
},
"author": "intake_agent",
"output": {
"name": "Jane Doe",
"phone_number": "555-1234"
},
"nodeInfo": {
"path": "task_in_workflow@1/intake_agent@2",
"outputFor": [
"task_in_workflow@1/intake_agent@2"
]
},
"isolationScope": "task_in_workflow@1/intake_agent@2"
},
{
"content": {
"parts": [
{
"text": "Hello Jane Doe! Let me look up your orders."
}
],
"role": "user"
},
"author": "task_in_workflow",
"nodeInfo": {
"path": "task_in_workflow@1/check_identity@2"
}
},
{
"content": {
"parts": [
{
"functionCall": {
"id": "fc-3",
"args": {},
"name": "find_orders"
}
}
],
"role": "model"
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
}
},
{
"content": {
"parts": [
{
"functionCall": {
"id": "fc-4",
"args": {
"originalFunctionCall": {
"id": "fc-3",
"args": {},
"name": "find_orders"
},
"toolConfirmation": {
"hint": "Please approve or reject the tool call find_orders() by responding with a FunctionResponse with an expected ToolConfirmation payload.",
"confirmed": false
}
},
"name": "adk_request_confirmation"
}
}
]
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
},
"longRunningToolIds": [
"fc-4"
]
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-3",
"name": "find_orders",
"response": {
"error": "This tool call requires confirmation, please approve or reject."
}
}
}
],
"role": "user"
},
"author": "generate_instruction",
"actions": {
"skipSummarization": true,
"requestedToolConfirmations": {
"fc-3": {
"hint": "Please approve or reject the tool call find_orders() by responding with a FunctionResponse with an expected ToolConfirmation payload.",
"confirmed": false
}
}
},
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
}
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-4",
"name": "adk_request_confirmation",
"response": {
"confirmed": true,
"payload": {}
}
}
}
],
"role": "user"
},
"author": "user",
"nodeInfo": {
"path": ""
}
},
{
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-3",
"name": "find_orders",
"response": {
"result": [
"CBC (Complete Blood Count)",
"Lipid Panel"
]
}
}
}
],
"role": "user"
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1"
}
},
{
"content": {
"parts": [
{
"text": "Here are your orders:\n* CBC (Complete Blood Count)\n* Lipid Panel\n\nTo prepare for these orders, you will need to fast for 9-12 hours prior to your appointment. You may drink water during this time."
}
],
"role": "model"
},
"author": "generate_instruction",
"nodeInfo": {
"path": "task_in_workflow@1/generate_instruction@1",
"outputFor": [
"task_in_workflow@1/generate_instruction@1",
"task_in_workflow@1"
],
"messageAsOutput": true
}
}
]
}
@@ -0,0 +1,111 @@
# ADK Workflow Auth Config Sample
## Overview
This sample demonstrates how to use `auth_config` on a `FunctionNode` to require user authentication before the node runs.
When a node has `auth_config`, the workflow automatically:
1. Pauses the node and emits an `adk_request_credential` FunctionCall event
1. The invocation ends — the node is marked as waiting
1. The client sends a new request with the credential as a FunctionResponse
1. The workflow stores the credential in session state and re-runs the node
The **ADK web UI** (`adk web`) handles step 3 automatically — it recognizes auth
requests and presents an auth dialog. If you use a custom client, you need to
handle the `adk_request_credential` FunctionCall and respond with the credential
yourself.
This sample uses **API key** authentication (the simplest credential type).
## No External Setup Required
This sample uses a mock weather lookup. No external API key or server is needed. When the auth UI prompts for a key, you can enter any value (e.g., `my-test-key-123`).
## Sample Inputs
Send any message (e.g., `go`) to start the workflow.
## Graph
```mermaid
graph TD
START --> fetch_weather[fetch_weather <br/>pauses for auth on first run]
fetch_weather --> summarize
```
## How To
1. Define an `AuthConfig` with the auth scheme and credential type:
```python
from google.adk.auth.auth_tool import AuthConfig
from google.adk.auth.auth_credential import AuthCredential, AuthCredentialTypes
auth_config = AuthConfig(
auth_scheme=APIKey(**{'in': APIKeyIn.header, 'name': 'X-Api-Key'}),
raw_auth_credential=AuthCredential(
auth_type=AuthCredentialTypes.API_KEY,
api_key='placeholder',
),
credential_key='weather_api_key',
)
```
1. Use the `@node` decorator with `auth_config` and `rerun_on_resume=True`:
```python
@node(auth_config=auth_config, rerun_on_resume=True)
def fetch_weather(ctx: Context):
...
```
1. Inside the function, retrieve the credential from `ctx`:
```python
def fetch_weather(ctx: Context):
cred = ctx.get_auth_response(auth_config)
api_key = cred.api_key
# Use api_key to call your API...
```
## OAuth2
The same `auth_config` pattern works with OAuth2 and OpenID Connect. The key
differences:
- **Auth scheme**: Use `OAuth2` (from `fastapi.openapi.models`) instead of
`APIKey`. Configure the authorization and token URLs in the OAuth2 flows.
- **Raw credential**: Set `auth_type=AuthCredentialTypes.OAUTH2` and provide
`client_id`, `client_secret`, and `redirect_uri` in the `oauth2` field.
- **Web UI flow**: The ADK web UI recognizes OAuth2 auth requests and opens
an authorization popup automatically. The user authenticates with the
provider, and the UI sends the full `AuthConfig` response back. No special
handling is needed in the node.
- **Token exchange**: The framework automatically exchanges the authorization
code for an access token via `AuthHandler.exchange_auth_token()`.
```python
from fastapi.openapi.models import OAuth2, OAuthFlowAuthorizationCode, OAuthFlows
auth_config = AuthConfig(
auth_scheme=OAuth2(
flows=OAuthFlows(
authorizationCode=OAuthFlowAuthorizationCode(
authorizationUrl='https://provider.com/authorize',
tokenUrl='https://provider.com/token',
scopes={'read': 'Read access'},
)
)
),
raw_auth_credential=AuthCredential(
auth_type=AuthCredentialTypes.OAUTH2,
oauth2=OAuth2Auth(
client_id='YOUR_CLIENT_ID',
client_secret='YOUR_CLIENT_SECRET',
redirect_uri='http://localhost:8000/callback',
),
),
credential_key='my_oauth_credential',
)
```
@@ -0,0 +1,83 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Auth API Key sample: FunctionNode with API key authentication.
Demonstrates how to use `auth_config` on a FunctionNode to pause
the workflow and request user credentials before running the node.
Flow:
1. User sends any message to start the workflow.
2. The `fetch_weather` node pauses and requests an API key.
3. The user provides the API key through the auth UI.
4. The node runs with the credential available in session state.
5. The `summarize` node displays the result.
"""
from fastapi.openapi.models import APIKey
from fastapi.openapi.models import APIKeyIn
from google.adk import Event
from google.adk import Workflow
from google.adk.agents.context import Context
from google.adk.auth.auth_credential import AuthCredential
from google.adk.auth.auth_credential import AuthCredentialTypes
from google.adk.auth.auth_tool import AuthConfig
from google.adk.workflow import node
# --- Auth configuration ---
# Uses API key auth: the simplest credential type.
# The user will be prompted to provide an API key via the auth UI.
auth_config = AuthConfig(
auth_scheme=APIKey(**{'in': APIKeyIn.header, 'name': 'X-Api-Key'}),
raw_auth_credential=AuthCredential(
auth_type=AuthCredentialTypes.API_KEY,
api_key='placeholder',
),
credential_key='weather_api_key',
)
@node(auth_config=auth_config, rerun_on_resume=True)
def fetch_weather(ctx: Context):
"""Fetches weather data using the authenticated API key."""
# After auth completes, the credential is available via ctx.
cred = ctx.get_auth_response(auth_config)
api_key = cred.api_key if cred else 'unknown'
# In a real agent, you would use the api_key to call an external API.
# For this sample, we just echo it back (masked).
masked = api_key[:4] + '****' if len(api_key) > 4 else '****'
return {
'city': 'San Francisco',
'temperature': '18C',
'condition': 'Sunny',
'api_key_used': masked,
}
def summarize(node_input: dict):
"""Displays the weather result."""
yield Event(
message=(
f"Weather for {node_input['city']}:"
f" {node_input['temperature']}, {node_input['condition']}."
f" (Authenticated with key: {node_input['api_key_used']})"
)
)
root_agent = Workflow(
name='auth_api_key',
edges=[('START', fetch_weather, summarize)],
)
@@ -0,0 +1,121 @@
{
"appName": "auth_api_key",
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "go"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "auth_api_key",
"content": {
"parts": [
{
"functionCall": {
"args": {
"authConfig": {
"authScheme": {
"in": "header",
"name": "X-Api-Key",
"type": "apiKey"
},
"credentialKey": "weather_api_key",
"rawAuthCredential": {
"apiKey": "placeholder",
"authType": "apiKey"
}
},
"functionCallId": "fc-1",
"message": "Please provide your API key for X-Api-Key."
},
"id": "fc-1",
"name": "adk_request_credential"
}
}
],
"role": "model"
},
"id": "e-2",
"invocationId": "i-1",
"longRunningToolIds": [
"fc-1"
],
"nodeInfo": {
"path": "auth_api_key@1/fetch_weather@1"
}
},
{
"author": "user",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "adk_request_credential",
"response": {
"result": "12345678"
}
}
}
],
"role": "user"
},
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "auth_api_key",
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"auth_api_key@1/fetch_weather@1"
],
"path": "auth_api_key@1/fetch_weather@1"
},
"output": {
"api_key_used": "1234****",
"city": "San Francisco",
"condition": "Sunny",
"temperature": "18C"
}
},
{
"author": "auth_api_key",
"content": {
"parts": [
{
"text": "Weather for San Francisco: 18C, Sunny. (Authenticated with key: 1234****)"
}
],
"role": "user"
},
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"path": "auth_api_key@1/summarize@1"
}
}
],
"id": "3bb54ead-8f56-468e-b64f-f0d534a66c69",
"state": {
"__session_metadata__": {
"displayName": "go"
}
},
"userId": "user"
}
@@ -0,0 +1,112 @@
# GitHub OAuth Authentication Sample
## Overview
This sample demonstrates how to use `AuthConfig` with GitHub OAuth2 on a `FunctionNode` in a workflow. It shows how to pause execution to request a GitHub OAuth token from the user and then use that token to list the user's owned repositories via the GitHub API.
## Prerequisites
To run this sample and actually log in, you need to:
1. **Register an OAuth Application on GitHub**:
- Go to your GitHub account settings.
- Navigate to **Developer settings** > **OAuth Apps** > **New OAuth App**.
- Set the **Homepage URL** and **Authorization callback URL** appropriate for your testing environment (e.g., `http://localhost:8000` if running locally).
1. **Get Credentials**:
- Copy the **Client ID** and **Client Secret**.
1. **Configure Environment Variables**:
- Set the following environment variables in your terminal before running the sample:
```bash
export GITHUB_CLIENT_ID="your_actual_client_id"
export GITHUB_CLIENT_SECRET="your_actual_client_secret"
```
- Alternatively, you can create a `.env` file in the sample directory (`contributing/workflow_samples/auth_oauth/.env`) with the following content:
```env
GITHUB_CLIENT_ID="your_actual_client_id"
GITHUB_CLIENT_SECRET="your_actual_client_secret"
```
The ADK CLI automatically loads `.env` files from the agent directory.
## Sample Inputs
- `start`
- `list my repos`
## Graph
```mermaid
graph TD
START --> list_github_repos
list_github_repos --> display_result
```
## How To
### 1. Define the AuthConfig for GitHub
We define an `AuthConfig` that specifies the GitHub OAuth2 endpoints and reads credentials from environment variables.
```python
auth_config = AuthConfig(
auth_scheme=OAuth2(
flows=OAuthFlows(
authorizationCode=OAuthFlowAuthorizationCode(
authorizationUrl="https://github.com/login/oauth/authorize",
tokenUrl="https://github.com/login/oauth/access_token",
scopes={
"user": "Read user profile",
"repo": "Access public repositories",
},
)
)
),
raw_auth_credential=AuthCredential(
auth_type=AuthCredentialTypes.OAUTH2,
oauth2=OAuth2Auth(
client_id=os.environ.get("GITHUB_CLIENT_ID", "YOUR_GITHUB_CLIENT_ID"),
client_secret=os.environ.get("GITHUB_CLIENT_SECRET", "YOUR_GITHUB_CLIENT_SECRET"),
),
),
credential_key="github_oauth_token",
)
```
### 2. Apply to a Node
We apply the `auth_config` to the `list_github_repos` node.
```python
@node(auth_config=auth_config, rerun_on_resume=True)
def list_github_repos(ctx: Context):
# ...
```
### 3. Call GitHub API
Inside the node, we retrieve the token and use the `requests` library to call the GitHub API.
```python
cred = ctx.get_auth_response(auth_config)
access_token = cred.oauth2.access_token if cred and cred.oauth2 else None
# ... (headers setup) ...
response = requests.get("https://api.github.com/user/repos", headers=headers)
repos_data = response.json()
repo_names = [repo["name"] for repo in repos_data]
```
## Running the Sample
To run this sample interactively, use the ADK CLI:
```bash
adk run contributing/workflow_samples/auth_oauth
```
Or use the Web UI:
```bash
adk web contributing/workflow_samples/
```
@@ -0,0 +1,15 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from . import agent
@@ -0,0 +1,135 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""OAuth Authentication sample: FunctionNode with GitHub OAuth2 token request.
Demonstrates how to use `auth_config` with GitHub OAuth2 on a FunctionNode to pause
the workflow, request an OAuth token from the user, and use it to list the user's
GitHub repositories.
Flow:
1. User sends any message to start the workflow.
2. The `list_github_repos` node pauses and requests GitHub OAuth credentials.
3. The user provides the credentials (after logging in to GitHub).
4. The node runs, calls the GitHub API to list repos, and returns the list.
5. The `display_result` node displays the repository names.
Sample queries:
- "start"
- "list my repos"
"""
import os
from fastapi.openapi.models import OAuth2
from fastapi.openapi.models import OAuthFlowAuthorizationCode
from fastapi.openapi.models import OAuthFlows
from google.adk import Event
from google.adk import Workflow
from google.adk.agents.context import Context
from google.adk.auth.auth_credential import AuthCredential
from google.adk.auth.auth_credential import AuthCredentialTypes
from google.adk.auth.auth_credential import OAuth2Auth
from google.adk.auth.auth_tool import AuthConfig
from google.adk.workflow import node
import requests
# --- Auth configuration ---
# Uses GitHub OAuth2 authorization code flow.
# To use this sample, you need to register an OAuth application on GitHub
# and get a Client ID and Client Secret.
# Set the GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET environment variables.
auth_config = AuthConfig(
auth_scheme=OAuth2(
flows=OAuthFlows(
authorizationCode=OAuthFlowAuthorizationCode(
authorizationUrl="https://github.com/login/oauth/authorize",
tokenUrl="https://github.com/login/oauth/access_token",
scopes={
"user": "Read user profile",
"repo": "Access public repositories",
},
)
)
),
raw_auth_credential=AuthCredential(
auth_type=AuthCredentialTypes.OAUTH2,
oauth2=OAuth2Auth(
client_id=os.environ.get(
"GITHUB_CLIENT_ID", "YOUR_GITHUB_CLIENT_ID"
),
client_secret=os.environ.get(
"GITHUB_CLIENT_SECRET", "YOUR_GITHUB_CLIENT_SECRET"
),
),
),
credential_key="github_oauth_token",
)
@node(auth_config=auth_config, rerun_on_resume=True)
def list_github_repos(ctx: Context):
"""Fetches GitHub repositories for the authenticated user."""
# After auth completes, the credential is available via ctx.
cred = ctx.get_auth_response(auth_config)
access_token = cred.oauth2.access_token if cred and cred.oauth2 else None
if not access_token:
return {"status": "Error", "message": "No access token found"}
# GitHub API requires a User-Agent header
headers = {
"Authorization": f"Bearer {access_token}",
"User-Agent": "ADK-Sample-Agent",
"Accept": "application/json",
}
try:
response = requests.get(
"https://api.github.com/user/repos", headers=headers
)
response.raise_for_status()
repos_data = response.json()
# Extract repo names
repo_names = [repo["name"] for repo in repos_data]
return {
"status": "Success",
"repos": repo_names,
}
except Exception as e:
return {
"status": "Error",
"message": f"Failed to fetch repos: {e}",
}
def display_result(node_input: dict):
"""Displays the result of accessing the resource."""
if node_input["status"] == "Success":
repos_str = ", ".join(node_input["repos"])
yield Event(message=f"Successfully fetched repositories: {repos_str}")
else:
yield Event(
message=(
"Failed to fetch repositories. Error:"
f" {node_input.get('message', 'Unknown error')}"
)
)
root_agent = Workflow(
name="auth_oauth",
edges=[("START", list_github_repos, display_result)],
)
@@ -0,0 +1,61 @@
# Dynamic Fan-Out / Fan-In with Dynamic Nodes
## Overview
This sample demonstrates how to perform **Dynamic Fan-Out and Fan-In** using ADK's dynamic node scheduling (`ctx.run_node()`).
Unlike static graph-based parallel execution (which requires pre-defined branches), this pattern allows you to determine the number of parallel tasks at runtime based on the input data.
## Sample Inputs
- `AI, Cloud Computing, Quantum Computing`
- `Python, Go, Rust, TypeScript`
## Graph
```mermaid
graph TD
START --> Orchestrator
Orchestrator --> Gen_0[Generator Task 0]
Orchestrator --> Gen_1[Generator Task 1]
Orchestrator --> Gen_N[Generator Task N]
Gen_0 --> Aggregator[Orchestrator Fan-In]
Gen_1 --> Aggregator
Gen_N --> Aggregator
```
## How To
Key techniques demonstrated in this sample:
1. **Dynamic Scheduling**: Using a loop to create tasks via `ctx.run_node()`.
1. **Context Isolation**: Using `sub_branch` in `run_node` to isolate events for each parallel task, preventing context contamination.
1. **`rerun_on_resume=True`**: Required on the orchestrator node to support resumption if any child node interrupts.
### Code Snippet
```python
# Fan-out: Schedule a dynamic node for each topic
tasks = []
for i, topic in enumerate(topics):
tasks.append(
ctx.run_node(
generator,
node_input=topic,
sub_branch=f"branch_{i}"
)
)
# Wait for all tasks to complete
results = await asyncio.gather(*tasks)
```
## Pro Tip: Custom `run_id`
ADK auto-generates numeric IDs (e.g., `@1`), but you can pass a custom `run_id` to improve log readability (e.g., `generator@task_AI`) or map events to business keys.
**Rules**:
- **Unique**: Must be unique per node for fresh executions (otherwise returns cached results).
- **Non-Numeric**: Must contain non-numeric characters to avoid collision with auto-generated IDs.
@@ -0,0 +1,69 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import asyncio
from google.adk import Agent
from google.adk import Context
from google.adk import Event
from google.adk import Workflow
from google.adk.workflow import node
# Worker agent to generate a headline for a single topic
generator = Agent(
name="generator",
instruction=(
"Write a catchy one-line headline about the topic provided in the user"
" message."
),
)
@node(rerun_on_resume=True)
async def orchestrator(ctx: Context, node_input: str) -> str:
"""Orchestrator node that performs dynamic fan-out and fan-in."""
# Split input comma-separated string into topics
topics = [t.strip() for t in node_input.split(",") if t.strip()]
yield Event(message=f"Processing {len(topics)} topics in parallel.")
# Fan-out: Schedule a dynamic node for each topic
tasks = []
for i, topic in enumerate(topics):
tasks.append(
ctx.run_node(
generator,
node_input=topic,
use_sub_branch=True,
)
)
# Wait for all tasks to complete
results = await asyncio.gather(*tasks)
# Fan-in: Aggregate results
aggregated = "### Aggregated Headlines\n\n"
aggregated += "| Topic | Headline |\n"
aggregated += "| :--- | :--- |\n"
for topic, headline in zip(topics, results):
aggregated += f"| {topic} | {headline} |\n"
yield Event(message=aggregated)
root_agent = Workflow(
name="dynamic_fan_out_fan_in",
edges=[("START", orchestrator)],
)
@@ -0,0 +1,59 @@
# ADK Workflow Dynamic Node Execution Sample
## Overview
This sample demonstrates how to use `ctx.run_node` to execute nodes dynamically during workflow execution in **ADK Workflows**.
In standard workflow execution, the execution path is defined statically by the `edges`. However, there are scenarios where the exact nodes, or the number of times a node runs, cannot be determined until runtime.
In this sample, we handle the dynamic loop scenario: an `orchestrate` Python node acts as the driver. It uses a `while True:` loop to first execute a `generate_headline` agent to create a headline based on a given topic, and then an `evaluate_headline` agent to grade it. If the grade is `"tech-related"`, the loop returns the headline. If `"unrelated"`, the feedback is passed back into the state, and the loop repeats.
This is a rewritten version of the standard `loop` sample, achieved without complex graph edge routing (e.g., without conditional routing functions in `edges`), by instead leveraging native Python control flow (`while` loops) combined with asynchronous `ctx.run_node` calls.
## Sample Inputs
- `flower`
- `quantum mechanics`
- `renewable energy`
## Graph
```mermaid
graph TD
START --> orchestrate[orchestrate <br/>PYTHON FUNCTION]
orchestrate -.->|ctx.run_node| generate_headline
generate_headline --> evaluate_headline
evaluate_headline -.-> orchestrate
```
## How To
1. **Enable Resumability**: For a python node to use `ctx.run_node`, it must be declared with `@node(rerun_on_resume=True)`. This tells the engine to pause and possibly re-run the orchestrator if any dynamically scheduled node gets interrupted (e.g., waiting for human-in-the-loop).
```python
from google.adk.workflow import node
@node(rerun_on_resume=True)
async def orchestrate(ctx: Context, node_input: str) -> str:
# ...
```
1. **Run Node from Context**: Inject `ctx: Context` into your python node definition and await `ctx.run_node(node_to_run)`. The return value is the final output of that execution. You can also yield events to update the state within the loop before the next iteration.
```python
@node(rerun_on_resume=True)
async def orchestrate(ctx: Context, node_input: str) -> str:
yield Event(state={"topic": node_input})
while True:
headline = await ctx.run_node(generate_headline)
feedback = Feedback.model_validate(
await ctx.run_node(evaluate_headline, node_input=headline)
)
if feedback.grade == "tech-related":
yield headline
break # or return headline
```
@@ -0,0 +1,78 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Literal
from google.adk import Agent
from google.adk import Context
from google.adk import Event
from google.adk import Workflow
from google.adk.workflow import node
from pydantic import BaseModel
from pydantic import Field
class Feedback(BaseModel):
grade: Literal["tech-related", "unrelated"] = Field(
description=(
"Decide if the headline is related to technology or software"
" engineering."
),
)
feedback: str = Field(
description=(
"If the headline is unrelated to technology, provide feedback on how"
" to make it more tech-focused."
),
)
generate_headline = Agent(
name="generate_headline",
instruction="""
Write a headline about the topic "{topic}".
If feedback is provided, take it into account.
The feedback: {feedback?}
""",
)
evaluate_headline = Agent(
name="evaluate_headline",
instruction="""
Grade whether the headline is related to technology or software engineering.
""",
output_schema=Feedback,
output_key="feedback",
)
@node(rerun_on_resume=True)
async def orchestrate(ctx: Context, node_input: str) -> str:
yield Event(state={"topic": node_input})
while True:
headline = await ctx.run_node(generate_headline)
feedback = Feedback.model_validate(
await ctx.run_node(evaluate_headline, node_input=headline)
)
if feedback.grade == "tech-related":
yield headline
break
root_agent = Workflow(
name="root_agent",
edges=[("START", orchestrate)],
)
@@ -0,0 +1,159 @@
{
"appName": "dynamic_nodes",
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "flower"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"actions": {
"stateDelta": {
"topic": "flower"
}
},
"author": "root_agent",
"id": "e-2",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/orchestrate@1"
}
},
{
"author": "generate_headline",
"content": {
"parts": [
{
"text": "A World of Petals"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/orchestrate@1/generate_headline@1"
],
"path": "root_agent@1/orchestrate@1/generate_headline@1"
}
},
{
"actions": {
"stateDelta": {
"feedback": {
"feedback": "This headline sounds like it's about botany, gardening, or a natural theme. To make it more tech-focused, consider incorporating terms like 'AI', 'software', 'virtual reality', 'digital', 'engineering', 'innovation', 'data', or 'development'. For example, 'The AI-Powered World of Digital Petals' or 'Engineering a Virtual Reality of Petals'.",
"grade": "unrelated"
}
}
},
"author": "evaluate_headline",
"content": {
"parts": [
{
"text": "{\"grade\": \"unrelated\", \"feedback\": \"This headline sounds like it's about botany, gardening, or a natural theme. To make it more tech-focused, consider incorporating terms like 'AI', 'software', 'virtual reality', 'digital', 'engineering', 'innovation', 'data', or 'development'. For example, 'The AI-Powered World of Digital Petals' or 'Engineering a Virtual Reality of Petals'.\"}"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/orchestrate@1/evaluate_headline@1"
],
"path": "root_agent@1/orchestrate@1/evaluate_headline@1"
}
},
{
"author": "generate_headline",
"content": {
"parts": [
{
"text": "**Digital Petals: Engineering AI-Enhanced Blooms**"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/orchestrate@1/generate_headline@2"
],
"path": "root_agent@1/orchestrate@1/generate_headline@2"
}
},
{
"actions": {
"stateDelta": {
"feedback": {
"feedback": "This headline is clearly tech-related, specifically mentioning 'Engineering' and 'AI-Enhanced', which are direct ties to technology and software engineering.",
"grade": "tech-related"
}
}
},
"author": "evaluate_headline",
"content": {
"parts": [
{
"text": "{\"grade\": \"tech-related\", \"feedback\": \"This headline is clearly tech-related, specifically mentioning 'Engineering' and 'AI-Enhanced', which are direct ties to technology and software engineering.\"}"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-6",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/orchestrate@1/evaluate_headline@2"
],
"path": "root_agent@1/orchestrate@1/evaluate_headline@2"
}
},
{
"author": "root_agent",
"id": "e-7",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"root_agent@1/orchestrate@1",
"root_agent@1"
],
"path": "root_agent@1/orchestrate@1"
},
"output": "**Digital Petals: Engineering AI-Enhanced Blooms**"
}
],
"id": "1cf606cf-0c65-4512-96c2-07ccb0181853",
"state": {
"__session_metadata__": {
"displayName": "flower"
},
"feedback": {
"feedback": "This headline is already very tech-focused, mentioning 'AI-Powered' and 'Smart Technology' in the context of innovation.",
"grade": "tech-related"
},
"topic": "flower"
},
"userId": "user"
}
@@ -0,0 +1,59 @@
# ADK Workflow Fan-Out / Fan-In Sample
## Overview
This sample demonstrates how to run multiple nodes in parallel and aggregate their results using a **Fan-Out / Fan-In** pattern in **ADK Workflows**.
It takes an input string and fans out to three different processing functions concurrently: `make_uppercase`, `count_characters`, and `reverse_string`. Instead of independently triggering the downstream node (as seen in the `multi_triggers` sample), this workflow uses a `JoinNode` to wait for all the parallel processes to complete. Once all results are ready, the `JoinNode` packages them into a single dictionary and passes it to an `aggregate` node, which formats the final combined response.
In ADK Workflows, the `JoinNode` is a critical component for synchronizing parallel execution paths, ensuring that a downstream node only executes once all of its required upstream dependencies have furnished their outputs.
## Sample Inputs
- `Hello World`
- `ADK workflows`
- `testing concurrent nodes`
## Graph
```mermaid
graph TD
START --> make_uppercase
START --> count_characters
START --> reverse_string
make_uppercase --> join_node[join_node <br/>Waits for all 3]
count_characters --> join_node
reverse_string --> join_node
join_node --> aggregate
```
## How To
1. Define a `JoinNode` in your code:
```python
from google.adk.workflow import JoinNode
join_node = JoinNode(name="join_for_results")
```
1. In the `Workflow` edges definition, specify a tuple of nodes to fan out execution, followed by your `join_node` to fan in the results, and finally the node that processes the aggregated output:
```python
(
"START",
(make_uppercase, count_characters, reverse_string),
join_node,
aggregate,
)
```
1. The node following the `JoinNode` (in this case, `aggregate`) will receive a `dict` as its input. The keys of this dictionary are the names of the upstream nodes, and the values are their respective outputs:
```python
async def aggregate(node_input: dict[str, Any]):
uppercase_result = node_input['make_uppercase']
# ...
```
@@ -0,0 +1,55 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Any
from google.adk import Event
from google.adk import Workflow
from google.adk.workflow import JoinNode
def make_uppercase(node_input: str):
return node_input.upper()
def count_characters(node_input: str):
return len(node_input)
def reverse_string(node_input: str):
return node_input[::-1]
join_node = JoinNode(name="join_for_results")
async def aggregate(node_input: dict[str, Any]):
yield Event(
message=(
f"Uppercase: {node_input['make_uppercase']}\n\n"
f"Character Count: {node_input['count_characters']}\n\n"
f"Reversed: {node_input['reverse_string']}\n\n"
),
)
root_agent = Workflow(
name="root_agent",
edges=[(
"START",
(make_uppercase, count_characters, reverse_string),
join_node,
aggregate,
)],
)
@@ -0,0 +1,99 @@
{
"appName": "fan_out_fan_in",
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "go"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "root_agent",
"branch": "make_uppercase@1",
"id": "e-2",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"root_agent@1/make_uppercase@1"
],
"path": "root_agent@1/make_uppercase@1"
},
"output": "GO"
},
{
"author": "root_agent",
"branch": "count_characters@1",
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"root_agent@1/count_characters@1"
],
"path": "root_agent@1/count_characters@1"
},
"output": 2
},
{
"author": "root_agent",
"branch": "reverse_string@1",
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"root_agent@1/reverse_string@1"
],
"path": "root_agent@1/reverse_string@1"
},
"output": "og"
},
{
"author": "root_agent",
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"root_agent@1/join_for_results@1"
],
"path": "root_agent@1/join_for_results@1"
},
"output": {
"count_characters": 2,
"make_uppercase": "GO",
"reverse_string": "og"
}
},
{
"author": "root_agent",
"content": {
"parts": [
{
"text": "Uppercase: GO\n\nCharacter Count: 2\n\nReversed: og\n\n"
}
],
"role": "user"
},
"id": "e-6",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/aggregate@1"
}
}
],
"id": "de416178-357c-4f4c-bf32-ff19e39f33db",
"state": {
"__session_metadata__": {
"displayName": "go"
}
},
"userId": "user"
}
@@ -0,0 +1,46 @@
# ADK Workflow Loop Sample
## Overview
This sample demonstrates how to create a feedback loop between different nodes in **ADK Workflows**.
It takes a user-provided topic and uses the `generate_headline` agent to write a headline. The `evaluate_headline` agent then grades the headline as either "tech-related" or "unrelated", providing feedback if it's unrelated. The `route_headline` function checks this grade. If the headline is "unrelated", the workflow loops back to the `generate_headline` agent, passing the feedback so it can try again. This process repeats until a "tech-related" headline is generated.
In ADK Workflows, loops allow for iterative refinement and evaluation by conditionally routing execution back to an earlier node in the sequence.
## Sample Inputs
- `flower`
- `quantum mechanics`
- `renewable energy`
## Graph
```mermaid
graph TD
START --> process_input
process_input --> generate_headline
generate_headline --> evaluate_headline
evaluate_headline --> route_headline
route_headline -->|unrelated| generate_headline
route_headline -->|tech-related| END[Loop ends]
```
## How To
1. Define a node (like `route_headline`) that yields an `Event` with a specific route based on a condition:
```python
def route_headline(node_input: Feedback):
return Event(route=node_input.grade)
```
1. In the `Workflow` edges definition, create a conditional edge that connects the routing node back to a previous node in the workflow, using a routing map dict:
```python
(route_headline, {"unrelated": generate_headline})
```
This creates the cycle. If the route yielded by `route_headline` is "unrelated", execution jumps back to `generate_headline`.
@@ -0,0 +1,80 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Literal
from google.adk import Agent
from google.adk import Event
from google.adk import Workflow
from pydantic import BaseModel
from pydantic import Field
class Feedback(BaseModel):
grade: Literal["tech-related", "unrelated"] = Field(
description=(
"Decide if the headline is related to technology or software"
" engineering."
),
)
feedback: str = Field(
description=(
"If the headline is unrelated to technology, provide feedback on how"
" to make it more tech-focused."
),
)
def process_input(node_input: str):
"""Puts user input in the state."""
return Event(state={"topic": node_input})
generate_headline = Agent(
name="generate_headline",
instruction="""
Write a headline about the topic "{topic}".
If feedback is provided, take it into account.
The feedback: {feedback?}
""",
)
evaluate_headline = Agent(
name="evaluate_headline",
instruction="""
Grade whether the headline is related to technology or software engineering.
""",
output_schema=Feedback,
output_key="feedback",
)
def route_headline(node_input: Feedback):
return Event(route=node_input.grade)
root_agent = Workflow(
name="root_agent",
edges=[
(
"START",
process_input,
generate_headline,
evaluate_headline,
route_headline,
),
(route_headline, {"unrelated": generate_headline}),
],
)
@@ -0,0 +1,94 @@
{
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "computer"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"actions": {
"stateDelta": {
"topic": "computer"
}
},
"author": "root_agent",
"id": "e-2",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/process_input@1"
}
},
{
"author": "generate_headline",
"content": {
"parts": [
{
"text": "Computers: Shaping Our World"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/generate_headline@1"
],
"path": "root_agent@1/generate_headline@1"
}
},
{
"actions": {
"stateDelta": {
"feedback": {
"feedback": "This headline is clearly tech-related as it directly discusses computers.",
"grade": "tech-related"
}
}
},
"author": "evaluate_headline",
"content": {
"parts": [
{
"text": "{\"grade\": \"tech-related\", \"feedback\": \"This headline is clearly tech-related as it directly discusses computers.\"}"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/evaluate_headline@1"
],
"path": "root_agent@1/evaluate_headline@1"
}
},
{
"actions": {
"route": "tech-related"
},
"author": "root_agent",
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/route_headline@1"
}
}
]
}
@@ -0,0 +1,168 @@
{
"appName": "loop",
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "flower"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"actions": {
"stateDelta": {
"topic": "flower"
}
},
"author": "root_agent",
"id": "e-2",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/process_input@1"
}
},
{
"author": "generate_headline",
"content": {
"parts": [
{
"text": "\"Petal Power: The Timeless Allure of Flowers\""
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/generate_headline@1"
],
"path": "root_agent@1/generate_headline@1"
}
},
{
"actions": {
"stateDelta": {
"feedback": {
"feedback": "To make this headline more tech-focused, consider incorporating elements like AI for plant recognition, robotics in gardening, biotechnology in horticulture, or data analytics related to flower cultivation and sales. For example, 'AI-Driven Botany: The Tech Unlocking Petal Power' or 'Smart Gardens: Engineering the Timeless Allure of Flowers'.",
"grade": "unrelated"
}
}
},
"author": "evaluate_headline",
"content": {
"parts": [
{
"text": "{\"grade\": \"unrelated\", \"feedback\": \"To make this headline more tech-focused, consider incorporating elements like AI for plant recognition, robotics in gardening, biotechnology in horticulture, or data analytics related to flower cultivation and sales. For example, 'AI-Driven Botany: The Tech Unlocking Petal Power' or 'Smart Gardens: Engineering the Timeless Allure of Flowers'.\"}"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/evaluate_headline@1"
],
"path": "root_agent@1/evaluate_headline@1"
}
},
{
"actions": {
"route": "unrelated"
},
"author": "root_agent",
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/route_headline@1"
}
},
{
"author": "generate_headline",
"content": {
"parts": [
{
"text": "AI-Powered Petals: The Tech Revolution Blooming in Modern Floriculture"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-6",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/generate_headline@2"
],
"path": "root_agent@1/generate_headline@2"
}
},
{
"actions": {
"stateDelta": {
"feedback": {
"feedback": "This headline is strongly tech-related, explicitly mentioning 'AI-Powered' and 'Tech Revolution'. It effectively combines a traditional field (floriculture) with advanced technology.",
"grade": "tech-related"
}
}
},
"author": "evaluate_headline",
"content": {
"parts": [
{
"text": "{\"grade\": \"tech-related\", \"feedback\": \"This headline is strongly tech-related, explicitly mentioning 'AI-Powered' and 'Tech Revolution'. It effectively combines a traditional field (floriculture) with advanced technology.\"}"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-7",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/evaluate_headline@2"
],
"path": "root_agent@1/evaluate_headline@2"
}
},
{
"actions": {
"route": "tech-related"
},
"author": "root_agent",
"id": "e-8",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/route_headline@2"
}
}
],
"id": "2779014d-3ee3-429e-951d-e69756ddf789",
"state": {
"__session_metadata__": {
"displayName": "flower"
},
"feedback": {
"feedback": "This headline is already strongly tech-focused due to the explicit mention of AI and 'Tech Revolution'.",
"grade": "tech-related"
},
"topic": "flower"
},
"userId": "user"
}
@@ -0,0 +1,48 @@
# Workflow Loop Config Sample
## Overview
This sample demonstrates how to define a workflow with a feedback loop using a YAML configuration file. It mirrors the `workflow_samples/loop` sample, but uses YAML to define the workflow structure instead of Python.
## Sample Inputs
- `Python programming`
- `Baking cookies`
## Graph
```mermaid
graph TD
START --> process_input[process_input]
process_input --> generate_headline[generate_headline.yaml]
generate_headline --> evaluate_headline[evaluate_headline.yaml]
evaluate_headline --> route_headline[route_headline]
route_headline -->|unrelated| generate_headline
```
## How To
This sample uses some special syntax in `root_agent.yaml` to support dynamic resolution and graph construction:
### 1. `_code` Suffix
Fields ending with `_code` (like `output_schema_code` in `evaluate_headline.yaml`) tell the ADK YAML mapper to resolve the value as a Python code reference rather than treating it as a plain string.
- If it starts with `.`, it resolves relative to the current agent directory's Python package path.
- Example: `output_schema_code: .agent.Feedback` resolves to the `Feedback` Pydantic model in `agent.py` in the same directory.
### 2. Function References in Edges
If a string in the edge list does not end with `.yaml` and is not `'START'`, it is treated as a function reference.
- If it starts with `.`, it resolves relative to the current agent directory's Python package path.
- Example: `.agent.process_input` resolves to the `process_input` function in `agent.py`.
- It automatically creates a `FunctionNode` with the function's name as the node name.
### 3. External Agent Files
Agents can be defined in their own YAML files and referenced by filename in the edges list.
- Example: `generate_headline.yaml` references the agent defined in that file.
- The mapper caches resolved nodes by their string value, so using the same filename in multiple edges correctly reuses the same agent instance, preserving the graph structure (e.g. for loops).
@@ -0,0 +1,43 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Literal
from google.adk import Event
from pydantic import BaseModel
from pydantic import Field
class Feedback(BaseModel):
grade: Literal["tech-related", "unrelated"] = Field(
description=(
"Decide if the headline is related to technology or software"
" engineering."
)
)
feedback: str = Field(
description=(
"If the headline is unrelated to technology, provide feedback on how"
" to make it more tech-focused."
)
)
def process_input(node_input: str):
"""Puts user input in the state."""
return Event(state={"topic": node_input})
def route_headline(node_input: Feedback):
return Event(route=node_input.grade)
@@ -0,0 +1,20 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
agent_class: LlmAgent
name: evaluate_headline
instruction: |
Grade whether the headline is related to technology or software engineering.
output_schema_code: .agent.Feedback
output_key: feedback
@@ -0,0 +1,20 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
agent_class: LlmAgent
name: generate_headline
instruction: |
Write a headline about the topic "{topic}".
If feedback is provided, take it into account.
The feedback: {feedback?}
@@ -0,0 +1,24 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
agent_class: Workflow
name: loop_workflow
edges:
- - START
- .agent.process_input
- generate_headline.yaml
- evaluate_headline.yaml
- .agent.route_headline
- - .agent.route_headline
- unrelated: generate_headline.yaml
@@ -0,0 +1,44 @@
# ADK Workflow Loop Self Sample
## Overview
This sample demonstrates how a node can repeatedly loop back to itself based on a specific route condition in **ADK Workflows**.
It takes a user-provided target number (between 0 and 10), and uses a `guess_number` function to randomly generate guesses. If the guess is incorrect, the function yields a specific route (`guessed_wrong`). The workflow is configured such that this route directs the execution right back to the `guess_number` node, creating a loop that continues until the correct number is guessed.
In ADK Workflows, you can create self-referential loops or iterative processes by routing a node's output back to itself.
## Sample Inputs
- `5`
- `0`
- `10`
## Graph
```mermaid
graph TD
START --> validate_input
validate_input --> guess_number
guess_number -->|guessed_wrong| guess_number
guess_number -->|correct| END[Loop ends]
```
## How To
1. From within your node (agent or function), yield a specific `Event` with a route name when you determine the node needs to be executed again:
```python
def guess_number(target_number: int):
# ...
if guess != target_number:
yield Event(route='guessed_wrong')
```
1. In the `Workflow` edges definition, create a conditional edge where the source and target are the same node, using a routing map dict:
```python
(guess_number, {'guessed_wrong': guess_number})
```
@@ -0,0 +1,45 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import random
from google.adk import Event
from google.adk import Workflow
def validate_input(node_input: str):
parsed_number = int(node_input)
if parsed_number > 10 or parsed_number < 0:
yield Event(message='Please provide a number between 0 and 10.')
raise ValueError('Invalid input.')
else:
yield Event(state={'target_number': parsed_number})
def guess_number(target_number: int):
guess = random.randint(0, 10)
yield Event(message=f'Guessing {guess}...')
if guess == target_number:
yield Event(message='Correct!')
else:
yield Event(route='guessed_wrong')
root_agent = Workflow(
name='root_agent',
edges=[
('START', validate_input, guess_number),
(guess_number, {'guessed_wrong': guess_number}),
],
)
@@ -0,0 +1,191 @@
{
"appName": "loop_self",
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "3"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"actions": {
"stateDelta": {
"target_number": 3
}
},
"author": "root_agent",
"id": "e-2",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/validate_input@1"
}
},
{
"author": "root_agent",
"content": {
"parts": [
{
"text": "Guessing 10..."
}
],
"role": "user"
},
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/guess_number@1"
}
},
{
"actions": {
"route": "guessed_wrong"
},
"author": "root_agent",
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/guess_number@1"
}
},
{
"author": "root_agent",
"content": {
"parts": [
{
"text": "Guessing 1..."
}
],
"role": "user"
},
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/guess_number@2"
}
},
{
"actions": {
"route": "guessed_wrong"
},
"author": "root_agent",
"id": "e-6",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/guess_number@2"
}
},
{
"author": "root_agent",
"content": {
"parts": [
{
"text": "Guessing 0..."
}
],
"role": "user"
},
"id": "e-7",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/guess_number@3"
}
},
{
"actions": {
"route": "guessed_wrong"
},
"author": "root_agent",
"id": "e-8",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/guess_number@3"
}
},
{
"author": "root_agent",
"content": {
"parts": [
{
"text": "Guessing 4..."
}
],
"role": "user"
},
"id": "e-9",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/guess_number@4"
}
},
{
"actions": {
"route": "guessed_wrong"
},
"author": "root_agent",
"id": "e-10",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/guess_number@4"
}
},
{
"author": "root_agent",
"content": {
"parts": [
{
"text": "Guessing 3..."
}
],
"role": "user"
},
"id": "e-11",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/guess_number@5"
}
},
{
"author": "root_agent",
"content": {
"parts": [
{
"text": "Correct!"
}
],
"role": "user"
},
"id": "e-12",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/guess_number@5"
}
}
],
"id": "1f627a81-1341-43b7-b971-c8809ff8f6d1",
"mocks": {
"random.randint": [
10,
1,
0,
4,
3
]
},
"state": {
"__session_metadata__": {
"displayName": "3"
},
"target_number": 3
},
"userId": "user"
}
@@ -0,0 +1,81 @@
# ADK Workflow Message Sample
## Overview
This sample demonstrates different ways to send a message to a user using `Event(message=...)` within an **ADK Workflows** node. It covers:
1. **String Messages**: Standard string text replies.
1. **Multi-modal Messages**: Returning mixed modality inputs, such as a string combined with an inline image.
1. **Multiple Messages**: Emitting multiple full messages from the same node with a delay between them.
1. **Streaming Messages**: Simulating an LLM streaming response by breaking a message into chunks and yielding them with the `partial=True` flag at intervals.
## Sample Inputs
This workflow executes sequentially and successfully without any expected user input. Since it has only one `Workflow` node chain that automatically progresses from `START`, you can just type anything (e.g. `start`) to kick it off.
## Graph
```mermaid
graph TD
START --> send_string
send_string --> send_multimodal
send_multimodal --> multiple_messages
multiple_messages --> stream_sentence
stream_sentence --> END[Workflow Ends]
```
## How To
To send messages in an ADK node, yield an `Event` object with the `message` argument:
1. **Send a simple string**:
```python
yield Event(message="Hello world!")
```
1. **Send text with an image** (multi-modal):
```python
from google.genai import types
yield Event(
message=[
types.Part.from_text(text="Look at this image:"),
types.Part.from_bytes(data=image_bytes, mime_type="image/png"),
]
)
```
1. **Send multiple messages**:
To send multiple distinct messages from a single node, yield multiple `Event` objects sequentially.
> **Note**: When yielding multiple messages with delays (`await asyncio.sleep(...)`), your node function **must be an asynchronous generator** (`async def`). This allows ADK to yield each message to the client immediately without blocking.
```python
import asyncio
async def multiple_messages(node_input: Any = None):
yield Event(message="Processing step 1...")
await asyncio.sleep(1.0)
yield Event(message="Processing step 2...")
await asyncio.sleep(1.0)
yield Event(message="Done processing.")
```
1. **Stream a message in chunks**:
Provide the `partial=True` flag for intermediate chunks. This provides a better user experience by allowing the UI to show the response in a streaming fashion, thereby lowering the latency to see the first word. ADK automatically accumulates all partial messages and merges them into a final message for you for session storage.
> **Note**: To stream multiple messages or tokens smoothly, your node function **must be an asynchronous generator** (`async def`). This allows ADK to yield messages to the client immediately without blocking.
```python
import asyncio
async def stream_sentence(node_input: str):
yield Event(message="How ", partial=True)
await asyncio.sleep(0.5)
yield Event(message="may I", partial=True)
await asyncio.sleep(0.5)
yield Event(message=" help you?", partial=True)
```
@@ -0,0 +1,104 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import base64
import os
from typing import Any
from google.adk import Event
from google.adk import Workflow
from google.genai import types
async def sleep_if_not_pytest(seconds: float):
if "PYTEST_CURRENT_TEST" not in os.environ:
await asyncio.sleep(seconds)
def send_string(node_input: Any = None):
"""Sends a single string message."""
yield Event(message="#1 This is a simple string message.")
def send_multimodal(node_input: Any = None):
"""Sends a multi-modal message containing a string and an inline image."""
# A 16x16 solid red PNG base64 encoded
red_square_png = base64.b64decode(
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAXElEQVR4nO2TSQ7AIAwD"
"7fz/z+ZQtapwmrJc8QklmjBIgZJgIZMiAIl9KYbhjx4fgwosbNxgMrF0+4uhgHnYDM6"
"AzQHJeg5HYtyHFfgy2AztN/5tZWfrBtVzkl4DzfQkEPd+cEkAAAAASUVORK5CYII="
)
yield Event(
message=[
types.Part.from_text(
text=(
"#2 Here is a multi-modal message with an inline image (red"
" circle):"
)
),
types.Part.from_bytes(data=red_square_png, mime_type="image/png"),
]
)
async def multiple_messages(node_input: Any = None):
"""Sends multiple complete messages from the same node with an interval."""
yield Event(message="#3 Multiple messages")
await sleep_if_not_pytest(1.0)
yield Event(message="Processing step 1...")
await sleep_if_not_pytest(1.0)
yield Event(message="Processing step 2...")
await sleep_if_not_pytest(1.0)
yield Event(message="Done processing.")
async def stream_sentence(node_input: Any = None):
"""
Demonstrates streaming by sending a sentence in chunks.
The `partial=True` flag tells the UI that this is part of an ongoing message.
"""
yield Event(message="#4 Starting to stream...")
sentence = """\
This is a streaming message sent in chunks.
You can stream in markdown as well. For example, the table below:
| Header 1 | Header 2 |
|----------|----------|
| Cell 1 | Cell 2 |
| Cell 3 | Cell 4 |
"""
for i in range(0, len(sentence), 5):
chunk = sentence[i : i + 5]
yield Event(message=chunk, partial=True)
await sleep_if_not_pytest(0.2)
root_agent = Workflow(
name="message",
edges=[
(
"START",
send_string,
send_multimodal,
multiple_messages,
stream_sentence,
),
],
)
@@ -0,0 +1,146 @@
{
"appName": "message",
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "go"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "message",
"content": {
"parts": [
{
"text": "#1 This is a simple string message."
}
],
"role": "user"
},
"id": "e-2",
"invocationId": "i-1",
"nodeInfo": {
"path": "message@1/send_string@1"
}
},
{
"author": "message",
"content": {
"parts": [
{
"text": "#2 Here is a multi-modal message with an inline image (red circle):"
},
{
"inlineData": {
"data": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8_9hAAAAXElEQVR4nO2TSQ7AIAwD7fz_z-ZQtapwmrJc8QklmjBIgZJgIZMiAIl9KYbhjx4fgwosbNxgMrF0-4uhgHnYDM6AzQHJeg5HYtyHFfgy2AztN_5tZWfrBtVzkl4DzfQkEPd-cEkAAAAASUVORK5CYII=",
"mimeType": "image/png"
}
}
],
"role": "user"
},
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"path": "message@1/send_multimodal@1"
}
},
{
"author": "message",
"content": {
"parts": [
{
"text": "#3 Multiple messages"
}
],
"role": "user"
},
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"path": "message@1/multiple_messages@1"
}
},
{
"author": "message",
"content": {
"parts": [
{
"text": "Processing step 1..."
}
],
"role": "user"
},
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"path": "message@1/multiple_messages@1"
}
},
{
"author": "message",
"content": {
"parts": [
{
"text": "Processing step 2..."
}
],
"role": "user"
},
"id": "e-6",
"invocationId": "i-1",
"nodeInfo": {
"path": "message@1/multiple_messages@1"
}
},
{
"author": "message",
"content": {
"parts": [
{
"text": "Done processing."
}
],
"role": "user"
},
"id": "e-7",
"invocationId": "i-1",
"nodeInfo": {
"path": "message@1/multiple_messages@1"
}
},
{
"author": "message",
"content": {
"parts": [
{
"text": "#4 Starting to stream..."
}
],
"role": "user"
},
"id": "e-8",
"invocationId": "i-1",
"nodeInfo": {
"path": "message@1/stream_sentence@1"
}
}
],
"id": "9cfc0ef6-11d6-4260-84cf-be22731ab69e",
"state": {
"__session_metadata__": {
"displayName": "go"
}
},
"userId": "user"
}
@@ -0,0 +1,53 @@
# ADK Workflow Multi-Triggers Sample
## Overview
This sample demonstrates how a single node can fan out to execute multiple downstream nodes concurrently, and how multiple upstream nodes can trigger a single downstream node independently in **ADK Workflows**.
In this example, the `START` node fans out to three different processing functions (`make_uppercase`, `count_characters`, and `reverse_string`). Each of these functions receives the initial user input string, processes it, and then independently outputs its result.
Because the subsequent `send_message` node receives a continuous flow of outputs and does not use an aggregation mechanism (like `JoinNode`), it is triggered multiple times—once for every upstream event.
## Sample Inputs
- `Hello World`
- `ADK workflows`
- `testing concurrent nodes`
## Graph
```mermaid
graph TD
START --> make_uppercase
START --> count_characters
START --> reverse_string
make_uppercase --> send_message
count_characters --> send_message
reverse_string --> send_message
```
## How To
1. You can specify a tuple of nodes within an edge to create a parallel fan-out segment where the same input is provided to multiple nodes:
```python
(
"START",
(make_uppercase, count_characters, reverse_string),
# ...
)
```
1. By continuing the sequence to another node after the tuple, the outputs of all nodes in the tuple will independently trigger that target node:
```python
(
"START",
(make_uppercase, count_characters, reverse_string),
send_message,
)
```
In this case, `send_message` will be executed once for `make_uppercase`'s output, once for `count_characters`'s output, and once for `reverse_string`'s output.
@@ -0,0 +1,45 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Any
from google.adk import Event
from google.adk import Workflow
def make_uppercase(node_input: str):
return node_input.upper()
def count_characters(node_input: str):
return len(node_input)
def reverse_string(node_input: str):
return node_input[::-1]
async def send_message(node_input: Any):
yield Event(message=f"Triggered for input: {node_input}")
root_agent = Workflow(
name="root_agent",
edges=[(
"START",
(make_uppercase, count_characters, reverse_string),
send_message,
)],
input_schema=str,
)
@@ -0,0 +1,118 @@
{
"appName": "multi_triggers",
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "go"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "root_agent",
"branch": "make_uppercase@1",
"id": "e-2",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"root_agent@1/make_uppercase@1"
],
"path": "root_agent@1/make_uppercase@1"
},
"output": "GO"
},
{
"author": "root_agent",
"branch": "count_characters@1",
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"root_agent@1/count_characters@1"
],
"path": "root_agent@1/count_characters@1"
},
"output": 2
},
{
"author": "root_agent",
"branch": "reverse_string@1",
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"root_agent@1/reverse_string@1"
],
"path": "root_agent@1/reverse_string@1"
},
"output": "og"
},
{
"author": "root_agent",
"branch": "make_uppercase@1",
"content": {
"parts": [
{
"text": "Triggered for input: GO"
}
],
"role": "user"
},
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/send_message@1"
}
},
{
"author": "root_agent",
"branch": "count_characters@1",
"content": {
"parts": [
{
"text": "Triggered for input: 2"
}
],
"role": "user"
},
"id": "e-6",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/send_message@2"
}
},
{
"author": "root_agent",
"branch": "reverse_string@1",
"content": {
"parts": [
{
"text": "Triggered for input: og"
}
],
"role": "user"
},
"id": "e-7",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/send_message@3"
}
}
],
"id": "5cf6403e-fd4e-4fef-ad66-8ae78d26ba29",
"state": {
"__session_metadata__": {
"displayName": "go"
}
},
"userId": "user"
}
@@ -0,0 +1,64 @@
# ADK Workflow Nested Workflow Sample
## Overview
This sample demonstrates how to compose workflows by embedding one workflow inside another as a single node in **ADK Workflows**.
It takes a 4-digit year as input and performs two tasks in parallel:
1. **Historical Event (`find_historical_event`)**: A straightforward Agent node that generates a 2-sentence description of an event that happened that year.
1. **Famous Person (`find_famous_person`)**: A nested Workflow that first finds a person born in that year (`find_name`), and then forwards that name to another agent to write a biography (`generate_bio`).
From the perspective of the `root_agent` workflow, `find_famous_person` is just another node. The root workflow doesn't need to know the internal steps; it just waits for the parallel branches to finish, then synchronizes their outputs using a `JoinNode` before formatting them in `aggregate_results`.
## Sample Inputs
- `1969`
- `2000`
- `1984`
## Graph
### Root Workflow (`root_agent`)
```mermaid
graph TD
START --> process_input
process_input --> find_historical_event[find_historical_event <br/>AGENT]
process_input --> find_famous_person[find_famous_person <br/>WORKFLOW]
find_historical_event --> join_for_aggregation[join_for_aggregation <br/>JOIN]
find_famous_person --> join_for_aggregation
join_for_aggregation --> aggregate_results
```
### Nested Workflow (`find_famous_person`)
```mermaid
graph TD
START --> find_name
find_name --> generate_bio
```
## How To
1. Define your sub-workflow just like any regular workflow. Ensure it accepts the required state (e.g., `year`) and outputs the expected state (e.g., `person_bio`).
```python
find_famous_person = Workflow(
name="find_famous_person",
edges=[("START", find_name, generate_bio)],
)
```
1. Treat the sub-workflow as a normal node when defining the edges of the parent workflow. To run them concurrently, place the nodes in a tuple, then use a `JoinNode` to synchronize their parallel executions before the final aggregation.
```python
root_agent = Workflow(
name="root_agent",
edges=[
("START", process_input, (find_famous_person, find_historical_event), join_for_aggregation, aggregate_results),
],
)
```
@@ -0,0 +1,15 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from . import agent
@@ -0,0 +1,92 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
from google.adk import Agent
from google.adk import Event
from google.adk import Workflow
from google.adk.workflow import JoinNode
def process_input(node_input: str):
"""Validates the input is a valid 4-digit year."""
match = re.search(r"\b\d{4}\b", node_input)
if not match:
yield Event(message="Please provide a valid 4-digit year (e.g., 1955).")
raise ValueError("Invalid year format.")
yield Event(state={"year": match.group(0)})
find_name = Agent(
name="find_name",
instruction="""
Find the name of one famous person who was born in this year: {year}.
Return ONLY their name, nothing else.
""",
)
generate_bio = Agent(
name="generate_bio",
instruction="""
Write a short, engaging 3-sentence biography for the specified person.
""",
)
# Sub-workflow that acts as a single node in the parent workflow
find_famous_person = Workflow(
name="find_famous_person",
edges=[("START", find_name, generate_bio)],
)
find_historical_event = Agent(
name="find_historical_event",
instruction="""
Describe one highly significant historical event that occurred in this year: {year}.
Keep the description to 2 sentences.
""",
)
join_for_aggregation = JoinNode(name="join_for_aggregation")
def aggregate_results(node_input: dict[str, str], year: str):
"""Combines outputs from parallel branches found in context state."""
combined_message = (
f"# Year: {year}\n\n"
"## Famous Person Bio:\n\n"
f"{node_input['find_famous_person']}\n\n"
"## Historical Event:\n\n"
f"{node_input['find_historical_event']}"
)
yield Event(message=combined_message)
root_agent = Workflow(
name="root_agent",
edges=[
(
"START",
process_input,
(find_famous_person, find_historical_event),
join_for_aggregation,
aggregate_results,
),
],
)
@@ -0,0 +1,139 @@
{
"appName": "nested_workflow",
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "1984"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"actions": {
"stateDelta": {
"year": "1984"
}
},
"author": "root_agent",
"id": "e-2",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/process_input@1"
}
},
{
"author": "find_name",
"branch": "find_famous_person@1",
"content": {
"parts": [
{
"text": "Scarlett Johansson"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/find_famous_person@1/find_name@1"
],
"path": "root_agent@1/find_famous_person@1/find_name@1"
}
},
{
"author": "generate_bio",
"branch": "find_famous_person@1",
"content": {
"parts": [
{
"text": "Scarlett Johansson is an acclaimed actress renowned for her distinctive voice and versatile performances across a wide range of genres. From her captivating early roles in films like \"Lost in Translation\" to her iconic portrayal of Black Widow in the Marvel Cinematic Universe, she has consistently delivered powerful performances. A four-time Golden Globe nominee and two-time Academy Award nominee, she remains one of Hollywood's most bankable and respected stars."
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/find_famous_person@1/generate_bio@1",
"root_agent@1/find_famous_person@1"
],
"path": "root_agent@1/find_famous_person@1/generate_bio@1"
}
},
{
"author": "find_historical_event",
"branch": "find_historical_event@1",
"content": {
"parts": [
{
"text": "In December 1984, the Union Carbide chemical plant in Bhopal, India, experienced a catastrophic gas leak, releasing deadly methyl isocyanate. This disaster killed thousands instantly and caused hundreds of thousands of injuries, becoming one of the world's worst industrial accidents and a lasting symbol of corporate negligence."
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/find_historical_event@1"
],
"path": "root_agent@1/find_historical_event@1"
}
},
{
"author": "root_agent",
"id": "e-6",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"root_agent@1/join_for_aggregation@1"
],
"path": "root_agent@1/join_for_aggregation@1"
},
"output": {
"find_famous_person": "Scarlett Johansson is an acclaimed actress renowned for her distinctive voice and versatile performances across a wide range of genres. From her captivating early roles in films like \"Lost in Translation\" to her iconic portrayal of Black Widow in the Marvel Cinematic Universe, she has consistently delivered powerful performances. A four-time Golden Globe nominee and two-time Academy Award nominee, she remains one of Hollywood's most bankable and respected stars.",
"find_historical_event": "In December 1984, the Union Carbide chemical plant in Bhopal, India, experienced a catastrophic gas leak, releasing deadly methyl isocyanate. This disaster killed thousands instantly and caused hundreds of thousands of injuries, becoming one of the world's worst industrial accidents and a lasting symbol of corporate negligence."
}
},
{
"author": "root_agent",
"content": {
"parts": [
{
"text": "# Year: 1984\n\n## Famous Person Bio:\n\nScarlett Johansson is an acclaimed actress renowned for her distinctive voice and versatile performances across a wide range of genres. From her captivating early roles in films like \"Lost in Translation\" to her iconic portrayal of Black Widow in the Marvel Cinematic Universe, she has consistently delivered powerful performances. A four-time Golden Globe nominee and two-time Academy Award nominee, she remains one of Hollywood's most bankable and respected stars.\n\n## Historical Event:\n\nIn December 1984, the Union Carbide chemical plant in Bhopal, India, experienced a catastrophic gas leak, releasing deadly methyl isocyanate. This disaster killed thousands instantly and caused hundreds of thousands of injuries, becoming one of the world's worst industrial accidents and a lasting symbol of corporate negligence."
}
],
"role": "user"
},
"id": "e-7",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/aggregate_results@1"
}
}
],
"id": "b7848178-784d-4fe3-a11f-568b3419acb7",
"state": {
"__session_metadata__": {
"displayName": "1984"
}
},
"userId": "user"
}
@@ -0,0 +1,36 @@
# Node as Tool
## Overview
Demonstrates wrapping both a regular ADK `Node` (using the `@node` decorator) and a `Workflow` as tools that can be automatically called by a parent `Agent`.
In this sample:
1. The parent agent receives an inquiry about a customer's discount.
1. It invokes `customer_lookup_workflow` (a `Workflow` wrapped as a tool) to retrieve customer status.
1. It then invokes `calculate_discount` (a regular `Node` wrapped as a tool) using the retrieved status.
## Sample Inputs
- `What discount does customer c123 get?`
*The parent agent first invokes `customer_lookup_workflow` to verify status, then invokes `calculate_discount` to determine the discount percentage, and summarizes the results.*
## Agent Topology Graph
```mermaid
graph TD
customer_service_agent[customer_service_agent] -->|calls| customer_lookup_workflow(customer_lookup_workflow)
customer_service_agent -->|calls| calculate_discount(calculate_discount)
```
## How To
To expose an existing `Node` or `Workflow` as a tool callable by an `Agent`:
1. Define your `Node` (or `@node`) or `Workflow` and assign both an `input_schema` and a `description`.
1. Pass the node/workflow directly into your parent agent's `tools` list: `Agent(..., tools=[my_node, my_workflow])`.
## Related Guides
- [Workflows](../../../../docs/guides/workflows/workflows.md) - Explains building complex multi-step graphs.
@@ -0,0 +1,73 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from typing import Generator
from google.adk import Agent
from google.adk import Event
from google.adk import Workflow
from google.adk.workflow import node
from pydantic import BaseModel
from pydantic import Field
# 1. Define schemas
class CustomerLookupArgs(BaseModel):
user_id: str = Field(description="The customer's unique identifier.")
# 2. Define a regular Node using the @node decorator.
# This Node is wrapped as a NodeTool automatically by the Agent.
# As a NodeTool, it has the ability to yield intermediate Events during execution.
@node
def calculate_discount(tier: str, ctx) -> Generator[Event | str, None, None]:
"""Calculates the discount percentage based on customer tier.
Args:
tier: The customer's membership tier (e.g., VIP, Standard).
"""
yield Event(message=f"Checking discount rules for tier '{tier}'...")
discount = "20% off" if "VIP" in tier else "5% off"
yield discount
# 3. Define a Workflow.
# This Workflow is wrapped as a NodeTool automatically by the Agent.
def lookup_customer_data(node_input: CustomerLookupArgs, ctx) -> dict[str, str]:
return {"user_id": node_input.user_id, "tier": "Verified VIP Member"}
customer_lookup_workflow = Workflow(
name="customer_lookup_workflow",
description="Looks up customer status and tier by user_id.",
input_schema=CustomerLookupArgs,
edges=[
("START", lookup_customer_data),
],
)
# 4. Define the Agent that uses both Node and Workflow as tools.
root_agent = Agent(
name="customer_service_agent",
instruction="""
You are a customer service assistant.
1. First, call `customer_lookup_workflow` using the user_id to get their membership tier.
2. Then, call `calculate_discount` node with that tier to find out what discount they get.
Summarize these details for the customer.
""",
tools=[customer_lookup_workflow, calculate_discount],
)
@@ -0,0 +1,182 @@
{
"appName": "node_as_tool",
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "What discount does customer c123 get?"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "customer_service_agent",
"content": {
"parts": [
{
"functionCall": {
"args": {
"user_id": "c123"
},
"id": "fc-1",
"name": "customer_lookup_workflow"
}
}
],
"role": "model"
},
"id": "e-2",
"invocationId": "i-1",
"longRunningToolIds": [
"fc-1"
],
"nodeInfo": {
"path": "customer_service_agent@1"
}
},
{
"author": "customer_lookup_workflow",
"branch": "customer_lookup_workflow@fc-1",
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"customer_lookup_workflow@1/lookup_customer_data@1",
"customer_lookup_workflow@1"
],
"path": "customer_lookup_workflow@1/lookup_customer_data@1"
},
"output": {
"tier": "Verified VIP Member",
"user_id": "c123"
}
},
{
"author": "customer_service_agent",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "customer_lookup_workflow",
"response": {
"tier": "Verified VIP Member",
"user_id": "c123"
}
}
}
],
"role": "user"
},
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"path": "customer_service_agent@1"
}
},
{
"author": "customer_service_agent",
"content": {
"parts": [
{
"functionCall": {
"args": {
"tier": "Verified VIP Member"
},
"id": "fc-2",
"name": "calculate_discount"
}
}
],
"role": "model"
},
"id": "e-5",
"invocationId": "i-1",
"longRunningToolIds": [
"fc-2"
],
"nodeInfo": {
"path": "customer_service_agent@1"
}
},
{
"author": "calculate_discount",
"branch": "calculate_discount@fc-2",
"content": {
"parts": [
{
"text": "Checking discount rules for tier 'Verified VIP Member'..."
}
],
"role": "user"
},
"id": "e-6",
"invocationId": "i-1",
"nodeInfo": {
"path": "calculate_discount@1"
}
},
{
"author": "calculate_discount",
"branch": "calculate_discount@fc-2",
"id": "e-7",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"calculate_discount@1"
],
"path": "calculate_discount@1"
},
"output": "20% off"
},
{
"author": "customer_service_agent",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-2",
"name": "calculate_discount",
"response": {
"result": "20% off"
}
}
}
],
"role": "user"
},
"id": "e-8",
"invocationId": "i-1",
"nodeInfo": {
"path": "customer_service_agent@1"
}
},
{
"author": "customer_service_agent",
"content": {
"parts": [
{
"text": "Customer c123 is a Verified VIP Member and gets a 20% discount."
}
],
"role": "model"
},
"id": "e-9",
"invocationId": "i-1",
"nodeInfo": {
"path": "customer_service_agent@1"
}
}
],
"id": "12345678-1234-1234-1234-123456789abc",
"userId": "user"
}
@@ -0,0 +1,66 @@
# ADK Workflow Node Output Sample
## Overview
This sample demonstrates how to manage component outputs and structure data between nodes in an **ADK Workflow**.
When stringing nodes together, it's critical to know how the ADK framework passes data along edges. This sample shows:
1. Returning a raw string (it gets automatically wrapped in an `Event`).
1. Returning an explicit `Event` for more granular control over routes and state.
1. Generating a structured dictionary via `Agent(output_schema=MyModel)`.
1. Automatically coercing that raw dictionary back into a fully formed Pydantic model simply by defining it as a type-hint parameter in the Python function.
## Sample Inputs
- `cyberpunk future`
- `gardening tips for beginners`
## Graph
```mermaid
graph TD
START --> generate_string_output
generate_string_output --> generate_event_output
generate_event_output --> generate_pydantic_output
generate_pydantic_output --> consume_pydantic_output
```
## How To
1. **Return raw types (string, dict, list):** The node runner will automatically wrap primitives in an `Event(output=...)`.
```python
def generate_string_output(node_input: str):
return "Processed input: " + node_input
```
1. **Return an Event explicitly:** Use this when you also need to emit a `route` or modify `ctx.state`.
```python
def generate_event_output(node_input: str):
return Event(output=f"Wrapped output: {node_input}")
```
1. **Generate structured data from an LLM:** Pass a Pydantic class to the `Agent`'s `output_schema`. The LLM returns a dictionary/JSON matching the structure.
```python
class TopicDetails(BaseModel):
title: str
description: str
category: str
generate_pydantic_output = Agent(
name="generate_pydantic_output",
output_schema=TopicDetails,
)
```
1. **Consume structured data in a function:** Simply type-hint the parameter. `FunctionNode` leverages Pydantic to parse the dictionary back into your fully accessible `TopicDetails` class automatically before your function starts running.
```python
def consume_pydantic_output(node_input: TopicDetails):
# Type coercion converts dict to model. Now you have .title, .category, etc.
return f"Title: {node_input.title}"
```
@@ -0,0 +1,70 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from google.adk import Agent
from google.adk import Event
from google.adk import Workflow
from pydantic import BaseModel
from pydantic import Field
class TopicDetails(BaseModel):
title: str = Field(description="The title of the generated topic.")
description: str = Field(description="A short description of the topic.")
category: str = Field(description="The broad category of the topic.")
def generate_string_output(node_input: str):
"""Returns a simple string. Framework automatically wraps it in an Event."""
return f"Processed input: {node_input}"
def generate_event_output(node_input: str):
"""Explicitly returns an Event object for more control."""
return Event(output=f"Event wrapped output: {node_input}")
generate_pydantic_output = Agent(
name="generate_pydantic_output",
instruction="Generate a creative topic based on the following input.",
output_schema=TopicDetails,
)
def consume_pydantic_output(node_input: TopicDetails):
"""
Relying on the FunctionNode's automatic type parsing.
The framework will coerce the dictionary or JSON into a TopicDetails
object automatically.
"""
return (
"Received Pydantic Model!\n"
f"Title: {node_input.title}\n"
f"Description: {node_input.description}\n"
f"Category: {node_input.category}"
)
root_agent = Workflow(
name="root_agent",
edges=[
(
"START",
generate_string_output,
generate_event_output,
generate_pydantic_output,
consume_pydantic_output,
),
],
)
@@ -0,0 +1,86 @@
{
"appName": "node_output",
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "go"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "root_agent",
"id": "e-2",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"root_agent@1/generate_string_output@1"
],
"path": "root_agent@1/generate_string_output@1"
},
"output": "Processed input: go"
},
{
"author": "root_agent",
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"root_agent@1/generate_event_output@1"
],
"path": "root_agent@1/generate_event_output@1"
},
"output": "Event wrapped output: Processed input: go"
},
{
"author": "generate_pydantic_output",
"content": {
"parts": [
{
"text": "{\"title\": \"The Impulse to Go: Decoding Humanity's Perpetual Motion\", \"description\": \"Investigating the fundamental human drive to 'go' - exploring its manifestations from ancient migrations and pioneering expeditions to the relentless pursuit of progress in science, technology, and personal growth, and what happens when we pause.\", \"category\": \"Human Behavior & Future Studies\"}"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/generate_pydantic_output@1"
],
"path": "root_agent@1/generate_pydantic_output@1"
}
},
{
"author": "root_agent",
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"root_agent@1/consume_pydantic_output@1",
"root_agent@1"
],
"path": "root_agent@1/consume_pydantic_output@1"
},
"output": "Received Pydantic Model!\nTitle: The Impulse to Go: Decoding Humanity's Perpetual Motion\nDescription: Investigating the fundamental human drive to 'go' - exploring its manifestations from ancient migrations and pioneering expeditions to the relentless pursuit of progress in science, technology, and personal growth, and what happens when we pause.\nCategory: Human Behavior & Future Studies"
}
],
"id": "82ce71ce-e580-4ae2-b291-f82e90221bbd",
"state": {
"__session_metadata__": {
"displayName": "go"
}
},
"userId": "user"
}
@@ -0,0 +1,69 @@
# ADK Workflow Parallel Worker Sample
## Overview
This sample demonstrates how to use **parallel workers** in ADK Workflows.
It takes a user-provided topic, uses an agent to find a list of related topics. The workflow engine will automatically fan-out execution across multiple concurrently running nodes when given an iterable of inputs. First, it dynamically spins up multiple instances of the `make_upper_case` function in parallel to capitalize the topics. Then, it dynamically spins up parallel instances of the `explain_topic` agent to explain each related topic concurrently. Finally, an `aggregate` function collects and formats all the parallel explanations into a single response.
## Sample Inputs
- `machine learning`
- `renewable energy`
- `space exploration`
## Graph
```mermaid
graph TD
START --> process_input
process_input --> find_related_topics
find_related_topics --> make_upper_case[make_upper_case <br/>parallel_worker=True]
make_upper_case --> worker1[worker 1]
make_upper_case --> worker2[worker 2]
make_upper_case --> workerN[worker N]
worker1 --> explain_topic[explain_topic <br/>parallel_worker=True]
worker2 --> explain_topic
workerN --> explain_topic
explain_topic --> eworker1[worker 1]
explain_topic --> eworker2[worker 2]
explain_topic --> eworkerN[worker N]
eworker1 --> aggregate
eworker2 --> aggregate
eworkerN --> aggregate
```
## How To
Both agents and functions can be designed as parallel workers in an ADK Workflow.
1. Ensure the preceding node in the workflow outputs an iterable (e.g., a `list`). The workflow engine will automatically fan-out and execute the parallel worker node concurrently for each item in the iterable.
1. To define an **Agent** as a parallel worker, use the `parallel_worker=True` parameter:
```python
explain_topic = Agent(
name="explain_topic",
instruction="""Explain how the following topic relates to the original topic: "{topic}".""",
parallel_worker=True,
output_schema=TopicExplanation,
)
```
1. To define a **Python function** as a parallel worker, decorate it with `@node(parallel_worker=True)`:
```python
from google.adk.workflow import node
@node(parallel_worker=True)
def make_upper_case(node_input: str):
yield node_input.upper()
```
1. The subsequent node in the workflow will receive the results from all parallel executions as a single aggregated list (e.g., `list[TopicExplanation]`).
@@ -0,0 +1,79 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from google.adk import Agent
from google.adk import Event
from google.adk import Workflow
from google.adk.workflow import node
from pydantic import BaseModel
class TopicExplanation(BaseModel):
topic: str
explanation: str
def process_input(node_input: str):
"""Puts user input in the state."""
return Event(state={"topic": node_input})
find_related_topics = Agent(
name="find_related_topics",
instruction=(
'Given the specific topic "{topic}", generate a list of 3 '
"related topics."
),
output_schema=list[str],
)
@node(parallel_worker=True)
def make_upper_case(node_input: str):
yield node_input.upper()
explain_topic = Agent(
name="explain_topic",
instruction=(
"Explain how the following topic relates the the original topic: "
'"{topic}".'
),
parallel_worker=True,
output_schema=TopicExplanation,
)
def aggregate(node_input: list[TopicExplanation]):
return Event(
message="\n\n---\n\n".join(
f"{explanation.topic}: {explanation.explanation}"
for explanation in node_input
),
)
root_agent = Workflow(
name="root_agent",
edges=[
(
"START",
process_input,
find_related_topics,
make_upper_case,
explain_topic,
aggregate,
),
],
)
@@ -0,0 +1,225 @@
{
"appName": "parallel_worker",
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "flower"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"actions": {
"stateDelta": {
"topic": "flower"
}
},
"author": "root_agent",
"id": "e-2",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/process_input@1"
}
},
{
"author": "find_related_topics",
"content": {
"parts": [
{
"text": "[\"gardening\", \"plants\", \"botany\"]"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/find_related_topics@1"
],
"path": "root_agent@1/find_related_topics@1"
}
},
{
"author": "root_agent",
"branch": "make_upper_case@1",
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"root_agent@1/make_upper_case@1/make_upper_case@1"
],
"path": "root_agent@1/make_upper_case@1/make_upper_case@1"
},
"output": "GARDENING"
},
{
"author": "root_agent",
"branch": "make_upper_case@2",
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"root_agent@1/make_upper_case@1/make_upper_case@2"
],
"path": "root_agent@1/make_upper_case@1/make_upper_case@2"
},
"output": "PLANTS"
},
{
"author": "root_agent",
"branch": "make_upper_case@3",
"id": "e-6",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"root_agent@1/make_upper_case@1/make_upper_case@3"
],
"path": "root_agent@1/make_upper_case@1/make_upper_case@3"
},
"output": "BOTANY"
},
{
"author": "root_agent",
"id": "e-7",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"root_agent@1/make_upper_case@1"
],
"path": "root_agent@1/make_upper_case@1"
},
"output": [
"GARDENING",
"PLANTS",
"BOTANY"
]
},
{
"author": "explain_topic",
"branch": "explain_topic@1",
"content": {
"parts": [
{
"text": "{\"topic\": \"GARDENING\", \"explanation\": \"Gardening is the practice of growing and cultivating plants, and flowers are a central element in many gardening practices. Gardeners often plant, nurture, and arrange flowers for their aesthetic beauty, fragrance, or to attract pollinators, making flowers an integral part of the gardening world.\"}"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-8",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/explain_topic@1/explain_topic@1"
],
"path": "root_agent@1/explain_topic@1/explain_topic@1"
}
},
{
"author": "explain_topic",
"branch": "explain_topic@2",
"content": {
"parts": [
{
"text": "{\"topic\": \"PLANTS\", \"explanation\": \"A flower is a reproductive part of many types of plants. Plants are the larger biological kingdom to which flowers belong, as flowers grow on and are integral components of flowering plants (angiosperms).\"}"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-9",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/explain_topic@1/explain_topic@2"
],
"path": "root_agent@1/explain_topic@1/explain_topic@2"
}
},
{
"author": "explain_topic",
"branch": "explain_topic@3",
"content": {
"parts": [
{
"text": "{\"topic\": \"BOTANY\", \"explanation\": \"Botany is the scientific study of plants, including their structure, growth, reproduction, metabolism, development, diseases, and chemical properties. Flowers are the reproductive organs of many plants, specifically angiosperms, and are therefore a primary subject of study within botany, with botanists analyzing their morphology, physiology, ecology, and evolutionary significance.\"}"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-10",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/explain_topic@1/explain_topic@3"
],
"path": "root_agent@1/explain_topic@1/explain_topic@3"
}
},
{
"author": "root_agent",
"id": "e-11",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"root_agent@1/explain_topic@1"
],
"path": "root_agent@1/explain_topic@1"
},
"output": [
{
"explanation": "Gardening is the practice of growing and cultivating plants, and flowers are a central element in many gardening practices. Gardeners often plant, nurture, and arrange flowers for their aesthetic beauty, fragrance, or to attract pollinators, making flowers an integral part of the gardening world.",
"topic": "GARDENING"
},
{
"explanation": "A flower is a reproductive part of many types of plants. Plants are the larger biological kingdom to which flowers belong, as flowers grow on and are integral components of flowering plants (angiosperms).",
"topic": "PLANTS"
},
{
"explanation": "Botany is the scientific study of plants, including their structure, growth, reproduction, metabolism, development, diseases, and chemical properties. Flowers are the reproductive organs of many plants, specifically angiosperms, and are therefore a primary subject of study within botany, with botanists analyzing their morphology, physiology, ecology, and evolutionary significance.",
"topic": "BOTANY"
}
]
},
{
"author": "root_agent",
"content": {
"parts": [
{
"text": "GARDENING: Gardening is the practice of growing and cultivating plants, and flowers are a central element in many gardening practices. Gardeners often plant, nurture, and arrange flowers for their aesthetic beauty, fragrance, or to attract pollinators, making flowers an integral part of the gardening world.\n\n---\n\nPLANTS: A flower is a reproductive part of many types of plants. Plants are the larger biological kingdom to which flowers belong, as flowers grow on and are integral components of flowering plants (angiosperms).\n\n---\n\nBOTANY: Botany is the scientific study of plants, including their structure, growth, reproduction, metabolism, development, diseases, and chemical properties. Flowers are the reproductive organs of many plants, specifically angiosperms, and are therefore a primary subject of study within botany, with botanists analyzing their morphology, physiology, ecology, and evolutionary significance."
}
],
"role": "user"
},
"id": "e-12",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/aggregate@1"
}
}
],
"id": "70e3790b-df94-4567-93c9-3c60abc6a4e6",
"state": {
"__session_metadata__": {
"displayName": "flower"
},
"topic": "flower"
},
"userId": "user"
}
@@ -0,0 +1,66 @@
# ADK Workflow Request Input Sample
## Overview
This sample demonstrates how to create a **Human-in-the-Loop** workflow in **ADK Workflows** using the `RequestInput` event.
It shows a customer support scenario where an LLM agent (`draft_email`) drafts a response to a customer complaint. The workflow then halts execution and prompts a human user for review (`request_human_review`). Depending on the human's input (`approve`, `reject`, or custom feedback), the workflow either completes, aborts, or loops back to the AI for revisions.
This pattern is crucial for tasks where AI actions require human verification before proceeding.
## Sample Inputs
- `My phone battery drains too fast`
- `I never received my order`
- `The software crashes when I open the settings`
## Graph
```mermaid
graph TD
START --> draft_email
draft_email --> request_human_review
request_human_review --> handle_human_review
handle_human_review -->|revise| draft_email
handle_human_review -->|approved| send_email
handle_human_review -->|rejected| END_rejected[END rejected]
```
## How To
1. Yield a `RequestInput` event from a node to halt the workflow and prompt the user for input.
```python
from google.adk.events import RequestInput
def request_human_review(draft: str):
yield RequestInput(
message="Please review the draft...",
)
```
1. The subsequent node will receive the user's input as its argument (`node_input`). You can use this input to determine the next routing step.
```python
def handle_human_review(node_input: str):
if node_input == "approve":
yield Event(route="approved")
elif node_input == "reject":
yield Event(route="rejected")
else:
yield Event(state={"feedback": node_input}, route="revise")
```
1. Define the edges in your workflow to handle the different routes, including looping back for revisions.
```python
Workflow(
name="request_input",
edges=[
("START", ..., draft_email, request_human_review, handle_human_review),
(handle_human_review, {"revise": draft_email, "approved": send_email}),
],
)
```
@@ -0,0 +1,82 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from google.adk import Agent
from google.adk import Event
from google.adk import Workflow
from google.adk.events import RequestInput
def process_input(node_input: str):
"""Takes the initial customer complaint as input and sets it in the state."""
yield Event(state={"complaint": node_input, "feedback": ""})
draft_email = Agent(
name="draft_email",
instruction="""
Please write a polite, helpful response email to the following customer complaint: "{complaint}"
If there is any feedback from the manager to revise the draft, please incorporate it: "{feedback?}"
""",
output_key="draft",
)
def request_human_review(draft: str):
yield RequestInput(
message=(
"Please review the following draft email and provide 'approve',"
f" 'reject', or feedback to revise.\n\n---\n{draft}\n---"
),
)
def handle_human_review(node_input: str):
if node_input == "reject":
yield Event(route="rejected")
elif node_input == "approve":
yield Event(route="approved")
else:
yield Event(state={"feedback": node_input}, route="revise")
def reject_email():
yield Event(message="Draft rejected.")
def send_email(draft: str):
yield Event(message="Draft approved and sent successfully.")
root_agent = Workflow(
name="request_input",
edges=[
(
"START",
process_input,
draft_email,
request_human_review,
handle_human_review,
),
(
handle_human_review,
{
"revise": draft_email,
"approved": send_email,
"rejected": reject_email,
},
),
],
)
@@ -0,0 +1,238 @@
{
"appName": "request_input",
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "phone broke"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"actions": {
"stateDelta": {
"complaint": "phone broke",
"feedback": ""
}
},
"author": "request_input",
"id": "e-2",
"invocationId": "i-1",
"nodeInfo": {
"path": "request_input@1/process_input@1"
}
},
{
"actions": {
"stateDelta": {
"draft": "Subject: Regarding Your Phone Issue - How We Can Help\n\nDear Customer,\n\nWe're very sorry to hear that your phone has broken. We understand how frustrating and disruptive this can be.\n\nTo help us understand what happened and assist you as quickly and efficiently as possible, could you please provide a few more details?\n\nPlease tell us:\n* **What is the make and model of your phone?** (e.g., iPhone 13, Samsung Galaxy S22)\n* **When did the phone break, and what happened?** (e.g., dropped it, it got wet, stopped turning on suddenly, screen went blank, etc.)\n* **When and where was the phone purchased?** (This helps us determine warranty status.)\n* **Have you already tried any troubleshooting steps?** (e.g., restarting, charging, etc.)\n\nOnce we have this information, we can guide you through the appropriate next steps, which may include troubleshooting, repair options, or warranty claims.\n\nPlease reply to this email with the requested details, or if you prefer to speak with someone directly, you can call our support team at [Your Company Phone Number] during our business hours [Business Hours, e.g., Monday-Friday, 9 AM - 5 PM EST].\n\nWe're here to help get your phone back up and running as quickly as possible.\n\nThank you for your patience.\n\nSincerely,\n\nThe [Your Company Name] Support Team"
}
},
"author": "draft_email",
"content": {
"parts": [
{
"text": "Subject: Regarding Your Phone Issue - How We Can Help\n\nDear Customer,\n\nWe're very sorry to hear that your phone has broken. We understand how frustrating and disruptive this can be.\n\nTo help us understand what happened and assist you as quickly and efficiently as possible, could you please provide a few more details?\n\nPlease tell us:\n* **What is the make and model of your phone?** (e.g., iPhone 13, Samsung Galaxy S22)\n* **When did the phone break, and what happened?** (e.g., dropped it, it got wet, stopped turning on suddenly, screen went blank, etc.)\n* **When and where was the phone purchased?** (This helps us determine warranty status.)\n* **Have you already tried any troubleshooting steps?** (e.g., restarting, charging, etc.)\n\nOnce we have this information, we can guide you through the appropriate next steps, which may include troubleshooting, repair options, or warranty claims.\n\nPlease reply to this email with the requested details, or if you prefer to speak with someone directly, you can call our support team at [Your Company Phone Number] during our business hours [Business Hours, e.g., Monday-Friday, 9 AM - 5 PM EST].\n\nWe're here to help get your phone back up and running as quickly as possible.\n\nThank you for your patience.\n\nSincerely,\n\nThe [Your Company Name] Support Team"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"request_input@1/draft_email@1"
],
"path": "request_input@1/draft_email@1"
}
},
{
"author": "request_input",
"content": {
"parts": [
{
"functionCall": {
"args": {
"interruptId": "fc-1",
"message": "Please review the following draft email and provide 'approve', 'reject', or feedback to revise.\n\n---\nSubject: Regarding Your Phone Issue - How We Can Help\n\nDear Customer,\n\nWe're very sorry to hear that your phone has broken. We understand how frustrating and disruptive this can be.\n\nTo help us understand what happened and assist you as quickly and efficiently as possible, could you please provide a few more details?\n\nPlease tell us:\n* **What is the make and model of your phone?** (e.g., iPhone 13, Samsung Galaxy S22)\n* **When did the phone break, and what happened?** (e.g., dropped it, it got wet, stopped turning on suddenly, screen went blank, etc.)\n* **When and where was the phone purchased?** (This helps us determine warranty status.)\n* **Have you already tried any troubleshooting steps?** (e.g., restarting, charging, etc.)\n\nOnce we have this information, we can guide you through the appropriate next steps, which may include troubleshooting, repair options, or warranty claims.\n\nPlease reply to this email with the requested details, or if you prefer to speak with someone directly, you can call our support team at [Your Company Phone Number] during our business hours [Business Hours, e.g., Monday-Friday, 9 AM - 5 PM EST].\n\nWe're here to help get your phone back up and running as quickly as possible.\n\nThank you for your patience.\n\nSincerely,\n\nThe [Your Company Name] Support Team\n---",
"payload": null,
"response_schema": null
},
"id": "fc-1",
"name": "adk_request_input"
}
}
],
"role": "model"
},
"id": "e-4",
"invocationId": "i-1",
"longRunningToolIds": [
"fc-1"
],
"nodeInfo": {
"path": "request_input@1/request_human_review@1"
}
},
{
"author": "user",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "adk_request_input",
"response": {
"result": "shorter"
}
}
}
],
"role": "user"
},
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"actions": {
"route": "revise",
"stateDelta": {
"feedback": "shorter"
}
},
"author": "request_input",
"id": "e-6",
"invocationId": "i-1",
"nodeInfo": {
"path": "request_input@1/handle_human_review@1"
}
},
{
"actions": {
"stateDelta": {
"draft": "Subject: Your Phone Issue - How We Can Help\n\nDear Customer,\n\nWe're sorry to hear your phone has broken. To help us assist you quickly, please provide the following details:\n\n* **Make and model of your phone?** (e.g., iPhone 13, Samsung Galaxy S22)\n* **When and how did it break?** (e.g., dropped, got wet, stopped turning on)\n* **When and where was it purchased?** (This helps with warranty)\n* **Have you tried any troubleshooting steps?** (e.g., restarting, charging)\n\nOnce we have this information, we can guide you through the appropriate next steps.\n\nPlease reply to this email with the requested details, or call our support team at [Your Company Phone Number] during [Business Hours] if you prefer to speak directly.\n\nWe look forward to helping you.\n\nSincerely,\n\nThe [Your Company Name] Support Team"
}
},
"author": "draft_email",
"content": {
"parts": [
{
"text": "Subject: Your Phone Issue - How We Can Help\n\nDear Customer,\n\nWe're sorry to hear your phone has broken. To help us assist you quickly, please provide the following details:\n\n* **Make and model of your phone?** (e.g., iPhone 13, Samsung Galaxy S22)\n* **When and how did it break?** (e.g., dropped, got wet, stopped turning on)\n* **When and where was it purchased?** (This helps with warranty)\n* **Have you tried any troubleshooting steps?** (e.g., restarting, charging)\n\nOnce we have this information, we can guide you through the appropriate next steps.\n\nPlease reply to this email with the requested details, or call our support team at [Your Company Phone Number] during [Business Hours] if you prefer to speak directly.\n\nWe look forward to helping you.\n\nSincerely,\n\nThe [Your Company Name] Support Team"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-7",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"request_input@1/draft_email@2"
],
"path": "request_input@1/draft_email@2"
}
},
{
"author": "request_input",
"content": {
"parts": [
{
"functionCall": {
"args": {
"interruptId": "fc-2",
"message": "Please review the following draft email and provide 'approve', 'reject', or feedback to revise.\n\n---\nSubject: Your Phone Issue - How We Can Help\n\nDear Customer,\n\nWe're sorry to hear your phone has broken. To help us assist you quickly, please provide the following details:\n\n* **Make and model of your phone?** (e.g., iPhone 13, Samsung Galaxy S22)\n* **When and how did it break?** (e.g., dropped, got wet, stopped turning on)\n* **When and where was it purchased?** (This helps with warranty)\n* **Have you tried any troubleshooting steps?** (e.g., restarting, charging)\n\nOnce we have this information, we can guide you through the appropriate next steps.\n\nPlease reply to this email with the requested details, or call our support team at [Your Company Phone Number] during [Business Hours] if you prefer to speak directly.\n\nWe look forward to helping you.\n\nSincerely,\n\nThe [Your Company Name] Support Team\n---",
"payload": null,
"response_schema": null
},
"id": "fc-2",
"name": "adk_request_input"
}
}
],
"role": "model"
},
"id": "e-8",
"invocationId": "i-1",
"longRunningToolIds": [
"fc-2"
],
"nodeInfo": {
"path": "request_input@1/request_human_review@2"
}
},
{
"author": "user",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-2",
"name": "adk_request_input",
"response": {
"result": "approve"
}
}
}
],
"role": "user"
},
"id": "e-9",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"actions": {
"route": "approved"
},
"author": "request_input",
"id": "e-10",
"invocationId": "i-1",
"nodeInfo": {
"path": "request_input@1/handle_human_review@2"
}
},
{
"author": "request_input",
"content": {
"parts": [
{
"text": "Draft approved and sent successfully."
}
],
"role": "user"
},
"id": "e-11",
"invocationId": "i-1",
"nodeInfo": {
"path": "request_input@1/send_email@1"
}
}
],
"id": "cbd1b977-915d-4000-a8f8-f1c747fd3d0e",
"state": {
"__session_metadata__": {
"displayName": "phone broke"
},
"complaint": "phone broke",
"draft": "Subject: Regarding Your Phone Issue\n\nDear [Customer Name],\n\nI'm sorry to hear your phone has broken. I understand how inconvenient this must be.\n\nTo help us investigate and find the best solution for you, please reply with details about your phone (model, purchase date) and what happened. If you have an order number, please include that too.\n\nWe're ready to assist you further once we have this information.\n\nSincerely,\n[Your Name/Company Support Team]",
"feedback": "shorter"
},
"userId": "user"
}
@@ -0,0 +1,137 @@
{
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "phone broke"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"actions": {
"stateDelta": {
"complaint": "phone broke",
"feedback": ""
}
},
"author": "request_input",
"id": "e-2",
"invocationId": "i-1",
"nodeInfo": {
"path": "request_input@1/process_input@1"
}
},
{
"actions": {
"stateDelta": {
"draft": "Subject: Regarding Your Phone Issue We're Here to Help\n\nDear Customer,\n\nThank you for reaching out. I'm very sorry to hear that your phone has broken. I understand how disruptive and frustrating it can be when your primary device isn't working as it should.\n\nTo help us understand the best way to assist you with a repair or replacement, could you please provide us with a few more details?\n\n1. **What is the make and model of your phone?** (e.g., iPhone 13, Samsung Galaxy S22)\n2. **When did you purchase the phone?** (This helps us check warranty status.)\n3. **Can you briefly describe what happened or how it broke?** (e.g., dropped, water damage, stopped turning on, screen issues)\n4. **Have you already tried any troubleshooting steps?** (e.g., restarting, charging)\n\nOnce we have this information, we can guide you through the next steps, which may include:\n* **Warranty Service:** If your phone is still under warranty and the damage is covered.\n* **Repair Options:** Providing details on how to send your device for repair.\n* **Replacement Options:** Discussing potential replacement pathways.\n\nIn the meantime, you can also visit our dedicated support page for common issues and troubleshooting at [Link to your support/repair page, e.g., www.yourcompany.com/support/repairs].\n\nPlease reply to this email with the requested details, or if you prefer to speak with someone directly, you can call us at [Your Customer Service Phone Number] during business hours.\n\nWe appreciate your patience and look forward to helping you get this resolved as quickly as possible.\n\nSincerely,\n\nThe [Your Company Name] Support Team"
}
},
"author": "draft_email",
"content": {
"parts": [
{
"text": "Subject: Regarding Your Phone Issue We're Here to Help\n\nDear Customer,\n\nThank you for reaching out. I'm very sorry to hear that your phone has broken. I understand how disruptive and frustrating it can be when your primary device isn't working as it should.\n\nTo help us understand the best way to assist you with a repair or replacement, could you please provide us with a few more details?\n\n1. **What is the make and model of your phone?** (e.g., iPhone 13, Samsung Galaxy S22)\n2. **When did you purchase the phone?** (This helps us check warranty status.)\n3. **Can you briefly describe what happened or how it broke?** (e.g., dropped, water damage, stopped turning on, screen issues)\n4. **Have you already tried any troubleshooting steps?** (e.g., restarting, charging)\n\nOnce we have this information, we can guide you through the next steps, which may include:\n* **Warranty Service:** If your phone is still under warranty and the damage is covered.\n* **Repair Options:** Providing details on how to send your device for repair.\n* **Replacement Options:** Discussing potential replacement pathways.\n\nIn the meantime, you can also visit our dedicated support page for common issues and troubleshooting at [Link to your support/repair page, e.g., www.yourcompany.com/support/repairs].\n\nPlease reply to this email with the requested details, or if you prefer to speak with someone directly, you can call us at [Your Customer Service Phone Number] during business hours.\n\nWe appreciate your patience and look forward to helping you get this resolved as quickly as possible.\n\nSincerely,\n\nThe [Your Company Name] Support Team"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"request_input@1/draft_email@1"
],
"path": "request_input@1/draft_email@1"
}
},
{
"author": "request_input",
"content": {
"parts": [
{
"functionCall": {
"args": {
"interruptId": "fc-1",
"message": "Please review the following draft email and provide 'approve', 'reject', or feedback to revise.\n\n---\nSubject: Regarding Your Phone Issue We're Here to Help\n\nDear Customer,\n\nThank you for reaching out. I'm very sorry to hear that your phone has broken. I understand how disruptive and frustrating it can be when your primary device isn't working as it should.\n\nTo help us understand the best way to assist you with a repair or replacement, could you please provide us with a few more details?\n\n1. **What is the make and model of your phone?** (e.g., iPhone 13, Samsung Galaxy S22)\n2. **When did you purchase the phone?** (This helps us check warranty status.)\n3. **Can you briefly describe what happened or how it broke?** (e.g., dropped, water damage, stopped turning on, screen issues)\n4. **Have you already tried any troubleshooting steps?** (e.g., restarting, charging)\n\nOnce we have this information, we can guide you through the next steps, which may include:\n* **Warranty Service:** If your phone is still under warranty and the damage is covered.\n* **Repair Options:** Providing details on how to send your device for repair.\n* **Replacement Options:** Discussing potential replacement pathways.\n\nIn the meantime, you can also visit our dedicated support page for common issues and troubleshooting at [Link to your support/repair page, e.g., www.yourcompany.com/support/repairs].\n\nPlease reply to this email with the requested details, or if you prefer to speak with someone directly, you can call us at [Your Customer Service Phone Number] during business hours.\n\nWe appreciate your patience and look forward to helping you get this resolved as quickly as possible.\n\nSincerely,\n\nThe [Your Company Name] Support Team\n---",
"payload": null,
"response_schema": null
},
"id": "fc-1",
"name": "adk_request_input"
}
}
],
"role": "model"
},
"id": "e-4",
"invocationId": "i-1",
"longRunningToolIds": [
"fc-1"
],
"nodeInfo": {
"path": "request_input@1/request_human_review@1"
}
},
{
"author": "user",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "adk_request_input",
"response": {
"result": "reject"
}
}
}
],
"role": "user"
},
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"actions": {
"route": "rejected"
},
"author": "request_input",
"id": "e-6",
"invocationId": "i-1",
"nodeInfo": {
"path": "request_input@1/handle_human_review@1"
}
},
{
"author": "request_input",
"content": {
"parts": [
{
"text": "Draft rejected."
}
],
"role": "user"
},
"id": "e-7",
"invocationId": "i-1",
"nodeInfo": {
"path": "request_input@1/reject_email@1"
}
}
]
}
@@ -0,0 +1,76 @@
# ADK Workflow Request Input Advanced Sample
## Overview
This sample demonstrates advanced features for requesting Human-in-the-Loop (HITL) input dynamically during an **ADK Workflow** execution.
Specifically, it highlights how to pass structured data to the client UI using the `payload` parameter, and how to mandate a structured response type using the `response_schema` parameter on the yielded `RequestInput` event.
In this scenario, an employee requests time off by providing a natural language description of their request (e.g., "I need next Monday off to go to the dentist").
- An LLM agent (`process_request`) parses the natural language into a structured Pydantic model containing the number of `days` and a `reason`.
- A python node (`evaluate_request`) evaluates the parsed request:
- If `days <= 1`, it yields a `TimeOffDecision` approving the request.
- If `days > 1`, it yields a `RequestInput` to a manager. It attaches the request details to the `payload` so the client UI can render it. It enforces that the manager must respond with a JSON object containing an `approved` boolean and an optional `approved_days` integer by specifying `response_schema` with a valid Pydantic JSON schema.
## Sample Inputs
Start the workflow by providing the initial time off request in natural language:
- `I'm feeling under the weather and need to take today off.`
*Parses as 1 day, auto-approves.*
- `Taking my family to Disney World, I'll be out for 5 days next week.`
*Parses as 5 days, routes to manager review.*
When the terminal prompts you as the manager, provide valid JSON matching the schema:
- `{"approved": true, "approved_days": 5}`
- `{"approved": false, "approved_days": 0}`
## Graph
```mermaid
graph TD
START --> process_request[process_request <br/>LLM Agent]
process_request --> evaluate_request
evaluate_request -->|Yields TimeOffDecision OR RequestInput| process_decision
process_decision --> END[END]
```
## How To
1. **Define the Response Schema:** Use a Pydantic model's `model_json_schema()` to get a standard layout of what the human should return.
```python
from typing import Optional
from pydantic import BaseModel, Field
class TimeOffDecision(BaseModel):
approved: bool = Field(...)
approved_days: Optional[int] = Field(None)
```
1. **Yield a RequestInput:** Pass the schema and optionally a `payload` for the client to display.
```python
def evaluate_request(request: TimeOffRequest):
# ... logic to check if manager review is needed ...
yield RequestInput(
interrupt_id="manager_approval",
message="Please review this time off request.",
payload=request,
response_schema=TimeOffDecision.model_json_schema()
)
```
1. **Parse the Resumed Input:** When the workflow resumes, the `node_input` to the next node will be the parsed Pydantic model implicitly (if type-hinted).
```python
def process_decision(request: TimeOffRequest, node_input: TimeOffDecision):
if node_input.approved:
# ...
```
@@ -0,0 +1,87 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Optional
from google.adk import Agent
from google.adk import Event
from google.adk import Workflow
from google.adk.events import RequestInput
from pydantic import BaseModel
from pydantic import Field
class TimeOffRequest(BaseModel):
days: int = Field(description="Number of days requested.")
reason: str = Field(description="Reason for the time off.")
class TimeOffDecision(BaseModel):
"""The structured response we expect back from the human manager."""
approved: bool = Field(description="Whether the time off is approved.")
approved_days: Optional[int] = Field(
default=None, description="Number of days approved."
)
process_request = Agent(
name="process_request",
instruction=(
"Extract the number of days and the reason from the user's natural"
" language time off request."
),
output_schema=TimeOffRequest,
output_key="request",
)
def evaluate_request(request: TimeOffRequest):
"""
If days <= 1, it's auto-approved. Otherwise, routes to manager review.
"""
if request.days <= 1:
return TimeOffDecision(approved=True)
else:
return RequestInput(
interrupt_id="manager_approval",
message="Please review this time off request.",
payload=request,
response_schema=TimeOffDecision,
)
def process_decision(request: TimeOffRequest, node_input: TimeOffDecision):
if node_input.approved:
approved_days = (
node_input.approved_days
if node_input.approved_days is not None
else request.days
)
message = (
f"Time Off Approved! {approved_days} out of {request.days} days"
" granted."
)
else:
message = "Time Off Denied."
yield Event(message=message)
root_agent = Workflow(
name="request_input_advanced",
edges=[
("START", process_request, evaluate_request, process_decision),
],
)
@@ -0,0 +1,157 @@
{
"appName": "request_input_advanced",
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "2 sick days"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"actions": {
"stateDelta": {
"request": {
"days": 2,
"reason": "sick"
}
}
},
"author": "process_request",
"content": {
"parts": [
{
"text": "{\"days\":2,\"reason\":\"sick\"}"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-2",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"request_input_advanced@1/process_request@1"
],
"path": "request_input_advanced@1/process_request@1"
}
},
{
"author": "request_input_advanced",
"content": {
"parts": [
{
"functionCall": {
"args": {
"interruptId": "fc-1",
"message": "Please review this time off request.",
"payload": {
"days": 2,
"reason": "sick"
},
"response_schema": {
"description": "The structured response we expect back from the human manager.",
"properties": {
"approved": {
"description": "Whether the time off is approved.",
"title": "Approved",
"type": "boolean"
},
"approved_days": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Number of days approved.",
"title": "Approved Days"
}
},
"required": [
"approved"
],
"title": "TimeOffDecision",
"type": "object"
}
},
"id": "fc-1",
"name": "adk_request_input"
}
}
],
"role": "model"
},
"id": "e-3",
"invocationId": "i-1",
"longRunningToolIds": [
"fc-1"
],
"nodeInfo": {
"path": "request_input_advanced@1/evaluate_request@1"
}
},
{
"author": "user",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "adk_request_input",
"response": {
"result": "{\"approved\": true}"
}
}
}
],
"role": "user"
},
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "request_input_advanced",
"content": {
"parts": [
{
"text": "Time Off Approved! 2 out of 2 days granted."
}
],
"role": "user"
},
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"path": "request_input_advanced@1/process_decision@1"
}
}
],
"id": "131bfe8b-de90-47bd-b442-957f27dc58a6",
"state": {
"__session_metadata__": {
"displayName": "2 sick days"
},
"request": {
"days": 2,
"reason": "sick days"
}
},
"userId": "user"
}
@@ -0,0 +1,86 @@
# ADK Workflow Request Input Rerun Sample
## Overview
This sample demonstrates an alternative way to handle a **Human-in-the-Loop** workflow in **ADK Workflows** using the `RequestInput` event combined with the `@node(rerun_on_resume=True)` decorator.
Like the standard `request_input` sample, this workflow simulates a customer support scenario where an AI drafts an email and a human reviews it. The key difference lies in *how* the human input is processed when the workflow resumes.
### `request_input` vs `request_input_rerun`
- **Standard (`request_input`):** The workflow pauses after a node yields `RequestInput`. When the user provides input and execution resumes, the input is automatically passed as the argument to the **next node** in the edge definition. This requires two separate nodes: one to request the input and one to handle it.
- **Rerun (`request_input_rerun`):** The node yielding `RequestInput` is decorated with `@node(rerun_on_resume=True)`. When execution resumes, the workflow **re-runs the exact same node** that asked for the input. The node can then access the provided input via the execution `Context`.
This allows you to combine the requesting and handling of human input into a single, cohesive node.
## Sample Inputs
- `The delivery was a week late`
- `I received the wrong item`
- `My account was charged twice`
## Graph
```mermaid
graph TD
START --> draft_email
draft_email --> human_review[human_review <br/>reruns on resume]
human_review -->|revise| draft_email
human_review -->|approved| send_email
human_review -->|rejected| END_rejected[END rejected]
```
## How To
1. Decorate the node that needs human input with `@node(rerun_on_resume=True)`. Ensure the function signature includes the workflow `Context`.
```python
from google.adk.workflow import node
from google.adk import Context
@node(rerun_on_resume=True)
def human_review(draft: str, ctx: Context):
# ...
```
1. Inside the node, check if you are being resumed by looking for the `interrupt_id` in `ctx.resume_inputs`.
```python
resume_input = ctx.resume_inputs.get('human_review')
```
1. If `resume_input` is missing (i.e., this is the first time the node is executing), yield the `RequestInput` event to pause the workflow. Include an explicit `interrupt_id`.
```python
if not resume_input:
yield RequestInput(
interrupt_id="human_review",
message="Please review the draft...",
)
return # Important: Stop execution of this node for now
```
1. If `resume_input` is present (i.e., the workflow was resumed with user input), process the input and yield the appropriate routing events.
```python
if resume_input == "reject":
yield Event(route="rejected")
elif resume_input == "approve":
yield Event(route="approved")
else:
yield Event(state={"feedback": resume_input}, route="revise")
```
1. The edge definition is much simpler because the single `human_review` node handles everything:
```python
Workflow(
name="request_input",
edges=[
("START", process_input, draft_email, human_review),
(human_review, {"revise": draft_email, "approved": send_email}),
],
)
```
@@ -0,0 +1,81 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from google.adk import Agent
from google.adk import Context
from google.adk import Event
from google.adk import Workflow
from google.adk.events import RequestInput
from google.adk.workflow import node
def process_input(node_input: str):
"""Takes the initial customer complaint as input and sets it in the state."""
yield Event(state={"complaint": node_input, "feedback": ""})
draft_email = Agent(
name="draft_email",
instruction="""
Please write a polite, helpful response email to the following customer complaint: "{complaint}"
If there is any feedback from the manager to revise the draft, please incorporate it: "{feedback?}"
""",
output_key="draft",
)
@node(rerun_on_resume=True)
def human_review(draft: str, ctx: Context):
resume_input = ctx.resume_inputs.get("human_review")
if not resume_input:
yield RequestInput(
interrupt_id="human_review",
message=(
"Please review the following draft email and provide 'approve',"
f" 'reject', or feedback to revise.\n\n---\n{draft}\n---"
),
)
return
if resume_input == "reject":
yield Event(route="rejected")
elif resume_input == "approve":
yield Event(route="approved")
else:
yield Event(state={"feedback": resume_input}, route="revise")
def reject_email():
yield Event(message="Draft rejected.")
def send_email(draft: str):
yield Event(message="Draft approved and sent successfully.")
root_agent = Workflow(
name="request_input_rerun",
edges=[
("START", process_input, draft_email, human_review),
(
human_review,
{
"revise": draft_email,
"approved": send_email,
"rejected": reject_email,
},
),
],
)
@@ -0,0 +1,238 @@
{
"appName": "request_input_rerun",
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "phone broke"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"actions": {
"stateDelta": {
"complaint": "phone broke",
"feedback": ""
}
},
"author": "request_input_rerun",
"id": "e-2",
"invocationId": "i-1",
"nodeInfo": {
"path": "request_input_rerun@1/process_input@1"
}
},
{
"actions": {
"stateDelta": {
"draft": "Subject: Regarding Your Phone Issue - How We Can Help - [Your Company Name]\n\nDear Valued Customer,\n\nThank you for reaching out to us. We're very sorry to hear that you're experiencing an issue with your phone. We understand how frustrating it can be when your device isn't working as expected, and we're here to help.\n\nTo assist you as quickly and effectively as possible, could you please provide a few more details about the situation? This will help us understand the problem and guide you towards the best solution.\n\nPlease tell us:\n\n1. **What is the model of your phone?** (e.g., iPhone 15, Samsung Galaxy S24, Google Pixel 8, etc.)\n2. **When did you purchase the phone?** (Rough date or year is fine if you don't have the exact date.)\n3. **Could you describe what happened and how the phone \"broke\"?** (e.g., \"It fell and the screen cracked,\" \"It won't turn on,\" \"It got wet,\" \"The battery stopped charging,\" \"It's stuck in a loop,\" etc.)\n4. **Have you already attempted any troubleshooting steps?** (e.g., restarting the phone, checking charging cables, etc.)\n\nOnce we have this information, our support team will be able to assess the situation, discuss potential solutions such as troubleshooting, repair options, or warranty service, and guide you through the next steps.\n\nPlease reply to this email with the requested details, or if you prefer to speak with someone directly, you can call us at [Your Phone Number] during our business hours of [Your Business Hours].\n\nWe look forward to helping you resolve this issue soon.\n\nSincerely,\n\nThe Customer Support Team\n[Your Company Name]\n[Your Company Website (Optional)]"
}
},
"author": "draft_email",
"content": {
"parts": [
{
"text": "Subject: Regarding Your Phone Issue - How We Can Help - [Your Company Name]\n\nDear Valued Customer,\n\nThank you for reaching out to us. We're very sorry to hear that you're experiencing an issue with your phone. We understand how frustrating it can be when your device isn't working as expected, and we're here to help.\n\nTo assist you as quickly and effectively as possible, could you please provide a few more details about the situation? This will help us understand the problem and guide you towards the best solution.\n\nPlease tell us:\n\n1. **What is the model of your phone?** (e.g., iPhone 15, Samsung Galaxy S24, Google Pixel 8, etc.)\n2. **When did you purchase the phone?** (Rough date or year is fine if you don't have the exact date.)\n3. **Could you describe what happened and how the phone \"broke\"?** (e.g., \"It fell and the screen cracked,\" \"It won't turn on,\" \"It got wet,\" \"The battery stopped charging,\" \"It's stuck in a loop,\" etc.)\n4. **Have you already attempted any troubleshooting steps?** (e.g., restarting the phone, checking charging cables, etc.)\n\nOnce we have this information, our support team will be able to assess the situation, discuss potential solutions such as troubleshooting, repair options, or warranty service, and guide you through the next steps.\n\nPlease reply to this email with the requested details, or if you prefer to speak with someone directly, you can call us at [Your Phone Number] during our business hours of [Your Business Hours].\n\nWe look forward to helping you resolve this issue soon.\n\nSincerely,\n\nThe Customer Support Team\n[Your Company Name]\n[Your Company Website (Optional)]"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"request_input_rerun@1/draft_email@1"
],
"path": "request_input_rerun@1/draft_email@1"
}
},
{
"author": "request_input_rerun",
"content": {
"parts": [
{
"functionCall": {
"args": {
"interruptId": "fc-1",
"message": "Please review the following draft email and provide 'approve', 'reject', or feedback to revise.\n\n---\nSubject: Regarding Your Phone Issue - How We Can Help - [Your Company Name]\n\nDear Valued Customer,\n\nThank you for reaching out to us. We're very sorry to hear that you're experiencing an issue with your phone. We understand how frustrating it can be when your device isn't working as expected, and we're here to help.\n\nTo assist you as quickly and effectively as possible, could you please provide a few more details about the situation? This will help us understand the problem and guide you towards the best solution.\n\nPlease tell us:\n\n1. **What is the model of your phone?** (e.g., iPhone 15, Samsung Galaxy S24, Google Pixel 8, etc.)\n2. **When did you purchase the phone?** (Rough date or year is fine if you don't have the exact date.)\n3. **Could you describe what happened and how the phone \"broke\"?** (e.g., \"It fell and the screen cracked,\" \"It won't turn on,\" \"It got wet,\" \"The battery stopped charging,\" \"It's stuck in a loop,\" etc.)\n4. **Have you already attempted any troubleshooting steps?** (e.g., restarting the phone, checking charging cables, etc.)\n\nOnce we have this information, our support team will be able to assess the situation, discuss potential solutions such as troubleshooting, repair options, or warranty service, and guide you through the next steps.\n\nPlease reply to this email with the requested details, or if you prefer to speak with someone directly, you can call us at [Your Phone Number] during our business hours of [Your Business Hours].\n\nWe look forward to helping you resolve this issue soon.\n\nSincerely,\n\nThe Customer Support Team\n[Your Company Name]\n[Your Company Website (Optional)]\n---",
"payload": null,
"response_schema": null
},
"id": "fc-1",
"name": "adk_request_input"
}
}
],
"role": "model"
},
"id": "e-4",
"invocationId": "i-1",
"longRunningToolIds": [
"fc-1"
],
"nodeInfo": {
"path": "request_input_rerun@1/human_review@1"
}
},
{
"author": "user",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "adk_request_input",
"response": {
"result": "shorter"
}
}
}
],
"role": "user"
},
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"actions": {
"route": "revise",
"stateDelta": {
"feedback": "shorter"
}
},
"author": "request_input_rerun",
"id": "e-6",
"invocationId": "i-1",
"nodeInfo": {
"path": "request_input_rerun@1/human_review@1"
}
},
{
"actions": {
"stateDelta": {
"draft": "Subject: Regarding Your Phone Issue - How We Can Help - [Your Company Name]\n\nDear Valued Customer,\n\nThank you for contacting us. We're sorry to hear about your phone issue and are ready to help.\n\nTo assist you efficiently, please provide the following details:\n\n1. **What is the model of your phone?** (e.g., iPhone 15, Samsung Galaxy S24)\n2. **When did you purchase the phone?** (Rough date or year is fine)\n3. **How did the phone \"break\"?** (e.g., screen cracked, won't turn on, got wet)\n4. **Have you attempted any troubleshooting steps?** (e.g., restarting, checking cables)\n\nWith this information, our team can better assess the problem and outline next steps (troubleshooting, repair, warranty).\n\nPlease reply to this email with the details. You can also call us at [Your Phone Number] during [Your Business Hours] if you prefer to speak directly.\n\nWe look forward to helping you resolve this soon.\n\nSincerely,\n\nThe Customer Support Team\n[Your Company Name]\n[Your Company Website (Optional)]"
}
},
"author": "draft_email",
"content": {
"parts": [
{
"text": "Subject: Regarding Your Phone Issue - How We Can Help - [Your Company Name]\n\nDear Valued Customer,\n\nThank you for contacting us. We're sorry to hear about your phone issue and are ready to help.\n\nTo assist you efficiently, please provide the following details:\n\n1. **What is the model of your phone?** (e.g., iPhone 15, Samsung Galaxy S24)\n2. **When did you purchase the phone?** (Rough date or year is fine)\n3. **How did the phone \"break\"?** (e.g., screen cracked, won't turn on, got wet)\n4. **Have you attempted any troubleshooting steps?** (e.g., restarting, checking cables)\n\nWith this information, our team can better assess the problem and outline next steps (troubleshooting, repair, warranty).\n\nPlease reply to this email with the details. You can also call us at [Your Phone Number] during [Your Business Hours] if you prefer to speak directly.\n\nWe look forward to helping you resolve this soon.\n\nSincerely,\n\nThe Customer Support Team\n[Your Company Name]\n[Your Company Website (Optional)]"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-7",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"request_input_rerun@1/draft_email@2"
],
"path": "request_input_rerun@1/draft_email@2"
}
},
{
"author": "request_input_rerun",
"content": {
"parts": [
{
"functionCall": {
"args": {
"interruptId": "fc-2",
"message": "Please review the following draft email and provide 'approve', 'reject', or feedback to revise.\n\n---\nSubject: Regarding Your Phone Issue - How We Can Help - [Your Company Name]\n\nDear Valued Customer,\n\nThank you for contacting us. We're sorry to hear about your phone issue and are ready to help.\n\nTo assist you efficiently, please provide the following details:\n\n1. **What is the model of your phone?** (e.g., iPhone 15, Samsung Galaxy S24)\n2. **When did you purchase the phone?** (Rough date or year is fine)\n3. **How did the phone \"break\"?** (e.g., screen cracked, won't turn on, got wet)\n4. **Have you attempted any troubleshooting steps?** (e.g., restarting, checking cables)\n\nWith this information, our team can better assess the problem and outline next steps (troubleshooting, repair, warranty).\n\nPlease reply to this email with the details. You can also call us at [Your Phone Number] during [Your Business Hours] if you prefer to speak directly.\n\nWe look forward to helping you resolve this soon.\n\nSincerely,\n\nThe Customer Support Team\n[Your Company Name]\n[Your Company Website (Optional)]\n---",
"payload": null,
"response_schema": null
},
"id": "fc-2",
"name": "adk_request_input"
}
}
],
"role": "model"
},
"id": "e-8",
"invocationId": "i-1",
"longRunningToolIds": [
"fc-2"
],
"nodeInfo": {
"path": "request_input_rerun@1/human_review@2"
}
},
{
"author": "user",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-2",
"name": "adk_request_input",
"response": {
"result": "approve"
}
}
}
],
"role": "user"
},
"id": "e-9",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"actions": {
"route": "approved"
},
"author": "request_input_rerun",
"id": "e-10",
"invocationId": "i-1",
"nodeInfo": {
"path": "request_input_rerun@1/human_review@2"
}
},
{
"author": "request_input_rerun",
"content": {
"parts": [
{
"text": "Draft approved and sent successfully."
}
],
"role": "user"
},
"id": "e-11",
"invocationId": "i-1",
"nodeInfo": {
"path": "request_input_rerun@1/send_email@1"
}
}
],
"id": "2daf1ec0-1582-419d-95e9-c5be7269e701",
"state": {
"__session_metadata__": {
"displayName": "phone broke"
},
"complaint": "phone broke",
"draft": "Subject: Regarding Your Broken Phone\n\nDear [Customer Name],\n\nWe're very sorry to hear your phone has broken. We understand this is frustrating, and we want to help resolve it for you as quickly as possible.\n\nTo assist you best, please reply with some additional details:\n* Your phone's model\n* When you purchased it\n* A brief description of what happened\n* Whether it is currently under warranty\n\nYou can also find immediate support and repair options on our website here: [Link to Repair/Support Page]\n\nAlternatively, please feel free to call us directly at [Phone Number] if you prefer to speak with someone.\n\nWe appreciate your patience and look forward to helping.\n\nSincerely,\n\nThe [Your Company Name] Team",
"feedback": "shorter"
},
"userId": "user"
}
@@ -0,0 +1,43 @@
# ADK Workflow Sample: Node Retries
## Overview
In real-world applications, interacting with external APIs, databases, or third-party services can occasionally result in transient failures (e.g., temporary network outages, rate limits, or bad gateways).
The ADK framework allows you to easily handle these scenarios by wrapping the unreliable logic in a `@node` decorator configured with `RetryConfig`. If the node raises one of the expected exceptions, the workflow engine automatically pauses, waits for a backoff delay, and reschedules the node for another attempt.
When a node raises an exception, the framework automatically emits an error event (with `error_code` and `error_message`) so the error is visible in the event stream. If the node has retry configured, it will be retried after the backoff delay.
This sample demonstrates a `get_weather` node that intentionally fails randomly (70% chance) by raising an `HTTPError` representing a 500 Internal Server error. The framework gracefully recovers and eventually succeeds, passing the result to `report_weather`.
## Graph
```mermaid
graph TD
START --> get_weather[get_weather <br/>Retries on HTTPError]
get_weather --> report_weather
```
## How To
1. **Import `RetryConfig`**: Ensure you import the configuration class to set your retry parameters.
```python
from google.adk.workflow import RetryConfig
```
1. **Configure the Decorator**: Apply the `@node` decorator to your Python function and specify the `retry_config` parameter with your desired logic (e.g., `max_attempts`, `initial_delay`).
```python
@node(retry_config=RetryConfig(max_attempts=5, initial_delay=1))
def get_weather(ctx: Context) -> str:
# ... flaky logic here ...
```
When an exception like `HTTPError` occurs, the ADK framework catches it, emits an error event, and processes the backoff delay automatically. As long as `max_attempts` hasn't been exceeded, the node executes again.
1. **Track Retries (Optional)**: If you need to know which attempt the node is currently running, you can access `ctx.attempt_count` from the `Context`.
```python
yield Event(message=f"Getting weather... attempt {ctx.attempt_count}")
```
@@ -0,0 +1,49 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import random
from urllib.error import HTTPError
from google.adk import Context
from google.adk import Event
from google.adk import Workflow
from google.adk.workflow import node
from google.adk.workflow import RetryConfig
@node(retry_config=RetryConfig(max_attempts=5, initial_delay=1))
def get_weather(ctx: Context) -> str:
"""A mock task that fails randomly."""
yield Event(message=f"Getting weather... attempt {ctx.attempt_count}")
if random.random() < 0.7: # 70% chance of failure
raise HTTPError(
url="http://mock-api.example.com",
code=500,
msg="Internal Server Error",
hdrs={},
fp=None,
)
yield "sunny"
def report_weather(node_input: str):
yield Event(message=f"The weather is {node_input}")
root_agent = Workflow(
name="root_agent",
edges=[("START", get_weather, report_weather)],
)
@@ -0,0 +1,131 @@
{
"appName": "retry",
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "go"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "root_agent",
"content": {
"parts": [
{
"text": "Getting weather... attempt 1"
}
],
"role": "user"
},
"id": "e-2",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/get_weather@1"
}
},
{
"author": "root_agent",
"errorCode": "HTTPError",
"errorMessage": "HTTP Error 500: Internal Server Error",
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/get_weather@1"
}
},
{
"author": "root_agent",
"content": {
"parts": [
{
"text": "Getting weather... attempt 2"
}
],
"role": "user"
},
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/get_weather@1"
}
},
{
"author": "root_agent",
"errorCode": "HTTPError",
"errorMessage": "HTTP Error 500: Internal Server Error",
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/get_weather@1"
}
},
{
"author": "root_agent",
"content": {
"parts": [
{
"text": "Getting weather... attempt 3"
}
],
"role": "user"
},
"id": "e-6",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/get_weather@1"
}
},
{
"author": "root_agent",
"id": "e-7",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"root_agent@1/get_weather@1"
],
"path": "root_agent@1/get_weather@1"
},
"output": "sunny"
},
{
"author": "root_agent",
"content": {
"parts": [
{
"text": "The weather is sunny"
}
],
"role": "user"
},
"id": "e-8",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/report_weather@1"
}
}
],
"id": "c0e0c167-1e63-4e18-84b6-ad64ba59376f",
"mocks": {
"random.random": [
0.5,
0.5,
0.8
]
},
"state": {
"__session_metadata__": {
"displayName": "go"
}
},
"userId": "user"
}
@@ -0,0 +1,43 @@
# ADK Workflow Routing Sample
## Overview
This sample demonstrates how to use routing in **ADK Workflows**.
It takes user input and uses an LLM node to categorize it as a **question**, a **statement**, or **other**. Based on the classification, it appropriately routes the execution to a specialized agent or function to handle that specific type of input.
In ADK Workflows, **routing** allows conditionally executing different execution paths based on the output of a previous node.
## Sample Inputs
- `What is the capital of France?`
- `The weather is very nice today.`
- `Translate bonjour to english`
## Graph
```mermaid
graph TD
START --> process_input
process_input --> classify_input
classify_input --> route_on_category
route_on_category -->|question| answer_question
route_on_category -->|statement| comment_on_statement
route_on_category -->|other| handle_other
```
## How To
1. A node (agent or function) yields an `Event` with a specific route name:
```python
yield Event(route="your_route_name")
```
1. In the `Workflow` edges definition, conditional edges are constructed using a routing map dict as the second element of the edge tuple:
```python
(source_node, {"your_route_name": target_node})
```
@@ -0,0 +1,77 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Literal
from google.adk import Agent
from google.adk import Event
from google.adk import Workflow
from pydantic import BaseModel
class InputCategory(BaseModel):
category: Literal["question", "statement", "other"]
def process_input(node_input: str):
return Event(state={"input": node_input})
classify_input = Agent(
name="classify_input",
instruction=(
"Based on this input, decide which category it belongs to: {input}"
),
output_schema=InputCategory,
output_key="category",
)
def route_on_category(category: InputCategory):
"""Yields an Event with a specific route based on the classification."""
yield Event(route=category.category)
answer_question = Agent(
name="answer_question",
instruction="""Answer the question: {input}""",
)
comment_on_statement = Agent(
name="comment_on_statement",
instruction="""Comment on the statement: {input}""",
)
def handle_other():
yield Event(
message="Sorry I can only anwer questions or comment on statements."
)
root_agent = Workflow(
name="root_agent",
edges=[
("START", process_input, classify_input, route_on_category),
(
route_on_category,
{
"question": answer_question,
"statement": comment_on_statement,
"other": handle_other,
},
),
],
)
@@ -0,0 +1,106 @@
{
"appName": "route",
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "who are you"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"actions": {
"stateDelta": {
"input": "who are you"
}
},
"author": "root_agent",
"id": "e-2",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/process_input@1"
}
},
{
"actions": {
"stateDelta": {
"category": {
"category": "question"
}
}
},
"author": "classify_input",
"content": {
"parts": [
{
"text": "{\"category\": \"question\"}"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/classify_input@1"
],
"path": "root_agent@1/classify_input@1"
}
},
{
"actions": {
"route": "question"
},
"author": "root_agent",
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"path": "root_agent@1/route_on_category@1"
}
},
{
"author": "answer_question",
"content": {
"parts": [
{
"text": "I am a large language model, trained by Google."
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/answer_question@1",
"root_agent@1"
],
"path": "root_agent@1/answer_question@1"
}
}
],
"id": "f1ec33bf-99ef-49a5-b64a-e4d31f85db85",
"state": {
"__session_metadata__": {
"displayName": "who are you"
},
"category": {
"category": "question"
},
"input": "who are you"
},
"userId": "user"
}
@@ -0,0 +1,39 @@
# ADK Workflow Sequence Sample
## Overview
This sample demonstrates how to create a simple sequential workflow with **ADK Workflows**.
It connects two LLM agents in a chain. The first agent (`generate_fruit_agent`) is instructed to return the name of a random fruit. The output of this agent becomes the input for the second agent (`generate_benefit_agent`), which then tells a health benefit about that specific fruit.
In a sequence, the execution flows unconditionally from one node to the next in the order they are defined.
## Sample Inputs
This sample does not require any input to run.
## Graph
```mermaid
graph TD
START --> generate_fruit_agent
generate_fruit_agent --> generate_benefit_agent
```
## How To
1. Define the agents or functions that will make up the steps in your sequence.
```python
generate_fruit_agent = Agent(...)
generate_benefit_agent = Agent(...)
```
1. Pass a tuple of three or more elements to `edges` to define an unconditional sequence starting from the first element and passing through each subsequent node in order.
```python
Workflow(
name="root_agent",
edges=[("START", generate_fruit_agent, generate_benefit_agent)],
)
```
@@ -0,0 +1,15 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from . import agent
@@ -0,0 +1,35 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Sample workflow for simple sequential workflow with LLM agents."""
from google.adk import Agent
from google.adk import Workflow
generate_fruit_agent = Agent(
name="generate_fruit_agent",
instruction="""Return the name of a random fruit.
Return only the name, nothing else.""",
)
generate_benefit_agent = Agent(
name="generate_benefit_agent",
instruction="""Tell me a health benefit about the specified fruit.""",
)
root_agent = Workflow(
name="root_agent",
edges=[("START", generate_fruit_agent, generate_benefit_agent)],
)
@@ -0,0 +1,71 @@
{
"appName": "sequence",
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "go"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "generate_fruit_agent",
"content": {
"parts": [
{
"text": "Apple"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-2",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/generate_fruit_agent@1"
],
"path": "root_agent@1/generate_fruit_agent@1"
}
},
{
"author": "generate_benefit_agent",
"content": {
"parts": [
{
"text": "Apples are a good source of **dietary fiber**, particularly soluble fiber like pectin. This can help with digestion, promote a feeling of fullness (aiding in weight management), and may contribute to lowering cholesterol levels."
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/generate_benefit_agent@1",
"root_agent@1"
],
"path": "root_agent@1/generate_benefit_agent@1"
}
}
],
"id": "0e5ab646-61d6-49cd-b058-24fde0afebae",
"state": {
"__session_metadata__": {
"displayName": "go"
}
},
"userId": "user"
}
@@ -0,0 +1,60 @@
# ADK Workflow State Sample
## Overview
This sample demonstrates different ways to manage state in an **ADK Workflow**. State is a dictionary shared across all nodes in the workflow execution, useful for gathering information across multiple steps without passing everything directly from one node's output to another's input.
In this sample, we show four techniques:
1. Updating state via direct dictionary mutation: `ctx.state["key"] = "value"`
1. Updating state by yielding an event: `yield Event(state={"key": "value"})`
1. Reading state via direct dictionary access: `ctx.state["key"]`
1. Reading state via automatic parameter injection: `def func(key: str): ...`
## Sample Inputs
- `Hello ADK!`
- `Testing state management.`
## Graph
```mermaid
graph TD
START --> process_initial_input
process_initial_input --> update_state_via_event
update_state_via_event --> read_state_via_ctx
read_state_via_ctx --> read_state_via_param
```
## How To
1. **Update state via direct mutation:** Access the context and modify `ctx.state` directly.
```python
def process_initial_input(ctx, node_input: str):
ctx.state["original_text"] = node_input
```
1. **Update state via Event:** Yield an `Event` object with a `state` delta dictionary.
```python
def update_state_via_event(node_input: str):
yield Event(
state={"uppercased_text": node_input.upper()}
)
```
1. **Read state via context:** Retrieve values from `ctx.state`.
```python
def read_state_via_ctx(ctx):
original = ctx.state["original_text"]
```
1. **Read state via parameter injection:** Declare a function parameter that matches the key in the workflow state, and ADK will automatically populate it.
```python
def read_state_via_param(appended_text: str):
return f"Final Result: {appended_text}!"
```
@@ -0,0 +1,57 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from google.adk import Event
from google.adk import Workflow
def process_initial_input(ctx, node_input: str):
"""Takes initial input and sets it in state via direct dict modification."""
ctx.state["original_text"] = node_input
return node_input
def update_state_via_event(node_input: str):
"""Returns an Event that implicitly updates the shared workflow state."""
yield Event(state={"uppercased_text": node_input.upper()})
def read_state_via_ctx(ctx):
"""Reads a state variable via direct dictionary access and appends to it."""
original = ctx.state["original_text"]
uppercased = ctx.state["uppercased_text"]
result = f"{uppercased} (Original was: {original})"
ctx.state["appended_text"] = result
return result
def read_state_via_param(appended_text: str):
"""Reads a state variable via automatic parameter injection."""
return f"Final Result: {appended_text}!"
root_agent = Workflow(
name="state_sample",
edges=[
(
"START",
process_initial_input,
update_state_via_event,
read_state_via_ctx,
read_state_via_param,
),
],
)
@@ -0,0 +1,91 @@
{
"appName": "state",
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "go"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"actions": {
"stateDelta": {
"original_text": "go"
}
},
"author": "state_sample",
"id": "e-2",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"state_sample@1/process_initial_input@1"
],
"path": "state_sample@1/process_initial_input@1"
},
"output": "go"
},
{
"actions": {
"stateDelta": {
"uppercased_text": "GO"
}
},
"author": "state_sample",
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"path": "state_sample@1/update_state_via_event@1"
}
},
{
"actions": {
"stateDelta": {
"appended_text": "GO (Original was: go)"
}
},
"author": "state_sample",
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"state_sample@1/read_state_via_ctx@1"
],
"path": "state_sample@1/read_state_via_ctx@1"
},
"output": "GO (Original was: go)"
},
{
"author": "state_sample",
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"state_sample@1/read_state_via_param@1",
"state_sample@1"
],
"path": "state_sample@1/read_state_via_param@1"
},
"output": "Final Result: GO (Original was: go)!"
}
],
"id": "89d894be-5d57-4d5c-9a29-ccc15b3b5eb7",
"state": {
"__session_metadata__": {
"displayName": "go"
},
"appended_text": "GO (Original was: go)",
"original_text": "go",
"uppercased_text": "GO"
},
"userId": "user"
}
@@ -0,0 +1,55 @@
# ADK Workflow use_as_output Sample
## Overview
This sample demonstrates how to use `ctx.run_node(node, use_as_output=True)` to delegate a node's output to a dynamically executed child node.
When `use_as_output=True` is set, the child node's output replaces the parent's output. The parent's own output event is suppressed to avoid duplication, and the child's output flows downstream through the graph as if the parent produced it.
The child node can be any node type — this sample uses a single_turn LLM agent (`summarizer`) as the delegated child.
## Sample Inputs
- `The quick brown fox jumped over the lazy dog near the riverbank on a warm summer afternoon`
## Graph
```mermaid
graph TD
START --> orchestrate
orchestrate -.->|delegates output via ctx.run_node| summarizer
summarizer --> finalize
```
## How To
1. **Define the child node**: The child can be a function or an LLM agent. Its output becomes the parent's output and flows to downstream nodes.
```python
from google.adk import Agent
summarizer = Agent(
name='summarizer',
model='gemini-2.5-flash',
instruction='Summarize the following text in one sentence.',
)
```
1. **Mark the orchestrator as rerun_on_resume**: The parent node that calls `ctx.run_node` must use `@node(rerun_on_resume=True)`.
```python
from google.adk.workflow import node
@node(rerun_on_resume=True)
async def orchestrate(ctx: Context, node_input: str) -> str:
return await ctx.run_node(
summarizer, node_input=node_input, use_as_output=True
)
```
1. **Downstream receives delegated output**: The `finalize` node receives the LLM's summary as its `node_input`, not the parent's.
```python
def finalize(node_input: str) -> str:
return f'final: {node_input}'
```
@@ -0,0 +1,41 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from google.adk import Agent
from google.adk import Context
from google.adk.workflow import node
from google.adk.workflow import Workflow
from google.adk.workflow._base_node import START
summarizer = Agent(
name='summarizer',
instruction='Summarize the following text in one sentence.',
)
@node(rerun_on_resume=True)
async def orchestrate(ctx: Context, node_input: str) -> str:
return await ctx.run_node(
summarizer, node_input=node_input, use_as_output=True
)
def finalize(node_input: str) -> str:
return f'final: {node_input}'
root_agent = Workflow(
name='root_agent',
edges=[(START, orchestrate, finalize)],
)
@@ -0,0 +1,63 @@
{
"appName": "use_as_output",
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "go"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "summarizer",
"content": {
"parts": [
{
"text": "Please provide the text you would like me to summarize!"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-2",
"invocationId": "i-1",
"nodeInfo": {
"messageAsOutput": true,
"outputFor": [
"root_agent@1/orchestrate@1/summarizer@1",
"root_agent@1/orchestrate@1"
],
"path": "root_agent@1/orchestrate@1/summarizer@1"
}
},
{
"author": "root_agent",
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"outputFor": [
"root_agent@1/finalize@1",
"root_agent@1"
],
"path": "root_agent@1/finalize@1"
},
"output": "final: Please provide the text you would like me to summarize!"
}
],
"id": "ba40366e-0563-469f-a4b2-09e007adf6ff",
"state": {
"__session_metadata__": {
"displayName": "go"
}
},
"userId": "user"
}