@@ -795,3 +817,6 @@ the following:
planning (`planner`), controlling agent transfer
(`disallow_transfer_to_parent`, `disallow_transfer_to_peers`), and system-wide
instructions (`global_instruction`). See [Custom agent workflows](/agents/custom-agents/).
+* **Graph-based workflows:** Compose LLM agents as steps in deterministic,
+ graph-based pipelines using [Graph-based agent workflows](/graphs/). In Go v2.0.0, use
+ `workflow.NewAgentNode` to wrap any LLM agent as a workflow node.
diff --git a/docs/agents/models/agent-platform.md b/docs/agents/models/agent-platform.md
index 29bfd7d0de..d8b6c74619 100644
--- a/docs/agents/models/agent-platform.md
+++ b/docs/agents/models/agent-platform.md
@@ -147,7 +147,7 @@ Agent Platform.
**Setup:**
1. **Agent Platform Environment:** Ensure the consolidated Agent Platform setup (ADC, Env
- Vars, `GOOGLE_GENAI_USE_VERTEXAI=TRUE`) is complete.
+ Vars, `GOOGLE_GENAI_USE_ENTERPRISE=TRUE`) is complete.
2. **Install Provider Library:** Install the necessary client library configured
for Agent Platform.
@@ -278,7 +278,7 @@ Agent Platform offers a curated selection of open-source models, such as Meta Ll
**Setup:**
1. **Agent Platform Environment:** Ensure the consolidated Agent Platform setup (ADC, Env
- Vars, `GOOGLE_GENAI_USE_VERTEXAI=TRUE`) is complete.
+ Vars, `GOOGLE_GENAI_USE_ENTERPRISE=TRUE`) is complete.
2. **Install LiteLLM:**
```shell
diff --git a/docs/agents/workflow-agents/index.md b/docs/agents/workflow-agents/index.md
index ef65bea433..0004855e90 100644
--- a/docs/agents/workflow-agents/index.md
+++ b/docs/agents/workflow-agents/index.md
@@ -12,11 +12,11 @@ how and when other agents run, defining the control flow of a process.
!!! note "Alternative: graph-based workflows"
- Starting in ADK 2.0, template workflows have been superseded
+ Starting in ADK 2.0 for Python and Go, template workflows have been superseded
by more flexible workflow structures, including
- [graph-based workflows](/workflows/graphs/) and
- [dynamic workflows](/workflows/dynamic/).
+ [graph-based workflows](/graphs/) and
+ [dynamic workflows](/graphs/dynamic/).
These workflow architectures provide more control, flexibility
and capability to evolve your agent workflows over time.
diff --git a/docs/agents/workflow-agents/loop-agents.md b/docs/agents/workflow-agents/loop-agents.md
index 89ba8ff1d3..9a692893c1 100644
--- a/docs/agents/workflow-agents/loop-agents.md
+++ b/docs/agents/workflow-agents/loop-agents.md
@@ -16,11 +16,11 @@ the ***LoopAgent*** object you define.
!!! note "Alternative: graph-based workflows"
- Starting in ADK 2.0, templated workflows have been superseded
+ Starting in ADK 2.0 for Python and Go, templated workflows have been superseded
by more flexible workflow structures, including
- [graph-based workflows](/workflows/graphs/) and
- [dynamic workflows](/workflows/dynamic/).
+ [graph-based workflows](/graphs/) and
+ [dynamic workflows](/graphs/dynamic/).
### Example scenario
diff --git a/docs/agents/workflow-agents/parallel-agents.md b/docs/agents/workflow-agents/parallel-agents.md
index a1e41bf118..b80217bed6 100644
--- a/docs/agents/workflow-agents/parallel-agents.md
+++ b/docs/agents/workflow-agents/parallel-agents.md
@@ -23,11 +23,11 @@ ultimately managed by the ***ParallelAgent*** object you define.
!!! note "Alternative: graph-based workflows"
- Starting in ADK 2.0, templated workflows have been superseded
+ Starting in ADK 2.0 for Python and Go, templated workflows have been superseded
by more flexible workflow structures, including
- [graph-based workflows](/workflows/graphs/) and
- [dynamic workflows](/workflows/dynamic/).
+ [graph-based workflows](/graphs/) and
+ [dynamic workflows](/graphs/dynamic/).
### How it works
diff --git a/docs/agents/workflow-agents/sequential-agents.md b/docs/agents/workflow-agents/sequential-agents.md
index 2b490b1f8a..9d9e6cf649 100644
--- a/docs/agents/workflow-agents/sequential-agents.md
+++ b/docs/agents/workflow-agents/sequential-agents.md
@@ -16,11 +16,11 @@ object you define.
!!! note "Alternative: graph-based workflows"
- Starting in ADK 2.0, templated workflows have been superseded
+ Starting in ADK 2.0 for Python and Go, templated workflows have been superseded
by more flexible workflow structures, including
- [graph-based workflows](/workflows/graphs/) and
- [dynamic workflows](/workflows/dynamic/).
+ [graph-based workflows](/graphs/) and
+ [dynamic workflows](/graphs/dynamic/).
### Example scenario
diff --git a/docs/deploy/cloud-run.md b/docs/deploy/cloud-run.md
index 9515d7a411..ee483e82d0 100644
--- a/docs/deploy/cloud-run.md
+++ b/docs/deploy/cloud-run.md
@@ -56,7 +56,7 @@ Set your environment variables as described in the [Setup and Installation](../g
```bash
export GOOGLE_CLOUD_PROJECT=your-project-id
export GOOGLE_CLOUD_LOCATION=us-central1 # Or your preferred location
-export GOOGLE_GENAI_USE_VERTEXAI=True
+export GOOGLE_GENAI_USE_ENTERPRISE=True
```
For more information on connecting to Google Cloud from ADK agents, see
@@ -327,7 +327,7 @@ unless you specify it as deployment setting, such as the `--with_ui` option for
--region $GOOGLE_CLOUD_LOCATION \
--project $GOOGLE_CLOUD_PROJECT \
--allow-unauthenticated \
- --set-env-vars="GOOGLE_CLOUD_PROJECT=$GOOGLE_CLOUD_PROJECT,GOOGLE_CLOUD_LOCATION=$GOOGLE_CLOUD_LOCATION,GOOGLE_GENAI_USE_VERTEXAI=$GOOGLE_GENAI_USE_VERTEXAI"
+ --set-env-vars="GOOGLE_CLOUD_PROJECT=$GOOGLE_CLOUD_PROJECT,GOOGLE_CLOUD_LOCATION=$GOOGLE_CLOUD_LOCATION,GOOGLE_GENAI_USE_ENTERPRISE=$GOOGLE_GENAI_USE_ENTERPRISE"
# Add any other necessary environment variables your agent might need
```
@@ -570,7 +570,7 @@ unless you specify it as deployment setting, such as the `--with_ui` option for
--region $GOOGLE_CLOUD_LOCATION \
--project $GOOGLE_CLOUD_PROJECT \
--allow-unauthenticated \
- --set-env-vars="GOOGLE_CLOUD_PROJECT=$GOOGLE_CLOUD_PROJECT,GOOGLE_CLOUD_LOCATION=$GOOGLE_CLOUD_LOCATION,GOOGLE_GENAI_USE_VERTEXAI=$GOOGLE_GENAI_USE_VERTEXAI"
+ --set-env-vars="GOOGLE_CLOUD_PROJECT=$GOOGLE_CLOUD_PROJECT,GOOGLE_CLOUD_LOCATION=$GOOGLE_CLOUD_LOCATION,GOOGLE_GENAI_USE_ENTERPRISE=$GOOGLE_GENAI_USE_ENTERPRISE"
# Add any other necessary environment variables your agent might need
```
diff --git a/docs/deploy/gke.md b/docs/deploy/gke.md
index 42dde6ca94..32089d68af 100644
--- a/docs/deploy/gke.md
+++ b/docs/deploy/gke.md
@@ -8,7 +8,7 @@
To deploy your agent you will need to have a Kubernetes cluster running on GKE. You can create a cluster using the Google Cloud Console or the `gcloud` command line tool.
-In this example we will deploy a simple agent to GKE. The Python agent is a FastAPI application that uses `Gemini Flash` as the LLM. The Go agent uses the ADK launcher and a statically-linked binary in a minimal container. We can use Vertex AI or AI Studio as the LLM provider.
+In this example deploys a simple agent to GKE. The Python agent is a FastAPI application that uses `Gemini Flash` as the LLM. The Go agent uses the ADK launcher and a statically-linked binary in a minimal container. We can use Vertex AI or AI Studio as the LLM provider. You can use Agent Platform or AI Studio as the LLM provider with the environment variable `GOOGLE_GENAI_USE_ENTERPRISE`.
## Environment variables
@@ -17,7 +17,7 @@ Set your environment variables as described in the [Setup and Installation](../g
```bash
export GOOGLE_CLOUD_PROJECT=your-project-id # Your GCP project ID
export GOOGLE_CLOUD_LOCATION=us-central1 # Or your preferred location
-export GOOGLE_GENAI_USE_VERTEXAI=true # Set to true if using Agent Platform
+export GOOGLE_GENAI_USE_ENTERPRISE=true # Set to true if using Agent Platform
export GOOGLE_CLOUD_PROJECT_NUMBER=$(gcloud projects describe --format json $GOOGLE_CLOUD_PROJECT | jq -r ".projectNumber")
```
@@ -479,9 +479,9 @@ spec:
value: $GOOGLE_CLOUD_PROJECT
- name: GOOGLE_CLOUD_LOCATION
value: $GOOGLE_CLOUD_LOCATION
- - name: GOOGLE_GENAI_USE_VERTEXAI
- value: "$GOOGLE_GENAI_USE_VERTEXAI"
- # If using AI Studio, set GOOGLE_GENAI_USE_VERTEXAI to false and set the following:
+ - name: GOOGLE_GENAI_USE_ENTERPRISE
+ value: "$GOOGLE_GENAI_USE_ENTERPRISE"
+ # If using AI Studio, set GOOGLE_GENAI_USE_ENTERPRISE to false and set the following:
# - name: GOOGLE_API_KEY
# value: $GOOGLE_API_KEY
# Add any other necessary environment variables your agent might need
diff --git a/docs/get-started/go.md b/docs/get-started/go.md
index 31a1bffe80..3ba3e03986 100644
--- a/docs/get-started/go.md
+++ b/docs/get-started/go.md
@@ -3,8 +3,15 @@
This guide shows you how to get up and running with Agent Development Kit
for Go. Before you start, make sure you have the following installed:
-* Go 1.24.4 or later
-* ADK Go v0.2.0 or later
+* Go 1.25 or later
+* ADK Go v2.0.0 or later
+
+!!! tip "What's new in ADK Go 2.0"
+
+ ADK Go 2.0 introduces graph-based workflow agents, parallel and loop
+ execution primitives, and Human-in-the-Loop tool confirmation. See the
+ [ADK 2.0 release page](/2.0/) for the full list of new features and
+ migration guidance.
## Create an agent project
@@ -48,13 +55,13 @@ import (
"log"
"os"
- "google.golang.org/adk/agent"
- "google.golang.org/adk/agent/llmagent"
- "google.golang.org/adk/cmd/launcher"
- "google.golang.org/adk/cmd/launcher/full"
- "google.golang.org/adk/model/gemini"
- "google.golang.org/adk/tool"
- "google.golang.org/adk/tool/geminitool"
+ "google.golang.org/adk/v2/agent"
+ "google.golang.org/adk/v2/agent/llmagent"
+ "google.golang.org/adk/v2/cmd/launcher"
+ "google.golang.org/adk/v2/cmd/launcher/full"
+ "google.golang.org/adk/v2/model/gemini"
+ "google.golang.org/adk/v2/tool"
+ "google.golang.org/adk/v2/tool/geminitool"
"google.golang.org/genai"
)
@@ -94,11 +101,13 @@ func main() {
### Configure project and dependencies
-Use the `go mod` command to initialize the project modules and install the
-required packages based on the `import` statement in your agent code file:
+Initialize your module, add ADK Go 2.0 as a pinned dependency, then let `go mod
+tidy` resolve the remaining packages based on the `import` statements in your
+agent code file:
```console
go mod init my-agent/main
+go get google.golang.org/adk/v2
go mod tidy
```
@@ -163,7 +172,7 @@ go run agent.go web api webui
```
This command starts a web server with a chat interface for your agent. You can
-access the web interface at (http://localhost:8080). Select your agent at the
+access the web interface at `http://localhost:8080`. Select your agent at the
upper left corner and type a request.

@@ -178,4 +187,6 @@ upper left corner and type a request.
Now that you have ADK installed and your first agent running, try building
your own agent with our build guides:
-* [Build your agent](/tutorials/)
+* [Build your agent](/tutorials/)
+* [Build graph-based workflows](/graphs/)
+* [ADK Go workflow agents](/agents/workflow-agents/)
diff --git a/docs/get-started/installation.md b/docs/get-started/installation.md
index e90b25ad0c..b87886b334 100644
--- a/docs/get-started/installation.md
+++ b/docs/get-started/installation.md
@@ -56,6 +56,8 @@ across supported languages. For a guided introduction, start with the
=== "Go"
+ **Prerequisites:** Go 1.25 or later is required for ADK Go v2.0.0.
+
**Create a new Go module**
If you are starting a new project, you can create a new Go module:
@@ -64,18 +66,30 @@ across supported languages. For a guided introduction, start with the
go mod init example.com/my-agent
```
- **Install ADK**
+ **Install ADK Go v2.0.0**
- To add the ADK to your project, run the following command:
+ To add ADK Go v2.0.0 to your project, run the following command:
```shell
- go get google.golang.org/adk
+ go get google.golang.org/adk/v2
```
- This will add the ADK as a dependency to your `go.mod` file.
+ This will add ADK Go v2.0.0 as a dependency to your `go.mod` file.
(Optional) Verify your installation by checking your `go.mod` file for the
- `google.golang.org/adk` entry.
+ `google.golang.org/adk/v2` entry.
+
+ ??? tip "Still using ADK Go v1.x?"
+
+ If you are not yet ready to upgrade to v2.0.0, you can continue using
+ the v1.x release line:
+
+ ```shell
+ go get google.golang.org/adk@v1
+ ```
+
+ See the [ADK 2.0 release page](/2.0/) for upgrade guidance, including
+ breaking changes and migration steps for ADK Go 1.x projects.
=== "Java"
diff --git a/docs/get-started/java.md b/docs/get-started/java.md
index 03749122e1..abd1886e91 100644
--- a/docs/get-started/java.md
+++ b/docs/get-started/java.md
@@ -269,7 +269,7 @@ mvn compile exec:java \
```
This command starts a web server with a chat interface for your agent. You can
-access the web interface at (http://localhost:8000). Select your agent at the
+access the web interface at `http://localhost:8000`. Select your agent at the
upper left corner and type a request.

diff --git a/docs/get-started/kotlin.md b/docs/get-started/kotlin.md
index 25d21ce8f4..18d3f1d813 100644
--- a/docs/get-started/kotlin.md
+++ b/docs/get-started/kotlin.md
@@ -279,7 +279,7 @@ gradle run -PmainClass=com.example.agent.WebMainKt
```
This command starts a web server with a chat interface for your agent. You can
-access the web interface at (http://localhost:8080). Select your agent at the
+access the web interface at `http://localhost:8080`. Select your agent at the
upper left corner and type a request.

diff --git a/docs/get-started/python.md b/docs/get-started/python.md
index 40d8de02a4..cacd20b70a 100644
--- a/docs/get-started/python.md
+++ b/docs/get-started/python.md
@@ -150,7 +150,7 @@ adk web --port 8000
run `adk web` from the `agents/` directory.
This command starts a web server with a chat interface for your agent. You can
-access the web interface at (http://localhost:8000). Select the agent at the
+access the web interface at `http://localhost:8000`. Select the agent at the
upper left corner and type a request.

diff --git a/docs/get-started/streaming/quickstart-streaming-java.md b/docs/get-started/streaming/quickstart-streaming-java.md
index bf415c3389..1029996d4d 100644
--- a/docs/get-started/streaming/quickstart-streaming-java.md
+++ b/docs/get-started/streaming/quickstart-streaming-java.md
@@ -109,7 +109,7 @@ To run the server, you’ll need to export two environment variables:
* a variable to specify we’re not using Agent Platform this time.
```shell
-export GOOGLE_GENAI_USE_VERTEXAI=FALSE
+export GOOGLE_GENAI_USE_ENTERPRISE=FALSE
export GOOGLE_API_KEY=YOUR_API_KEY
```
diff --git a/docs/get-started/streaming/quickstart-streaming.md b/docs/get-started/streaming/quickstart-streaming.md
index e4855c9bc1..21fc3c04eb 100644
--- a/docs/get-started/streaming/quickstart-streaming.md
+++ b/docs/get-started/streaming/quickstart-streaming.md
@@ -90,7 +90,7 @@ To run the agent, choose a platform from either Google AI Studio or Google Cloud
2. Open the **`.env`** file located inside (`app/`) and copy-paste the following code.
```env title=".env"
- GOOGLE_GENAI_USE_VERTEXAI=FALSE
+ GOOGLE_GENAI_USE_ENTERPRISE=FALSE
GOOGLE_API_KEY=PASTE_YOUR_ACTUAL_API_KEY_HERE
```
@@ -111,7 +111,7 @@ To run the agent, choose a platform from either Google AI Studio or Google Cloud
the following code and update the project ID and location.
```env title=".env"
- GOOGLE_GENAI_USE_VERTEXAI=TRUE
+ GOOGLE_GENAI_USE_ENTERPRISE=TRUE
GOOGLE_CLOUD_PROJECT=PASTE_YOUR_ACTUAL_PROJECT_ID
GOOGLE_CLOUD_LOCATION=us-central1
```
diff --git a/docs/get-started/typescript.md b/docs/get-started/typescript.md
index b241b67c2c..c928de220e 100644
--- a/docs/get-started/typescript.md
+++ b/docs/get-started/typescript.md
@@ -139,7 +139,7 @@ npx adk web
```
This command starts a web server with a chat interface for your agent. You can
-access the web interface at (http://localhost:8000). Select your agent at the
+access the web interface at `http://localhost:8000`. Select your agent at the
upper right corner and type a request.

diff --git a/docs/graphs/data-handling.md b/docs/graphs/data-handling.md
index b2858b660e..ff02684041 100644
--- a/docs/graphs/data-handling.md
+++ b/docs/graphs/data-handling.md
@@ -1,235 +1,386 @@
# Data handling for agent workflows
- Supported in ADKPython v2.0.0
+ Supported in ADKPython v2.0.0Go v2.0.0
Structuring and managing data between agents and graph-based nodes is critical
for building reliable processes with ADK. This guide explains data handling
within graph-based workflows and collaboration agents, including how information
-is transmitted and received between graph nodes using ***Events***. It covers
-the essential parameters for events, data, content, and state, and explains how
-to implement structured data transfer for both function and agent nodes using
-data format schemas and specific instruction syntax.
-
-## Workflow graph Events
-
-Within a graph-based workflow, you pass data using ***Events***. All execution
-*nodes* in a workflow graph consume and emit Events. This section covers the
-basics of transmitting and receiving data between nodes in a ***Workflow***.
-Events have specific parameters for transmitting different types of data between
-nodes. The key parameters for node data handling are as follows:
-
-- **`output`**: Parameter for passing information between *nodes*.
-- **`message`**: Data intended as a response to a user.
-- **`state`**: Data automatically persisted across nodes via ***Events***
- throughout an ADK session.
-
-Events also carry additional information about the workflow, including the
-source node of the Event.
-
-### Node input and output with Events
-
-Each node in a graph receives and transmits data through the ***Event*** class.
-Use the ***yield*** syntax to hand off data to the next node, as shown in the
-following code snippet:
-
-```python
-from google.adk import Event
-
-def my_function_node(node_input: str):
- output_value = node_input.upper()
- return Event(output=output_value) # "THE RESULT"
-```
-
-Use the ***return*** syntax when outputting ***Event*** data that does not
-require additional processing. When emitting data that requires additional
-processing, or if you are generating more than one data item, you can use more
-than one ***yield*** command. Each ***yield*** call adds to a list of data
-objects on the Event which is passed to the next node of a graph. A ***return***
-or ***yield*** command without a parameter passes a `None` value to the next
-node.
-
-### Event `output` parameter
-
-The ***output*** parameter of an ***Event*** is the standard way to pass data to
-the next node of a graph. The next node receives a ***node input*** object
-containing the data, as shown in the following code sample:
-
-```python
-def my_function_node_1():
- return Event(output="The Result")
-
-def my_function_node_2(node_input: str):
- output_value = node_input.lower()
- return Event(output=output_value) # "the result"
-```
-
-You can pass longer, structured data in a serializable format, as shown in this
-code sample:
-
-```python
-def my_function_node_3():
- yield Event(
- output={
- "city_name": "Paris",
- "city_time": "10:10 AM",
- },
+is transmitted and received between graph nodes. It covers the essential
+parameters for passing data, content, and state, and explains how to implement
+structured data transfer for both function and agent nodes using data format
+schemas and specific instruction syntax.
+
+## Workflow data flow
+
+Within a graph-based workflow, nodes pass data to downstream steps through
+events. A step writes its output to a named event field, and the next step
+receives it as its typed input.
+
+=== "Python"
+
+ In Python, data is exchanged between graph nodes using ***Events***. The key
+ parameters for node data handling are:
+
+ - **`output`**: Parameter for passing information between *nodes*.
+ - **`message`**: Data intended as a response to a user.
+ - **`state`**: Data automatically persisted across nodes via ***Events***
+ throughout an ADK session.
+
+=== "Go"
+
+ In ADK Go v2.0.0, the data-passing mechanism depends on which agent style
+ you use:
+
+ **workflow package** (`FunctionNode`, `AgentNode`, `DynamicNode`): nodes
+ communicate through `session.Event` fields, mirroring Python closely:
+
+ - **`Event.Output`**: the node's return value, set automatically by the
+ framework when a `FunctionNode` returns a non-`*genai.Content` value.
+ The successor node receives this as its typed `input` parameter.
+ - **`Event.Routes`**: routing keys set explicitly by an emitting node to
+ select which conditional edge to follow — the Go equivalent of
+ Python's `Event(route=...)`.
+ - **`Event.NodeInfo`**: scheduler metadata (`path`, `MessageAsOutput`,
+ `OutputFor`). Set by the workflow engine; nodes do not set this
+ directly.
+
+ **Prebuilt workflow agents** (`sequentialagent`, `parallelagent`,
+ `loopagent`): these agents communicate through session state:
+
+ - **`OutputKey`** on `llmagent.Config`: the framework writes the agent's
+ final text response to `state[OutputKey]` after each turn.
+ - **`ctx.Session().State().Set` / `.Get`**: write or read arbitrary
+ values from state inside custom code.
+ - **`{key}` in `Instruction`**: the framework substitutes `state["key"]`
+ into the prompt before calling the model.
+
+ State keys may carry a prefix that controls their lifetime and scope:
+
+ | Prefix constant | Prefix string | Scope |
+ |---|---|---|
+ | `session.KeyPrefixApp` | `"app:"` | Shared across all users and sessions for the app |
+ | `session.KeyPrefixUser` | `"user:"` | Tied to the user, shared across their sessions |
+ | `session.KeyPrefixTemp` | `"temp:"` | Discarded after the current invocation ends |
+ | *(none)* | — | Persists for the lifetime of the session |
+
+### Node output
+
+Each step in a workflow produces output for its successor.
+
+=== "Python"
+
+ Use the ***return*** or ***yield*** syntax to hand off data to the next node:
+
+ ```python
+ from google.adk import Event
+
+ def my_function_node(node_input: str):
+ output_value = node_input.upper()
+ return Event(output=output_value) # "THE RESULT"
+ ```
+
+ Use the ***return*** syntax when outputting ***Event*** data that does not
+ require additional processing. When emitting data that requires additional
+ processing, or if you are generating more than one data item, you can use
+ more than one ***yield*** command. Each ***yield*** call adds to a list of
+ data objects on the Event which is passed to the next node of a graph. A
+ ***return*** or ***yield*** command without a parameter passes a `None` value
+ to the next node.
+
+=== "Go"
+
+ **workflow package**: a `FunctionNode` simply returns a typed Go value.
+ The framework automatically wraps the return value in a `session.Event`
+ and sets `Event.Output`. The successor node receives this value as its
+ typed `input` parameter — no manual event construction needed:
+
+ ```go
+ --8<-- "examples/go/snippets/graphs/data-handling/main.go:event-output"
+ ```
+
+ **Prebuilt workflow agents**: use `OutputKey` on `llmagent.Config` to
+ save an agent's text response to session state, then reference it with
+ `{key}` in downstream agents' `Instruction` templates:
+
+ ```go
+ --8<-- "examples/go/snippets/graphs/data-handling/main.go:output-key"
+ ```
+
+### Node output: passing structured data
+
+=== "Python"
+
+ You can pass longer, structured data in a serializable format:
+
+ ```python
+ def my_function_node_3():
+ yield Event(
+ output={
+ "city_name": "Paris",
+ "city_time": "10:10 AM",
+ },
+ )
+ ```
+
+ !!! warning "Caution: Event.output limitation"
+
+ Nodes are only allowed to emit a single ***Event.output*** data payload
+ per execution. This limitation means that while you can use more than
+ one ***yield*** in a node, having two or more ***yield*** commands with
+ an ***Event.output*** results in a runtime error.
+
+=== "Go"
+
+ **workflow package**: a `FunctionNode` can return any JSON-serializable
+ Go struct. The framework serializes it into `Event.Output` and
+ deserializes it back into the successor node's typed `input` parameter.
+ There is no single-payload restriction — each node has exactly one typed
+ return value:
+
+ ```go
+ --8<-- "examples/go/snippets/graphs/data-handling/main.go:structured-output"
+ ```
+
+ **Prebuilt workflow agents**: use multiple `OutputKey` values, one per
+ agent, to store individual fields in session state. Downstream agents
+ read each field independently via `{key}` in their `Instruction`.
+
+### Routing output
+
+=== "Python"
+
+ Use the `route` parameter of an ***Event*** to drive conditional edge
+ dispatch:
+
+ ```python
+ def router(node_input: str):
+ return Event(route="BUG")
+ ```
+
+=== "Go"
+
+ **workflow package**: an emitting `FunctionNode` constructs a
+ `session.Event` directly, sets `Event.Routes` to the desired route keys,
+ and sets `Event.Output` to forward the payload to the successor. The
+ workflow engine reads `Event.Routes` at dispatch time to select the
+ matching edge:
+
+ ```go
+ --8<-- "examples/go/snippets/graphs/data-handling/main.go:routing-output"
+ ```
+
+### User-facing messages
+
+=== "Python"
+
+ Use the ***message*** parameter of an ***Event*** to send a response to a
+ user rather than pass data to the next node:
+
+ ```python
+ async def user_message(node_input: str):
+ """Tell user research process is starting."""
+ yield Event(message="Beginning research process...")
+ ```
+
+=== "Go"
+
+ **workflow package**: to emit a user-visible message without advancing
+ the node's typed output, set `Event.Content` on an intermediate event
+ emitted via the `emit` callback in an `EmittingFunctionNode`. The
+ terminal return value (or `nil`) controls `Event.Output`.
+
+ **Prebuilt workflow agents**: any `llmagent` step automatically emits its
+ model response as a user-facing event. For non-LLM steps, write a custom
+ `Run` function on an `agent.Agent` that yields events whose
+ `LLMResponse.Content` contains the text.
+
+### Session state and state scopes
+
+Session state persists data across turns within a session. It is the primary
+data-sharing mechanism for the prebuilt workflow agents, and is also available
+inside tools and callbacks regardless of which agent style you use.
+
+=== "Python"
+
+ Use the ***state*** parameter of an ***Event*** to maintain values across
+ nodes. Nodes can modify state values, and the modified state values are
+ available to downstream nodes:
+
+ ```python
+ async def init_state_node(attempts: int = 0):
+ yield Event(
+ state={
+ "attempts": attempts,
+ },
+ )
+
+ async def task_attempt_node(node_input: Content, attempts: int):
+ yield Event(
+ state={
+ "attempts": attempts + 1,
+ },
+ )
+
+ async def read_state_node(ctx: Context):
+ print(f"attempts state: {ctx.state}") # attempts state: attempts: 1
+
+ root_agent = Workflow(
+ name="root_agent",
+ edges=[("START", init_state_node, task_attempt_node, read_state_node)],
+ )
+ ```
+
+ !!! warning "Caution: `state` property data limitations"
+
+ The state parameter *should not be used to persist large amounts of
+ data* between nodes. Use artifacts or other data persistence mechanisms,
+ such as database Tools, to persist large data resources during the life
+ cycle of a Workflow.
+
+=== "Go"
+
+ State is written with `ctx.Session().State().Set(key, value)` and read
+ with `.Get(key)`. The `session` package defines prefix constants that map
+ to the same lifetime scopes as Python's state parameter. This pattern
+ applies to prebuilt workflow agents and to tools and callbacks in any
+ agent style:
+
+ ```go
+ --8<-- "examples/go/snippets/graphs/data-handling/main.go:state-scopes"
+ ```
+
+ !!! warning "Caution: state data limitations"
+
+ Session state is a lightweight key-value store. Do not use it to persist
+ large payloads such as file contents or binary data. Use ADK artifacts
+ or external storage tools instead.
+
+ !!! tip "workflow package: prefer Event.Output over state"
+
+ For the `workflow` package (`FunctionNode`, `AgentNode`, `DynamicNode`),
+ pass data between nodes by returning typed values — the framework sets
+ `Event.Output` automatically. Only use `State().Set` when you need to
+ share values with tools, callbacks, or agent `Instruction` templates.
+
+## Constrain node data with schemas
+
+You can set input and output data schemas to constrain the data formats
+accepted and produced by any agent node.
+
+=== "Python"
+
+ Use `input_schema` and `output_schema` with a class that extends
+ ***BaseModel*** to constrain any agent's input and output:
+
+ ```python
+ from google.adk import Agent
+ from pydantic import BaseModel
+
+ class FlightSearchInput(BaseModel):
+ origin: str # Airport code "SFO"
+ destination: str # Airport code "CDG"
+ departure_date: date # date(2026, 3, 15)
+ passengers: int = 1 # Number of passengers
+
+ class FlightSearchOutput(BaseModel):
+ flights: list[Flight]
+ cheapest_price: float
+
+ flight_searcher = Agent(
+ name="flight_searcher",
+ instruction="Search for available flights.",
+ input_schema=FlightSearchInput,
+ output_schema=FlightSearchOutput,
+ tools=[search_flights_api],
+ mode="single_turn",
+ ...
+ )
+
+ assistant = Agent(
+ name="assistant",
+ instruction="You help users plan trips.",
+ sub_agents=[flight_searcher],
+ ...
)
-```
-
-!!! warning "Caution: Event.output limitation"
-
- Nodes are only allowed to emit a single ***Event.output*** data payload
- per execution. This limitation means that while you can more than one
- ***yield*** in a node, having two or more ***yield*** commands with an
- ***Event.output*** results in a runtime error.
-
-### Event `message` parameter
-
-The ***message*** parameter of an ***Event*** is used to pass data intended as
-a user response. In general, you should not use the ***message*** parameter in
-your agent code unless it is specifically to provide information to a user or
-request information from a user. The following code example show how to provide
-information to a user during workflow execution:
-
-```python
-async def user_message(node_input: str):
- """Tell user research process is starting."""
- yield Event(message="Beginning research process...")
-```
-
-### Event `state` parameter
-
-The ***state*** parameter of an ***Event*** is used to maintain a small set of
-data values during an entire ADK session. Values in the state parameter
-automatically persist between Nodes and are meant for guiding the execution of
-more complex workflows. Nodes can modify state values, and the modified state
-values are available to downstream Nodes.The following code example shows how
-state is persisted across nodes:
-
-```python
-async def init_state_node(attempts: int = 0):
- yield Event(
- state={
- "attempts": attempts,
- },
- )
-
-async def task_attempt_node(node_input: Content, attempts: int):
- yield Event(
- state={
- "attempts": attempts + 1,
- },
- )
-
-async def read_state_node(ctx: Context):
- print(f"attempts state: {ctx.state}") # attempts state: attempts: 1
-
-root_agent = Workflow(
- name="root_agent",
- edges=[("START", init_state_node, task_attempt_node, read_state_node)],
-)
-```
-
-!!! warning "Caution: `state` property data limitations"
-
- The state parameter *should not be used to persist large amounts of data* between
- nodes. Use artifacts or other data persistence mechanisms, such as database
- Tools, to persist large data resources during the life cycle of a Workflow.
-
-## Constrain node data input and output with schemas
-
-You can set input and output data schemas to constrain the input and output data
-formats of any node, including ***FunctionNodes*** and **Agents**. The following
-parameters are optional settings for any node. You can set both or either one of
-these parameters on any workflow node as required by your agent project.
-
-- **`input_schema`**: Set the expected input schema using a class that
- extends ***BaseModel***.
-- **`output_schema`**: Set the required output schema using a class that
- extends ***BaseModel***.
-
-The code example below shows how to set both input and output schemas for a
-subagent.
-
-```python
-from google.adk import Agent
-from pydantic import BaseModel
-
-class FlightSearchInput(BaseModel):
- origin: str # Airport code "SFO"
- destination: str # Airport code "CDG"
- departure_date: date # date(2026, 3, 15)
- passengers: int = 1 # Number of passengers
-
-class FlightSearchOutput(BaseModel):
- flights: list[Flight]
- cheapest_price: float
-
-flight_searcher = Agent(
- name="flight_searcher",
- instruction="Search for available flights.",
- input_schema=FlightSearchInput,
- output_schema=FlightSearchOutput,
- tools=[search_flights_api],
- mode="single_turn",
- ...
-)
-
-assistant = Agent(
- name="assistant",
- instruction="You help users plan trips.",
- sub_agents=[flight_searcher],
- ...
-)
-```
+ ```
+
+=== "Go"
+
+ **workflow package**: use `workflow.NewAgentNodeTyped[Input, Output]` to
+ attach schemas to an agent node. The generic type parameters are reflected
+ into `*jsonschema.Schema` automatically — no hand-built schema construction
+ needed. The node's `Event.Output` carries the structured result to the
+ successor — no `OutputKey` or state write is needed:
+
+ ```go
+ --8<-- "examples/go/snippets/graphs/data-handling/main.go:input-output-schema"
+ ```
+
+ **Prebuilt workflow agents**: set `InputSchema` and `OutputSchema` on
+ `llmagent.Config`. `OutputSchema` forces the model to reply with a JSON
+ object matching the schema (the agent cannot use tools when `OutputSchema`
+ is set). Use `OutputKey` to save the JSON string to state for downstream
+ agents to reference via `{key}` in their `Instruction`.
## Access structured data in agents
-When you pass structured data into an agent from subagent or a workflow node,
-such as a Function Node, you can use specific syntax to add that data into the
-agent's instructions. Specifically, you can use the curly braces `{ }` to select
-the input schema properties, or `< >` to specify the input schema properties,
-the `from` keyword, and the name of the node providing the data. The following
-code snippet shows two ways to include data passed through an agent
-***input schema***:
-
-```python
-class CityTime(BaseModel):
- time_info: str # time information
- city: str # city name
-
-def lookup_time_function(city: str):
- """Simulate returning the current time in the specified city."""
- return Event(output=CityTime(time_info='10:10 AM', city=city))
-
-city_report_agent = Agent(
- name="city_report_agent",
- model="gemini-flash-latest",
- input_schema=CityTime,
-
- # data selection based on class and parameter
- # instruction="""
- # Return a sentence in the following format:
- # It is {CityTime.time_info} in {CityTime.city} right now.
- # """,
-
- # more restrictive data selection based on source node name
- instruction="""
- Return a sentence in the following format:
- It is
in
- right now.
- """,
-)
-
-root_agent = Workflow(
- name="root_agent",
- edges=[
- (START, city_generator_agent, lookup_time_function, city_report_agent)
- ],
-)
-```
-
-For a complete, but simplified version of this workflow, see
+=== "Python"
+
+ Use the curly-brace `{ }` syntax to select properties from the input
+ schema, or `< >` to select a property and also qualify it by the name
+ of the source node:
+
+ ```python
+ class CityTime(BaseModel):
+ time_info: str # time information
+ city: str # city name
+
+ def lookup_time_function(city: str):
+ """Simulate returning the current time in the specified city."""
+ return Event(output=CityTime(time_info='10:10 AM', city=city))
+
+ city_report_agent = Agent(
+ name="city_report_agent",
+ model="gemini-flash-latest",
+ input_schema=CityTime,
+
+ # data selection based on class and parameter
+ # instruction="""
+ # Return a sentence in the following format:
+ # It is {CityTime.time_info} in {CityTime.city} right now.
+ # """,
+
+ # more restrictive data selection based on source node name
+ instruction="""
+ Return a sentence in the following format:
+ It is in
+ right now.
+ """,
+ )
+
+ root_agent = Workflow(
+ name="root_agent",
+ edges=[
+ (START, city_generator_agent, lookup_time_function, city_report_agent)
+ ],
+ )
+ ```
+
+=== "Go"
+
+ In ADK Go v2.0.0, a `FunctionNode` returns a typed struct and the
+ framework serializes it into `Event.Output`. The successor `AgentNode`
+ receives the struct as its user content — the fields are available to the
+ agent's `Instruction` without any `{key}` template syntax. This is the
+ direct equivalent of Python's `input_schema=CityTime` with
+ `{CityTime.time_info}` template placeholders: the struct fields are
+ delivered as typed input rather than looked up by name from state.
+
+ ```go
+ --8<-- "examples/go/snippets/graphs/data-handling/main.go:structured-output"
+ ```
+
+For a complete example of this workflow, see
[Graph-based agent workflows](/graphs/#get-started).
diff --git a/docs/graphs/dynamic.md b/docs/graphs/dynamic.md
index bb342f8c84..d176affd09 100644
--- a/docs/graphs/dynamic.md
+++ b/docs/graphs/dynamic.md
@@ -1,7 +1,7 @@
# Dynamic agent workflows
- Supported in ADKPython v2.0.0
+ Supported in ADKPython v2.0.0Go v2.0.0
The ADK framework provides a programmatic way to define workflows as a more
@@ -14,20 +14,21 @@ manage.
Dynamic workflows in ADK allow you to put aside graph-based path structures and
use the full power of your chosen programming language to build workflows. With
-Dynamic workflows, you can create workflows with simple decorators, invoke
-workflow nodes as functions, and build complex routing logic. Here are some of
-the benefits of dynamic workflows in ADK:
+dynamic workflows, you can create workflows with simple decorators (Python) or
+constructor functions (Go), invoke workflow nodes as functions, and build
+complex routing logic. Here are some of the benefits of dynamic workflows in ADK:
- **Flexible Control Flow:** Define execution order dynamically using
loops, conditionals, and recursion which are difficult or impossible to
represent in static graphs.
- **Programmatic Experience:** Use familiar constructs like `while` loops
- and `async/await` instead of graph-based routing.
+ and `async/await` (Python) or `for` loops and `workflow.RunNode` (Go)
+ instead of graph-based routing.
- **Automatic Checkpointing:** Dynamic workflows track each node
execution. Successful sub-nodes are automatically skipped when resuming the
workflow, making complex logic durable and resumable by default.
- **Encapsulation:** Wrap business logic into *parent* nodes that
- internally compose lower-level nodes, keeping the overall workflow graph
+ internally compose lower-level nodes, keeping the overall workflow
clean and manageable.
## Get started
@@ -35,155 +36,244 @@ the benefits of dynamic workflows in ADK:
The following dynamic workflow code example shows how to define a basic
workflow containing a single node with a function:
-```python
-from google.adk import Context
-from google.adk import Workflow
-from google.adk.workflow import node
-from typing import Any
-
-@node(name="hello_node")
-def my_node(node_input: Any):
- return "Hello World"
-
-# define a dynamic workflow node
-@node(rerun_on_resume=True)
-async def my_workflow(ctx: Context, node_input: str) -> str:
- # run_node executes a node and returns its output
- result = await ctx.run_node(my_node, node_input="hello")
- return result
-
-# Run the workflow
-root_agent = Workflow(
- name="root_agent",
- edges=[("START", my_workflow)],
-)
-```
-
-This example uses the [***@node***](#node) annotation for convenience and to
-keep the written code as simple as possible. This annotation generates wrappers
-that allow the code to be run in the context of an ADK dynamic workflow.
+=== "Python"
+
+ ```python
+ from google.adk import Context
+ from google.adk import Workflow
+ from google.adk.workflow import node
+ from typing import Any
+
+ @node(name="hello_node")
+ def my_node(node_input: Any):
+ return "Hello World"
+
+ # define a dynamic workflow node
+ @node(rerun_on_resume=True)
+ async def my_workflow(ctx: Context, node_input: str) -> str:
+ # run_node executes a node and returns its output
+ result = await ctx.run_node(my_node, node_input="hello")
+ return result
+
+ # Run the workflow
+ root_agent = Workflow(
+ name="root_agent",
+ edges=[("START", my_workflow)],
+ )
+ ```
+
+ This example uses the [***@node***](#node) annotation for convenience and to
+ keep the written code as simple as possible. This annotation generates wrappers
+ that allow the code to be run in the context of an ADK dynamic workflow.
+
+=== "Go"
+
+ In Go, `workflow.NewFunctionNode` replaces the `@node` decorator and
+ `workflow.NewDynamicNode` replaces the `@node(rerun_on_resume=True)` async
+ orchestrator. `workflow.RunNode` is the direct equivalent of
+ `ctx.run_node()`. `workflowagent.New` with `workflow.Chain` replaces
+ `Workflow(edges=[...])`.
+
+ Resume behaviour after a human-in-the-loop pause is controlled by
+ `NodeConfig.RerunOnResume` — see [Nodes](#node) below for details.
+
+ ```go
+ --8<-- "examples/go/snippets/graphs/dynamic/main.go:get-started"
+ ```
## Building blocks: nodes and workflows
Nodes and workflows represent the basic building blocks of ADK's dynamic
-workflows. These classes provide the functionality required to wrap your code so
-it can be integrated into code-based workflows in ADK.
-
-### Nodes and @node {#node}
-
-A dynamic workflow in ADK is composed of *nodes*, which are classes derived
-from ***BaseNode***. A simple version of a usable workflow node is a
-***FunctionNode***, which allows you to wrap code with functionality required to
-run within a ***Workflow***. For convenience, the ADK framework provides the
-***@node*** annotation which generates the node wrapper, keeping boilerplate
-wrapper code to a minimum:
-
-```python
-@node(name="hello_node")
-def my_function_node(node_input: Any):
- return "Hello World"
-```
-
-The following code snippet shows the equivalent code *without* the
-***@node*** annotation:
-
-```python
-# base function
-def my_function_node(node_input: Any):
- return "Hello World"
-
-# FunctionNode wrapper with options
-success_node = FunctionNode(
- my_function_node,
- name="hello",
- rerun_on_resume=True,
-)
-```
-
-Creating the node wrapper code yourself can be useful if you are wrapping
-functions from an external library, need to create multiple nodes from the same
-function with different configurations, or if you are managing node references
-in a registry for advanced orchestration.
+workflows. These types and functions provide the functionality required to
+wrap your code so it can be integrated into code-based workflows in ADK.
+
+### Nodes {#node}
+
+A dynamic workflow in ADK is composed of *nodes*. A simple version of a
+usable workflow node wraps a plain function with the metadata required to
+run within a workflow.
+
+=== "Python"
+
+ In Python, the ***@node*** annotation generates the node wrapper, keeping
+ boilerplate to a minimum:
+
+ ```python
+ @node(name="hello_node")
+ def my_function_node(node_input: Any):
+ return "Hello World"
+ ```
+
+ The following code snippet shows the equivalent code *without* the
+ ***@node*** annotation:
+
+ ```python
+ # base function
+ def my_function_node(node_input: Any):
+ return "Hello World"
+
+ # FunctionNode wrapper with options
+ success_node = FunctionNode(
+ my_function_node,
+ name="hello",
+ rerun_on_resume=True,
+ )
+ ```
+
+ Creating the node wrapper code yourself can be useful if you are wrapping
+ functions from an external library, need to create multiple nodes from the
+ same function with different configurations, or if you are managing node
+ references in a registry for advanced orchestration.
+
+=== "Go"
+
+ In Go, `workflow.NewFunctionNode[IN, OUT]` wraps a plain function as a
+ workflow node, inferring input and output types from the generic parameters.
+ There is no decorator syntax; the node is a value that you pass as a child
+ to `workflow.RunNode` inside a dynamic orchestrator:
+
+ ```go
+ --8<-- "examples/go/snippets/graphs/dynamic/main.go:building-blocks-nodes"
+ ```
+
+ `NodeConfig` holds the same options as Python's `@node` arguments.
+ The most important field is `RerunOnResume *bool`, which controls what
+ happens when a workflow resumes after a human-in-the-loop pause:
+
+ - **`&true` (re-entry mode)**: the interrupted node is re-run from the
+ beginning on resume. Use this for dynamic orchestrator nodes that call
+ `workflow.RunNode` in a loop — the body re-executes and already-completed
+ child activations are skipped automatically (checkpointing). This mirrors
+ Python's `@node(rerun_on_resume=True)`.
+ - **`&false` (handoff mode)**: the resume payload is routed directly to
+ the node's successor as input, bypassing the interrupted node entirely.
+ Use this for leaf nodes that simply emit a pause event and expect the
+ human response to flow to the next step.
+ - **`nil`**: the default depends on node type. `workflow.NewDynamicNode`
+ automatically sets `nil → &true` (re-entry mode), because an
+ orchestrator body must be re-entered on resume to deliver cached child
+ results. `workflow.NewFunctionNode` and other leaf node constructors
+ leave `nil` as-is, which the engine treats as handoff (`&false`).
+ Explicit `&false` is always respected on any node type.
+
+ ```go
+ // NewDynamicNode: nil RerunOnResume is automatically set to &true.
+ // Passing &rerun explicitly is equivalent and makes the intent clear.
+ rerun := true
+ orchestratorNode := workflow.NewDynamicNode[string, string]("my_workflow",
+ myOrchestratorfn,
+ workflow.NodeConfig{RerunOnResume: &rerun}, // re-entry: node body re-runs on resume
+ )
+
+ // NewFunctionNode: nil RerunOnResume stays nil → engine treats as handoff.
+ handoffNode := workflow.NewFunctionNode("leaf_node",
+ myLeafFn,
+ workflow.NodeConfig{}, // nil RerunOnResume → handoff for FunctionNode
+ )
+ ```
+
### Workflows
-In an ADK dynamic workflow, you use the ***Workflow*** class as a primary
-container for orchestrating nodes. You use a node to define a dynamic workflow
-with code that manages running nodes and the execution logic (order and paths)
-for those nodes, as shown in the following code sample:
-
-```python
-@node(rerun_on_resume=True)
-async def my_workflow(ctx):
- # run_node executes a node and returns its output
- result = await ctx.run_node(my_function_node, node_input="Hello")
- result_formatted = await ctx.run_node(my_formatting_node, node_input=result)
- return result_formatted
-
-# Run the workflow
-root_agent = Workflow(
- name="root_agent",
- edges=[("START", my_workflow)],
-)
-```
+In an ADK dynamic workflow, you use a dynamic node as the primary
+orchestrator for nodes. A dynamic node manages running child nodes and the
+execution logic (order and paths) for those nodes.
+
+=== "Python"
+
+ ```python
+ @node(rerun_on_resume=True)
+ async def my_workflow(ctx):
+ # run_node executes a node and returns its output
+ result = await ctx.run_node(my_function_node, node_input="Hello")
+ result_formatted = await ctx.run_node(my_formatting_node, node_input=result)
+ return result_formatted
+
+ # Run the workflow
+ root_agent = Workflow(
+ name="root_agent",
+ edges=[("START", my_workflow)],
+ )
+ ```
+
+=== "Go"
+
+ `workflow.NewDynamicNode` creates an orchestrator whose body calls
+ `workflow.RunNode` for each child step. `workflowagent.New` with
+ `workflow.Chain(workflow.Start, myWorkflow)` is the equivalent of
+ `Workflow(edges=[("START", my_workflow)])`:
+
+ ```go
+ --8<-- "examples/go/snippets/graphs/dynamic/main.go:building-blocks-workflow"
+ ```
## Data handling
When using dynamic workflows with ADK, passing data is simpler than
-[graph-based workflows](/graphs/) because, with a workflow,
-the ***Context*** class's ***run_node()*** method returns the node's output
-directly. This eliminates the need to directly handle session state or complex
-routing outputs for data transfer. The following code example shows how you can
-pass string data between an agent node and a function node:
-
-```python
-from google.adk import Context
-from google.adk.workflow import node
-
-@node(rerun_on_resume=True)
-async def editorial_workflow(ctx: Context, user_request: str):
- # Agent Node generates output
- raw_draft = await ctx.run_node(draft_agent, user_request)
-
- # Function Node formats text
- formatted_text = await ctx.run_node(format_function_node, raw_draft)
-
- return formatted_text
-```
-
-You can also pass specific data schemas using defined class and configure input
-and output schemas, similar to graph-based workflow nodes, as shown in the
-following code example:
-
-```python
-from google.adk import Agent
-from google.adk import Context
-from google.adk.workflow import node
-from pydantic import BaseModel
-
-class CityTime(BaseModel):
- time_info: str # time information
- city: str # city name
-
-@node
-def city_time_function(city: str):
- """Simulate returning the current time in a specified city."""
- return CityTime(time_info="10:10 AM", city=city)
-
-city_report_agent = Agent(
- name="city_report_agent",
- model="gemini-flash-latest",
- input_schema=CityTime,
- instruction="""output the data provided by the previous node.""",
-)
-
-@node # workflow node
-async def city_workflow(ctx: Context):
- city_time = await ctx.run_node(city_time_function, "Paris")
- report_text = await ctx.run_node(city_report_agent, city_time)
-
- return report_text
-```
+[graph-based workflows](/graphs/) because `workflow.RunNode` returns the
+child node's output directly as a typed Go value — eliminating the need to
+manually read and write session state keys for data transfer.
+
+=== "Python"
+
+ ```python
+ from google.adk import Context
+ from google.adk.workflow import node
+
+ @node(rerun_on_resume=True)
+ async def editorial_workflow(ctx: Context, user_request: str):
+ # Agent Node generates output
+ raw_draft = await ctx.run_node(draft_agent, user_request)
+
+ # Function Node formats text
+ formatted_text = await ctx.run_node(format_function_node, raw_draft)
+
+ return formatted_text
+ ```
+
+ You can also pass specific data schemas using a defined class and configure
+ input and output schemas, similar to graph-based workflow nodes:
+
+ ```python
+ from google.adk import Agent
+ from google.adk import Context
+ from google.adk.workflow import node
+ from pydantic import BaseModel
+
+ class CityTime(BaseModel):
+ time_info: str # time information
+ city: str # city name
+
+ @node
+ def city_time_function(city: str):
+ """Simulate returning the current time in a specified city."""
+ return CityTime(time_info="10:10 AM", city=city)
+
+ city_report_agent = Agent(
+ name="city_report_agent",
+ model="gemini-flash-latest",
+ input_schema=CityTime,
+ instruction="""output the data provided by the previous node.""",
+ )
+
+ @node # workflow node
+ async def city_workflow(ctx: Context):
+ city_time = await ctx.run_node(city_time_function, "Paris")
+ report_text = await ctx.run_node(city_report_agent, city_time)
+
+ return report_text
+ ```
+
+=== "Go"
+
+ In Go, `workflow.NewAgentNode` wraps an `agent.Agent` so it can be
+ invoked via `workflow.RunNode` inside a dynamic orchestrator. The output
+ of each `RunNode` call is returned as a typed value — no session state
+ reads are required:
+
+ ```go
+ --8<-- "examples/go/snippets/graphs/dynamic/main.go:data-handling"
+ ```
For more information on data handling between workflow nodes, see
[Data handling for agent workflows](/graphs/data-handling/).
@@ -198,138 +288,199 @@ the techniques that you can use for routing.
### Sequence route
You can create sequential task processing with dynamic workflows in ADK, just
-as you can with graph-based workflows. The following code snippet shows a
-dynamic workflow with an agent, a function node, and a second agent:
+as you can with graph-based workflows.
+
+=== "Python"
+
+ The following code snippet shows a dynamic workflow with an agent, a
+ function node, and a second agent:
+
+ ```python
+ @node # workflow node
+ async def city_workflow(ctx: Context):
+ city = await ctx.run_node(city_generator_agent)
+ city_time = await ctx.run_node(city_time_function, city)
+ report_text = await ctx.run_node(city_report_agent, city_time)
-```python
-@node # workflow node
-async def city_workflow(ctx: Context):
- city = await ctx.run_node(city_generator_agent)
- city_time = await ctx.run_node(city_time_function, city)
- report_text = await ctx.run_node(city_report_agent, city_time)
+ return report_text
+ ```
- return report_text
-```
+=== "Go"
+
+ Call `workflow.RunNode` sequentially inside a `NewDynamicNode` body —
+ each call awaits the child before the next one starts. The
+ [data handling example above](#data-handling) demonstrates exactly this
+ pattern: `cityWorkflow` calls `workflow.RunNode` for `cityTimeNode` and
+ then `cityReportNode` in order, passing each node's typed output to the
+ next.
### Loop route
For workflows where you want to use an iterative loop for a task, dynamic
-workflows offer much more flexibility to define the routing logic you need. The
-following code example shows how to use dynamic workflows to construct a
-workflow loop for generating, reviewing, and updating code:
-
-```python
-from google.adk import Context
-from google.adk import Event
-from google.adk.agents import LlmAgent
-from google.adk.workflow import node
-
-coder_agent = LlmAgent(
- name="generator_agent",
- model="gemini-flash-latest",
- instruction="Write python code for user request.",
- output_schema=str,
-)
-
-@node(name="lint_reviewer")
-async def compile_lint_check(ctx: Context, code: str):
- # Simulate API call or lint check
- class Response:
- findings = ""
- return Response()
-
-fixer_agent = LlmAgent(
- name="fixer_agent",
- model="gemini-flash-latest",
- instruction="""Refactor current code {code}.
- Based on compile & lint review: {findings}""",
- output_schema=str,
-)
-
-@node # workflow node
-async def code_workflow(ctx: Context, user_request: str):
- code = await ctx.run_node(coder_agent, user_request)
- check_resp = await ctx.run_node(compile_lint_check, code)
-
- while check_resp.findings:
- yield Event(state={"code": code, "findings": check_resp.findings})
- code = await ctx.run_node(fixer_agent, {"code": code, "findings": check_resp.findings})
-
- check_resp = await ctx.run_node(compile_lint_check, code)
-
- return code
-```
+workflows offer much more flexibility to define the routing logic you need.
+
+=== "Python"
+
+ The following code example shows how to use dynamic workflows to construct
+ a workflow loop for generating, reviewing, and updating code:
+
+ ```python
+ from google.adk import Context
+ from google.adk import Event
+ from google.adk.agents import LlmAgent
+ from google.adk.workflow import node
+
+ coder_agent = LlmAgent(
+ name="generator_agent",
+ model="gemini-flash-latest",
+ instruction="Write python code for user request.",
+ output_schema=str,
+ )
+
+ @node(name="lint_reviewer")
+ async def compile_lint_check(ctx: Context, code: str):
+ # Simulate API call or lint check
+ class Response:
+ findings = ""
+ return Response()
+
+ fixer_agent = LlmAgent(
+ name="fixer_agent",
+ model="gemini-flash-latest",
+ instruction="""Refactor current code {code}.
+ Based on compile & lint review: {findings}""",
+ output_schema=str,
+ )
+
+ @node # workflow node
+ async def code_workflow(ctx: Context, user_request: str):
+ code = await ctx.run_node(coder_agent, user_request)
+ check_resp = await ctx.run_node(compile_lint_check, code)
+
+ while check_resp.findings:
+ yield Event(state={"code": code, "findings": check_resp.findings})
+ code = await ctx.run_node(fixer_agent, {"code": code, "findings": check_resp.findings})
+
+ check_resp = await ctx.run_node(compile_lint_check, code)
+
+ return code
+ ```
+
+=== "Go"
+
+ In Go, the loop is a plain `for` loop inside the dynamic node body. The
+ lint check node returns an empty string when there are no findings,
+ which signals the loop to exit:
+
+ ```go
+ --8<-- "examples/go/snippets/graphs/dynamic/main.go:loop-route"
+ ```
### Parallel execution routes
-Dynamic workflows in ADK can support parallel execution, and you can use
-standard asynchronous libraries, such as `asyncio`, to build this
-functionality. The following code example shows how to build a workflow node
-that supports parallel execution using `@node` and `asyncio.gather`:
+Dynamic workflows in ADK can support parallel execution.
+
+=== "Python"
+
+ In Python, you can use `asyncio.gather` to build parallel execution:
+
+ ```python
+ import asyncio
+ from typing import Any
+ from google.adk import Context
+ from google.adk.workflow import BaseNode, node
+
+
+ @node(rerun_on_resume=True)
+ async def parallel_supervisor(
+ ctx: Context, node_input: list[Any], real_node: BaseNode
+ ):
+ """Runs a worker node in parallel for each item in the input list."""
+ tasks = []
+ for item in node_input:
+ # ctx.run_node returns a future. Append instead of awaiting immediately.
+ tasks.append(ctx.run_node(real_node, item))
-```python
-import asyncio
-from typing import Any
-from google.adk import Context
-from google.adk.workflow import BaseNode, node
+ # Collect all results in parallel
+ results = await asyncio.gather(*tasks)
+ return results
+ ```
+ !!! tip "Tip: Resuming parallel nodes"
-@node(rerun_on_resume=True)
-async def parallel_supervisor(
- ctx: Context, node_input: list[Any], real_node: BaseNode
-):
- """Runs a worker node in parallel for each item in the input list."""
- tasks = []
- for item in node_input:
- # ctx.run_node returns a future. Append instead of awaiting immediately.
- tasks.append(ctx.run_node(real_node, item))
+ The workflow framework ensures that if a dynamic workflow is resumed,
+ only failed or interrupted worker nodes are re-executed, including
+ parallel worker nodes.
- # Collect all results in parallel
- results = await asyncio.gather(*tasks)
- return results
-```
+=== "Go"
-!!! tip "Tip: Resuming parallel nodes"
+ In Go, `workflow.NewParallelWorker` wraps a child node and runs it
+ concurrently for each element of a list input, collecting results into a
+ single output slice. The `maxConcurrency` parameter caps how many
+ concurrent activations may run simultaneously; `0` means unlimited:
- The workflow framework ensures that if a dynamic workflow is resumed, only
- failed or interrupted worker nodes are re-executed, including parallel worker
- nodes.
+ ```go
+ --8<-- "examples/go/snippets/graphs/dynamic/main.go:parallel-route"
+ ```
+
+ !!! tip "Tip: Resuming parallel nodes"
+
+ The workflow framework ensures that if a dynamic workflow is resumed,
+ only failed or interrupted worker nodes are re-executed, including
+ parallel worker nodes managed by `NewParallelWorker`.
## Human input
Dynamic workflows in ADK can also include human input or human in the loop
-(HITL) steps. You build human input into workflows by yielding a
-***RequestInput*** from a node, which pauses the workflow and waits for user
-input. The following code example shows how to build a human input node and
-include it in a workflow:
+(HITL) steps.
+
+=== "Python"
+
+ You build human input into workflows by yielding a ***RequestInput*** from
+ a node, which pauses the workflow and waits for user input. The following
+ code example shows how to build a human input node and include it in a
+ workflow:
+
+ ```python
+ from typing import Any
+ from google.adk import Context
+ from google.adk.events import RequestInput
+ from google.adk.workflow import node
+
+
+ @node(rerun_on_resume=False)
+ async def get_user_approval(ctx: Context, node_input: Any):
+ """Yields a RequestInput to pause the workflow and wait for user input."""
+ yield RequestInput(message="Please approve this request (Yes/No)")
-```python
-from typing import Any
-from google.adk import Context
-from google.adk.events import RequestInput
-from google.adk.workflow import node
+ @node(rerun_on_resume=True)
+ async def handle_process(ctx: Context, node_input: Any):
+ """The orchestrator calling the interactive step."""
+ user_response = await ctx.run_node(get_user_approval)
-@node(rerun_on_resume=False)
-async def get_user_approval(ctx: Context, node_input: Any):
- """Yields a RequestInput to pause the workflow and wait for user input."""
- yield RequestInput(message="Please approve this request (Yes/No)")
+ if user_response.lower() == "yes":
+ return "Approved"
+ return "Denied"
+ ```
+ !!! important "Important: Parent nodes with `ctx.run_node`"
-@node(rerun_on_resume=True)
-async def handle_process(ctx: Context, node_input: Any):
- """The orchestrator calling the interactive step."""
- user_response = await ctx.run_node(get_user_approval)
+ Parent nodes in dynamic workflows that call `ctx.run_node` must set
+ `rerun_on_resume=True` to handle interruptions properly.
- if user_response.lower() == "yes":
- return "Approved"
- return "Denied"
-```
+=== "Go"
-!!! important "Important: Parent nodes with `ctx.run_node`"
+ In Go, use `workflow.NewEmittingFunctionNode` with
+ `workflow.ResumeOrRequestInput` to implement the re-entry HITL pattern.
+ On the first pass `ResumeOrRequestInput` emits a `session.RequestInput`
+ event and returns `ErrNodeInterrupted`, pausing the workflow. After the
+ human replies, the node is re-run from the top (`RerunOnResume: &true`)
+ and `ResumeOrRequestInput` returns the human's reply directly:
- Parent nodes in dynamic workflows that call `ctx.run_node` must set
- `rerun_on_resume=True` to handle interruptions properly.
+ ```go
+ --8<-- "examples/go/snippets/graphs/dynamic/main.go:human-input"
+ ```
## Advanced features
@@ -349,48 +500,58 @@ or re-run workflow.
#### Custom execution IDs
In some rare cases, you may need to have stable identifiers, such as when
-processing a reorderable list, you can supply a custom ID when running a node.
-In general, you should avoid this due to the impacts to workflow task retries
-and process resumes. Specifically, these IDs are used to check node states and
-skip execution if a node was already run. If you provide custom IDs, make sure
-they are deterministic for workflow re-runs and logically remain the same for
-the input. The following example code shows how to add such an identifier when
-executing node in a workflow:
+processing a reorderable list. In general, you should avoid this due to the
+impacts to workflow task retries and process resumes. Specifically, these IDs
+are used to check node states and skip execution if a node was already run. If
+you provide custom IDs, make sure they are deterministic for workflow re-runs
+and logically remain the same for the input.
!!! warning "Warning: Custom execution IDs"
- Avoid creating custom execution IDs. Since execution IDs are used to determine
- the execution order of nodes, custom execution IDs can cause problems when the
- system attempts to re-run those nodes in your workflow.
-
-```python
-from google.adk import Context
-from google.adk.workflow import node
-from pydantic import BaseModel
-from typing import Any
-import asyncio
-
-class Order(BaseModel):
- order_id: str
- cart_items: list[Product]
-
-@node(rerun_on_resume=True)
-async def process_all_orders(ctx: Context, node_input: Any):
- orders = await get_orders()
-
- process_tasks = []
- for order in orders:
- # Use run_id to provide a custom identifier.
- # Custom run_ids must contain at least one non-numeric character
- # to avoid collision with auto-generated sequential numeric IDs.
- task = ctx.run_node(process_order, order, run_id=f"order-{order.order_id}")
- process_tasks.append(task)
-
- results = await asyncio.gather(*process_tasks)
- return results
-```
-
-By default, auto-generated run IDs are sequential integers starting from
-`"1"` (represented as strings). Custom `run_id` values must contain at
-least one non-numeric character to avoid collisions with these
-auto-generated IDs.
+ Avoid creating custom execution IDs. Since execution IDs are used to
+ determine the execution order of nodes, custom execution IDs can cause
+ problems when the system attempts to re-run those nodes in your workflow.
+
+=== "Python"
+
+ ```python
+ from google.adk import Context
+ from google.adk.workflow import node
+ from pydantic import BaseModel
+ from typing import Any
+ import asyncio
+
+ class Order(BaseModel):
+ order_id: str
+ cart_items: list[Product]
+
+ @node(rerun_on_resume=True)
+ async def process_all_orders(ctx: Context, node_input: Any):
+ orders = await get_orders()
+
+ process_tasks = []
+ for order in orders:
+ # Use run_id to provide a custom identifier.
+ # Custom run_ids must contain at least one non-numeric character
+ # to avoid collision with auto-generated sequential numeric IDs.
+ task = ctx.run_node(process_order, order, run_id=f"order-{order.order_id}")
+ process_tasks.append(task)
+
+ results = await asyncio.gather(*process_tasks)
+ return results
+ ```
+
+ By default, auto-generated run IDs are sequential integers starting from
+ `"1"` (represented as strings). Custom `run_id` values must contain at
+ least one non-numeric character to avoid collisions with these
+ auto-generated IDs.
+
+=== "Go"
+
+ In Go, pass `workflow.WithRunID("order-x")` as a trailing option to
+ `workflow.RunNode`. The ID must contain at least one non-numeric character
+ to avoid collision with the auto-generated sequential counter IDs:
+
+ ```go
+ --8<-- "examples/go/snippets/graphs/dynamic/main.go:custom-execution-ids"
+ ```
diff --git a/docs/graphs/human-input.md b/docs/graphs/human-input.md
index dd9e4c2236..ca3d0a42d3 100644
--- a/docs/graphs/human-input.md
+++ b/docs/graphs/human-input.md
@@ -1,7 +1,7 @@
# Human input for agent workflows
- Supported in ADKPython v2.0.0
+ Supported in ADKPython v2.0.0Go v2.0.0
Being able to request human input for data input, decision verification, or
@@ -13,110 +13,195 @@ the input process more predictable and reliable.
## Get started
-You can implement a human input node in a graph using the ***RequestInput***
-class and a text prompt for the user. The following code example shows how to
-add a human input node to an Workflow graph:
+=== "Python"
-```python
-from google.adk.events import RequestInput
-from google.adk import Workflow
+ You can implement a human input node in a graph using the ***RequestInput***
+ class and a text prompt for the user. The following code example shows how to
+ add a human input node to a Workflow graph:
-def step1(): # Human input step
- yield RequestInput(message="Enter a number:")
+ ```python
+ from google.adk.events import RequestInput
+ from google.adk import Workflow
-def step2(node_input):
- return node_input * 2
+ def step1(): # Human input step
+ yield RequestInput(message="Enter a number:")
-root_agent = Workflow(
- name="root_agent",
- edges=[('START', step1, step2)],
-)
-```
+ def step2(node_input):
+ return node_input * 2
-In this code example, `step1` pauses the execution of the agent until the
-system receives an input from a user. Once the system receives input from the
-user, that input is passed to the next node.
+ root_agent = Workflow(
+ name="root_agent",
+ edges=[('START', step1, step2)],
+ )
+ ```
+
+ In this code example, `step1` pauses the execution of the agent until the
+ system receives an input from a user. Once the system receives input from the
+ user, that input is passed to the next node.
+
+=== "Go"
+
+ In ADK Go v2.0.0, a HITL graph node is built with
+ `workflow.NewEmittingFunctionNode` and `workflow.ResumeOrRequestInput`.
+ This is the direct equivalent of Python's `RequestInput` node:
+
+ - On the **first pass**, `workflow.ResumeOrRequestInput` emits a
+ `session.RequestInput` event (surfaced as `Event.RequestedInput`) and
+ returns `ErrNodeInterrupted`, pausing the workflow.
+ - After the human replies, the node is **re-invoked from the top**
+ (`RerunOnResume: &true`) and `ResumeOrRequestInput` returns the reply
+ payload, which flows as typed input to the next node via `event.Output`.
+
+ ```go
+ --8<-- "examples/go/snippets/graphs/human-input/main.go:graph-hitl-get-started"
+ ```
## Configuration options
-Human input nodes can use the ***RequestInput*** class with the following
-configuration options:
+=== "Python"
+
+ Human input nodes can use the ***RequestInput*** class with the following
+ configuration options:
+
+ - **`message`:** Text provided to the user to explain the human input
+ request.
+ - **`payload`:** Structured data to be used as part of the human input
+ request.
+ - **`response_schema`:** A data structure the human response must conform to.
+
+ !!! note "Note: Response schema input limitations"
+
+ For the **response_schema** setting, the ***RequestInput*** class does not
+ automatically reformat human responses to fit a specified data structure. The
+ human response must be provided in the specified format. For a better user
+ experience, consider providing a user interface to collect structured data
+ or use an Agent node to conform unstructured data to the format required.
+
+=== "Go"
+
+ `session.RequestInput` carries the following fields, which map directly to
+ Python's `RequestInput` parameters:
+
+ - **`InterruptID`** (`string`): A unique identifier for this pause point.
+ Use a stable prefix plus a UUID to avoid collision across workflow runs.
+ Equivalent to the implicit interrupt ID in Python.
+ - **`Message`** (`string`): Human-readable prompt displayed to the user.
+ Equivalent to Python's `message` parameter.
+ - **`Payload`** (`any`): Optional structured data sent alongside the
+ prompt so the client can render additional context. Equivalent to
+ Python's `payload` parameter.
+
+ `workflow.NodeConfig.RerunOnResume` controls what happens on resume:
-- **`message`:** Text provided to the user to explain the human input
- request.
-- **`payload`:** Structured data to be used as part of the human input
- request.
-- **`response_schema`:** A data structure the human response must conform to.
+ - **`&true`**: the node body is re-run from the top; `ResumeOrRequestInput`
+ returns the human's reply on the second pass. Required for nodes that
+ use `ResumeOrRequestInput`.
+ - **`&false`** or **`nil`** (leaf default): the reply is routed to the
+ node's successor as input, bypassing the interrupted node.
-!!! note "Note: Response schema input limitations"
+ !!! note "Note: Structured response from the client"
- For the **response_schema** setting, the ***RequestInput*** class does not
- automatically reformat human responses to fit a specified data structure. The
- human response must be provided in the specified format. For a better user
- experience, consider providing a user interface to collect structured data
- or use an Agent node to conform unstructured data to the format required.
+ ADK Go does not automatically parse or validate the structure of the
+ human's reply payload. If your workflow needs structured feedback,
+ include a UI or a downstream agent node to validate the response before
+ acting on it.
## Human input examples
-The following code examples demonstrate more detailed human input requests,
-including the use of ***message***, ***payload*** and ***response schema***
-parameters.
-
-### Request input with response schema
-
-The following code sample shows how to construct a ***RequestInput*** object in
-a workflow node, including a ***response schema***:
-
-```python
-async def initial_prompt(ctx: Context):
- """Ask the user for itinerary information"""
- input_message = """
- This is an interactive concierge workflow tasked with making you a great
- itinerary for you in your city of choice. If you give some details about
- yourself or what you are generally looking for I can better personalize
- your itinerary.
- For example, input your:
- City (Required),
- Age,
- Hobby,
- Example of attraction you liked
- """
- yield RequestInput(message=input_message, response_schema=str)
-```
-
-### Request input with data payload
-
-The following code sample shows how to construct a ***RequestInput*** object in
-a workflow node, including a ***payload*** and ***response schema***. In this
-example, the `ActivitiesList` is expected to be completed by an agent node that
-composes a list of activities, and the `get_user_feedback()` node requests
-feedback for the user.
-
-```python
-class ActivitiesList(BaseModel):
- """Itinerary should be a list of dictionaries for each activity. Each
- activity has a name and a description"""
- itinerary: List[Dict[str, str]]
-
-class UserFeedback(BaseModel):
- """Expected response structure from the user."""
- user_response: str
-
-async def get_user_feedback(node_input: ActivitiesList):
- """
- Retrieves the user's thoughts on the agents initial itinerary in order to
- either expand on, change the list, or exit the loop
- """
- message = (
- f"""
- Here is your recommended base itinerary:\n{node_input}\n\n
- Which of these items appeal to you (if any)?
+The following code examples demonstrate more detailed human input requests.
+
+### Request input with a message and payload
+
+=== "Python"
+
+ The following code sample shows how to construct a ***RequestInput*** object
+ in a workflow node, including a ***payload*** and ***response schema***. In
+ this example, the `ActivitiesList` is expected to be completed by an agent
+ node that composes a list of activities, and the `get_user_feedback()` node
+ requests feedback from the user.
+
+ ```python
+ class ActivitiesList(BaseModel):
+ """Itinerary should be a list of dictionaries for each activity. Each
+ activity has a name and a description"""
+ itinerary: List[Dict[str, str]]
+
+ class UserFeedback(BaseModel):
+ """Expected response structure from the user."""
+ user_response: str
+
+ async def get_user_feedback(node_input: ActivitiesList):
+ """
+ Retrieves the user's thoughts on the agents initial itinerary in order to
+ either expand on, change the list, or exit the loop
"""
- )
-
- yield RequestInput(
- message=message,
- payload=node_input,
- response_schema=UserFeedback,
- )
-```
+ message = (
+ f"""
+ Here is your recommended base itinerary:\n{node_input}\n\n
+ Which of these items appeal to you (if any)?
+ """
+ )
+
+ yield RequestInput(
+ message=message,
+ payload=node_input,
+ response_schema=UserFeedback,
+ )
+ ```
+
+=== "Go"
+
+ The following code sample shows a three-node graph: a builder node generates
+ a structured itinerary, a HITL node sends it as `Payload` alongside the
+ prompt, and a final node acts on the user's feedback. The `Payload` field
+ lets the client render the full itinerary for the user before they respond:
+
+ ```go
+ --8<-- "examples/go/snippets/graphs/human-input/main.go:graph-hitl-with-payload"
+ ```
+
+## Tool-confirmation: approval prompts in LLM agents
+
+Tool-confirmation is a separate, LLM-agent–level mechanism for yes/no
+approval prompts. Unlike graph HITL nodes, tool-confirmation works inside an
+`llmagent` tool function rather than as a standalone graph node. It is useful
+when you want an LLM agent to pause and ask for approval before executing a
+specific tool call.
+
+=== "Python"
+
+ The following code sample shows how to construct a ***RequestInput*** object
+ in a workflow node, including a ***response schema***:
+
+ ```python
+ async def initial_prompt(ctx: Context):
+ """Ask the user for itinerary information"""
+ input_message = """
+ This is an interactive concierge workflow tasked with making you a great
+ itinerary for you in your city of choice. If you give some details about
+ yourself or what you are generally looking for I can better personalize
+ your itinerary.
+ For example, input your:
+ City (Required),
+ Age,
+ Hobby,
+ Example of attraction you liked
+ """
+ yield RequestInput(message=input_message, response_schema=str)
+ ```
+
+=== "Go"
+
+ Set `RequireConfirmation: true` in `functiontool.Config` for a static
+ yes/no approval before a tool executes, or call `ctx.RequestConfirmation`
+ from inside the tool for a custom hint message:
+
+ ```go
+ --8<-- "examples/go/snippets/graphs/human-input/main.go:simple-hitl"
+ ```
+
+ For a custom hint with manual re-entry handling:
+
+ ```go
+ --8<-- "examples/go/snippets/graphs/human-input/main.go:hitl-with-hint"
+ ```
diff --git a/docs/graphs/index.md b/docs/graphs/index.md
index 1421fa75f6..579c2cc8a1 100644
--- a/docs/graphs/index.md
+++ b/docs/graphs/index.md
@@ -1,7 +1,7 @@
# Graph-based agent workflows
- Supported in ADKPython v2.0.0
+ Supported in ADKPython v2.0.0Go v2.0.0
Graph-based agent workflows in ADK let you build agents with more precise control,
@@ -33,63 +33,92 @@ provide the following advantages:
- **Enhance reliability:** Improve the predictability of your agents by
relying on structured node definitions rather than prompts alone.
+!!! note "Workflow styles in ADK"
+
+ ADK offers three complementary ways to compose multi-step work:
+
+ - **Graph-based workflows** (this section): a declarative graph of nodes
+ and edges with explicit routing — best for deterministic, structured
+ processes.
+ - **[Dynamic workflows](/graphs/dynamic/):** programmatic orchestration
+ in your own code (loops, conditionals, recursion) — best when the
+ control flow is too complex or iterative for a static graph.
+ - **[Prebuilt workflow agents](/agents/workflow-agents/)** (sequential,
+ parallel, loop): higher-level building blocks for common patterns
+ without assembling a graph yourself.
+
## Get started
This section describes how to get started with graph-based agents. The following
example shows how to create a sequential graph-based agent workflow that
-generates a city name, looks up the current time in that city with code
+generates a city name, looks up the current time in that city with a code
function, and the final agent reports the information.
-```python
-from google.adk import Agent
-from google.adk import Workflow
-from google.adk import Event
-from pydantic import BaseModel
-
-city_generator_agent = Agent(
- name="city_generator_agent",
- model="gemini-flash-latest",
- instruction="""Return the name of a random city.
- Return only the name, nothing else.""",
- output_schema=str,
-)
-
-class CityTime(BaseModel):
- time_info: str # time information
- city: str # city name
-
-def lookup_time_function(node_input: str):
- """Simulate returning the current time in the specified city."""
- return CityTime(time_info="10:10 AM", city=node_input)
-
-city_report_agent = Agent(
- name="city_report_agent",
- model="gemini-flash-latest",
- input_schema=CityTime,
- instruction="""Output following line:
- It is {CityTime.time_info} in {CityTime.city} right now.""",
- output_schema=str,
-)
-
-def completed_message_function(node_input: str):
- return Event(
- message=f"{node_input}\n WORKFLOW COMPLETED.",
+=== "Python"
+
+ ```python
+ from google.adk import Agent
+ from google.adk import Workflow
+ from google.adk import Event
+ from pydantic import BaseModel
+
+ city_generator_agent = Agent(
+ name="city_generator_agent",
+ model="gemini-flash-latest",
+ instruction="""Return the name of a random city.
+ Return only the name, nothing else.""",
+ output_schema=str,
)
-root_agent = Workflow(
- name="root_agent",
- edges=[
- ("START", city_generator_agent, lookup_time_function,
- city_report_agent, completed_message_function)
- ],
-)
-```
-
-This sample code demonstrates how you can use the ***Workflow*** class to
-assemble a simple, sequential workflow and alternate between AI agent processing
-and code execution. While you could perform these steps using a single agent
-with a longer prompt and a tool call, the graph-based approach gives you precise
-control over the task execution order and the data output from each step.
+ class CityTime(BaseModel):
+ time_info: str # time information
+ city: str # city name
+
+ def lookup_time_function(node_input: str):
+ """Simulate returning the current time in the specified city."""
+ return CityTime(time_info="10:10 AM", city=node_input)
+
+ city_report_agent = Agent(
+ name="city_report_agent",
+ model="gemini-flash-latest",
+ input_schema=CityTime,
+ instruction="""Output following line:
+ It is {CityTime.time_info} in {CityTime.city} right now.""",
+ output_schema=str,
+ )
+
+ def completed_message_function(node_input: str):
+ return Event(
+ message=f"{node_input}\n WORKFLOW COMPLETED.",
+ )
+
+ root_agent = Workflow(
+ name="root_agent",
+ edges=[
+ ("START", city_generator_agent, lookup_time_function,
+ city_report_agent, completed_message_function)
+ ],
+ )
+ ```
+
+=== "Go"
+
+ In ADK Go v2.0.0, sequential workflows use the graph engine:
+ `workflow.NewFunctionNode` wraps each step, and `workflow.Chain` wires
+ the nodes into a sequential `edges` slice. The framework automatically
+ passes each node's typed return value to the next node via
+ `event.Output` — no session state writes are needed. The whole graph is
+ wrapped in `workflowagent.New`, which produces a standard `agent.Agent`.
+
+ ```go
+ --8<-- "examples/go/snippets/graphs/index/main.go:sequential-get-started"
+ ```
+
+This sample code demonstrates how you can assemble a simple, sequential
+workflow and alternate between agent processing and code execution. While you
+could perform these steps using a single agent with a longer prompt and a tool
+call, the graph-based approach gives you precise control over the task
+execution order and the data output from each step.
For more information about data handling with graph-based workflows, see
[Data handling with workflow nodes and agents](/graphs/data-handling/).
@@ -121,52 +150,66 @@ switching between non-deterministic AI-powered agents and deterministic code as
needed.
The following code sample shows how the workflow graph in Figure 2 could be
-translated into a graph-based agent using the ***Workflow*** class:
-
-```python
-process_message = Agent(
- name="process_message",
- model="gemini-flash-latest",
- instruction="""Classify user message into either "BUG", "CUSTOMER_SUPPORT",
- or "LOGISTICS". If you think a message applies to more than one category,
- reply with a comma separated list of categories.
- """,
- output_schema=str,
-)
-
-def router(node_input: str):
- routes = node_input.split(",")
- routes = [route.strip() for route in routes]
- return Event(route=routes)
-
-def response_1_bug():
- return Event(message="Handling bug...")
-
-def response_2_support():
- return Event(message="Handling customer support...")
-
-def response_3_logistics():
- return Event(message="Handling logistics...")
-
-root_agent = Workflow(
- name="routing_workflow",
- edges=[
- ("START", process_message, router),
- ( router,
- {
- "BUG": response_1_bug,
- "CUSTOMER_SUPPORT": response_2_support,
- "LOGISTICS": response_3_logistics,
- }
- )
- ],
-)
-```
-
-This sample code demonstrates how you can use an ***edges*** array to define a
-graph with routes between a set of *nodes*, which are discrete tasks that can
-include agents, Tools, your code, and even additional ***Workflows***. For
-information about building advanced graphs for workflows, see
+translated into a graph-based agent:
+
+=== "Python"
+
+ ```python
+ process_message = Agent(
+ name="process_message",
+ model="gemini-flash-latest",
+ instruction="""Classify user message into either "BUG", "CUSTOMER_SUPPORT",
+ or "LOGISTICS". If you think a message applies to more than one category,
+ reply with a comma separated list of categories.
+ """,
+ output_schema=str,
+ )
+
+ def router(node_input: str):
+ routes = node_input.split(",")
+ routes = [route.strip() for route in routes]
+ return Event(route=routes)
+
+ def response_1_bug():
+ return Event(message="Handling bug...")
+
+ def response_2_support():
+ return Event(message="Handling customer support...")
+
+ def response_3_logistics():
+ return Event(message="Handling logistics...")
+
+ root_agent = Workflow(
+ name="routing_workflow",
+ edges=[
+ ("START", process_message, router),
+ ( router,
+ {
+ "BUG": response_1_bug,
+ "CUSTOMER_SUPPORT": response_2_support,
+ "LOGISTICS": response_3_logistics,
+ }
+ )
+ ],
+ )
+ ```
+
+=== "Go"
+
+ In ADK Go v2.0.0, conditional routing uses `workflow.NewEmittingFunctionNode`
+ to set `event.Routes` and `workflow.StringRoute` edges to dispatch to the
+ matching handler — the direct equivalent of Python's `router` function and
+ dict dispatch. `workflow.Concat` merges the chain and the conditional edges
+ into a single `edges` slice passed to `workflowagent.New`.
+
+ ```go
+ --8<-- "examples/go/snippets/graphs/index/main.go:process-pipeline"
+ ```
+
+This sample code demonstrates how you can compose a sequence of agents to
+define a graph with routes between a set of *nodes*, which are discrete tasks
+that can include agents, Tools, your code, and even additional workflow agents.
+For information about building advanced pipelines, see
[Build graph routes for workflow agents](/graphs/routes/).
## Known limitations {#known-limitations}
@@ -174,8 +217,23 @@ information about building advanced graphs for workflows, see
There are some known limitations with graph-based workflows. They
are *not compatible* with the following ADK features:
-- **Live Streaming** functionality is not compatible with graph-based
- workflows.
+- **Live streaming:** Not supported in graph-based workflows.
- **Integrations:** Some third-party
- [Integrations](/integrations/) may not be
- compatible with graph-based workflows.
+ [integrations](/integrations/) may not be compatible with graph-based
+ workflows.
+
+!!! note "Go: graph workflow API"
+
+ The `workflow` package in ADK Go v2.0.0 is the direct equivalent of the
+ Python `Workflow` class. Use `workflow.NewFunctionNode` and
+ `workflow.NewAgentNode` to define nodes, `workflow.Chain` or
+ `workflow.Concat` with `[]workflow.Edge` to wire them, and
+ `workflowagent.New` to wrap the graph as a runnable agent. Conditional
+ routing uses `workflow.StringRoute`, `workflow.IntRoute`, or
+ `workflow.BoolRoute` matched against `event.Routes`. Fan-in is handled by
+ `workflow.NewJoinNode`.
+
+ For advanced routing patterns and fan-out/join examples, see
+ [Build graph routes for workflow agents](/graphs/routes/). For prebuilt
+ higher-level alternatives (sequential, parallel, loop), see
+ [Prebuilt workflow agents](/agents/workflow-agents/).
diff --git a/docs/graphs/routes.md b/docs/graphs/routes.md
index 00b7c0c285..a8960e6546 100644
--- a/docs/graphs/routes.md
+++ b/docs/graphs/routes.md
@@ -1,7 +1,7 @@
# Build graph routes for agent workflows
- Supported in ADKPython v2.0.0
+ Supported in ADKPython v2.0.0Go v2.0.0
Graph-based workflows in ADK define agent logic as a graph of execution nodes
@@ -13,26 +13,53 @@ logic, this approach allows you to define a specific, step-wise process workflow
in code, providing improved precision and reliability over purely prompt-based
agents.
-
-
-```python
-root_agent = Workflow(
- name="routing_workflow",
- edges=[
- ("START", process_message, router),
- (router,
- {
- "output-1": response_1,
- "output-2": response_2,
- "output-3": response_3,
- },
- ),
- ],
-)
-```
-
-**Figure 1.** Visualization of a task graph and the ***Workflow*** code to
-implement it.
+
+
+**Figure 1.** Visualization of a task graph and the routing code to implement it.
+
+=== "Python"
+
+ ```python
+ root_agent = Workflow(
+ name="routing_workflow",
+ edges=[
+ ("START", process_message, router),
+ (router,
+ {
+ "output-1": response_1,
+ "output-2": response_2,
+ "output-3": response_3,
+ },
+ ),
+ ],
+ )
+ ```
+
+=== "Go"
+
+ ADK Go v2.0.0 provides the following approach to graph-based
+ workflows:
+
+ **Graph engine** (`workflowagent` + `workflow.Edge`): A node-and-edges
+ graph API that maps directly to Python's `Workflow(edges=[...])`.
+ Nodes are defined with `workflow.NewFunctionNode`, `workflow.NewAgentNode`,
+ or `workflow.NewDynamicNode`, edges are declared as `[]workflow.Edge`, and
+ the whole graph is wrapped in a `workflowagent.New` call:
+
+ ```go
+ edges := workflow.Concat(
+ workflow.Chain(workflow.Start, classifyNode),
+ []workflow.Edge{
+ {From: classifyNode, To: responseA, Route: workflow.StringRoute("output-1")},
+ {From: classifyNode, To: responseB, Route: workflow.StringRoute("output-2")},
+ {From: classifyNode, To: responseC, Route: workflow.StringRoute("output-3")},
+ },
+ )
+ rootAgent, _ := workflowagent.New(workflowagent.Config{
+ Name: "routing_workflow",
+ Edges: edges,
+ })
+ ```
The advantage of using a graph-based agent workflow is the significant increase
in control, predictability, and reliability over prompt-based agents. By
@@ -49,189 +76,364 @@ Get started with graph-based workflows in ADK by checking out
A graph is composed of execution nodes. These *nodes* can be ***Agents***, ADK
***Tools***, human input tasks, or code functions you write. Nodes can take
inputs from previously executed nodes, and emit data through ***Event***
-objects. The following shows a simple ***FunctionNode*** that handles text
-inputs and sends a text output:
+objects.
-```python
-from google.adk import Event
+=== "Python"
-def my_function_node(node_input: str):
- input_text_modified = node_input.upper()
- return Event(output=input_text_modified)
-```
+ The following shows a simple ***FunctionNode*** that handles text inputs
+ and sends a text output:
-For more information about transferring data between nodes, see .
-[Data handling for agent workflows](/graphs/data-handling/).
+ ```python
+ from google.adk import Event
-## Workflow graphs syntax
+ def my_function_node(node_input: str):
+ input_text_modified = node_input.upper()
+ return Event(output=input_text_modified)
+ ```
+
+=== "Go"
-You define a graph by creating an ***edges*** array, which defines a logical
-execution path of *nodes* and conditions to be followed. This section
-provides an overview of graph syntax in an ***edges*** array. The following code
-example shows a basic workflow with two nodes to be executed in order:
+ In ADK Go v2.0.0, the primary node type is `workflow.NewFunctionNode`.
+ A `FunctionNode` wraps a plain Go function: the function returns a typed
+ value, and the framework automatically wraps it in a `session.Event`,
+ setting `event.Output`. The successor node receives this value as its
+ typed `input` parameter — no manual state writes or event construction
+ needed:
-```python
-from google.adk import Workflow
+ ```go
+ --8<-- "examples/go/snippets/graphs/routes/main.go:function-node"
+ ```
-root_agent = Workflow(
- name="sequential_workflow",
- edges=[("START", task_A_node, task_B_node)],
-)
-```
+For more information about transferring data between nodes, see
+[Data handling for agent workflows](/graphs/data-handling/).
-!!! caution "Caution: Workflows and agent limitations"
+## Workflow graphs syntax
- You can add ***Agents***, or ***LlmAgents***, to graph-based workflows,
- however they must be set to a task or single-turn mode. For more
- information about agent modes, see
+You define a graph by composing workflow agents. This section provides an
+overview of the common routing patterns.
+
+!!! caution "Caution: Workflow agent limitations"
+
+ You can add ***LlmAgents*** to graph-based workflows. However, they must
+ be configured for single-turn or task mode. For more information about
+ agent modes, see
[Build collaborative agent teams](/workflows/collaboration/#mode-configuration-and-behaviors).
### Route sequences
-The ***edges*** array executes nodes based on the order or nodes presented in
-the array, starting with the first row and proceeding through the subsequent
-rows until execution is complete. The first row of the ***edges*** array uses
-the ***START*** keyword to indicate the beginning of a graph execution, with
-each listed node executed in sequence, as shown in the following code
-snippets:
-
-```python
-edges=[("START", task_A_node)] # single node run
-edges=[("START",
- task_A_node,
- task_B_node,
- task_C_node)] # 3 nodes run in order
-```
-
-You can also use ***START*** more than once to initiate parallel tasks at the
-beginning of a workflow graph, as shown in the following code snippet:
-
-```python
-edges=[
- ("START", parallel_task_A),
- ("START", parallel_task_B),
- ("START", parallel_task_C),
-]
-```
-
-!!! warning "Caution: Limitations on parallel nodes"
-
- Not all workflow nodes or subagents can be run in parallel. In particular,
- you cannot run multiple interactive chat sessions within the same agent
- session.
+A sequential route runs each node once, in the listed order.
-### Route branches and conditional execution
+=== "Python"
-The subsequent rows of the ***edges*** arrays after the START keyword define
-additional execution logic for nodes. For branching paths, which is how you create a conditional node, you define a node,
-usually a ***FunctionNode***, that outputs an Event with a specific ***route*** value. In the edges graph, you then define the conditional execution logic by mapping these route values to target nodes, as shown in the following code example:
+ The `edges` array uses the `START` keyword to indicate the beginning of a
+ graph execution, with each listed node executed in sequence:
-```python
-def router(node_input: str):
- """Route to task B or C based on node_input."""
- if condition(node_input):
- return Event(route="RUN_TASK_C")
- return Event(route="RUN_TASK_B")
+ ```python
+ edges=[("START", task_A_node)] # single node run
+ edges=[("START",
+ task_A_node,
+ task_B_node,
+ task_C_node)] # 3 nodes run in order
+ ```
-task_B_node = Agent(name="task_B_agent") # An agent to execute node B
+=== "Go"
-def task_C_node(node_input: str):
- """A FunctionNode to execute node C."""
- return Event(output="Task C completed")
+ `workflow.Chain(workflow.Start, nodeA, nodeB, nodeC)` wires nodes into a
+ sequential edge slice. Each node's typed return value is forwarded to the
+ next node via `event.Output` — no session state writes needed:
-root_agent = Workflow(
- name="routing_workflow",
- edges=[
- ("START", task_A_node, router),
- (router,
- {
- # "route value": node_to_run
- "RUN_TASK_B": task_B_node,
- "RUN_TASK_C": task_C_node,
- },
- ),
- ],
-)
-```
+ ```go
+ --8<-- "examples/go/snippets/graphs/routes/main.go:sequential-nodes"
+ ```
+
+### Route branches and conditional execution
+
+=== "Python"
+
+ In Python, branching is handled by a `FunctionNode` that returns an
+ `Event(route=...)` value, which the `edges` dict dispatches to different nodes.
+
+ ```python
+ def router(node_input: str):
+ """Route to task B or C based on node_input."""
+ if condition(node_input):
+ return Event(route="RUN_TASK_C")
+ return Event(route="RUN_TASK_B")
+
+ task_B_node = Agent(name="task_B_agent") # An agent to execute node B
+
+ def task_C_node(node_input: str):
+ """A FunctionNode to execute node C."""
+ return Event(output="Task C completed")
+
+ root_agent = Workflow(
+ name="routing_workflow",
+ edges=[
+ ("START", task_A_node, router),
+ (router,
+ {
+ # "route value": node_to_run
+ "RUN_TASK_B": task_B_node,
+ "RUN_TASK_C": task_C_node,
+ },
+ ),
+ ],
+ )
+ ```
+
+=== "Go"
+
+ In ADK Go v2.0.0, conditional dispatch uses the `workflow` graph engine.
+ A node sets `Event.Routes` to one or more string route keys, and each
+ `workflow.Edge` selects its successor using a `workflow.Route` matcher:
+
+ - `workflow.StringRoute("category")` — matches a single string value
+ - `workflow.IntRoute(n)` or `workflow.MultiRoute[int]{1, 2, 3}` — matches
+ integer values
+ - `workflow.BoolRoute(true)` — matches a boolean value
+ - `workflow.Default` — matches when no other route on the same source
+ node matches
+
+ The following pattern is the Go equivalent of the Python router:
+
+ ```go
+ // classifyNode emits an Event with Routes=[]string{"BUG"},
+ // ["CUSTOMER_SUPPORT"], or ["LOGISTICS"] based on the message.
+ edges := workflow.Concat(
+ workflow.Chain(workflow.Start, processMessage, classifyNode),
+ []workflow.Edge{
+ {From: classifyNode, To: bugHandler, Route: workflow.StringRoute("BUG")},
+ {From: classifyNode, To: supportHandler, Route: workflow.StringRoute("CUSTOMER_SUPPORT")},
+ {From: classifyNode, To: logisticsHandler, Route: workflow.StringRoute("LOGISTICS")},
+ },
+ )
+ rootAgent, _ := workflowagent.New(workflowagent.Config{
+ Name: "routing_workflow",
+ Edges: edges,
+ })
+ ```
+
+ `workflow.EdgeBuilder` provides a fluent alternative to assembling the
+ `[]workflow.Edge` slice by hand. The builder's `Add`, `AddFanOut`, and
+ `AddFanIn` methods express the same topology with less repetition:
+
+ ```go
+ eb := workflow.NewEdgeBuilder()
+ eb.Add(workflow.Start, processMessage)
+ eb.Add(processMessage, classifyNode)
+ eb.AddRoute(classifyNode, bugHandler, workflow.StringRoute("BUG"))
+ eb.AddRoute(classifyNode, supportHandler, workflow.StringRoute("CUSTOMER_SUPPORT"))
+ eb.AddRoute(classifyNode, logisticsHandler, workflow.StringRoute("LOGISTICS"))
+
+ rootAgent, _ := workflowagent.New(workflowagent.Config{
+ Name: "routing_workflow",
+ Edges: eb.Build(),
+ })
+ ```
+
+ For complete, runnable routing examples see:
+ [string routing](https://github.com/google/adk-go/tree/v2/examples/workflow/routing/string),
+ [int / multi-value routing](https://github.com/google/adk-go/tree/v2/examples/workflow/routing/int),
+ and [LLM-driven routing](https://github.com/google/adk-go/tree/v2/examples/workflow/routing/llm).
+
+ !!! note "Prebuilt agents: encoding routing in state"
+
+ When using `sequentialagent` / `parallelagent` / `loopagent` instead
+ of the graph engine, there is no `Event.Routes` dispatch. Encode the
+ routing decision in session state via `OutputKey` and let downstream
+ agents inspect it in their `Instruction` template, or use a `loopagent`
+ with an `Escalate`-based exit — see the
+ [loop and escalation example](#loop-and-escalation-exit) below.
## Parallel tasks: fan out and join paths
You can create graphs that split execution across multiple, parallel nodes, and
typically you need to assemble the output of each node for further processing.
-This task execution pattern has two stages. The workflow first fans out when it
-starts multiple parallel tasks, and then it re-joins those paths when those
-those tasks are completed before proceeding to the next step.
-
-You accomplish the join step by using a ***JoinNode*** object, which waits for
-each parallel task to complete and then passes the collection of outputs from
-these nodes to the next node.
+This task execution pattern has two stages. The workflow first fans out when it
+starts multiple parallel tasks, and then it re-joins those paths when those
+tasks are completed before proceeding to the next step.

-**Figure 2.** The output of parallel task nodes can be assembled using a
-JoinNode object.
-
-The following code snippet shows how to start three parallel tasks from
-***START*** and use a basic ***JoinNode*** object to join their outputs before
-running the final task:
+**Figure 2.** The output of parallel task nodes can be assembled and joined
+before passing results to the next step.
-```python
-from google.adk.workflow import JoinNode
+=== "Python"
-my_join_node = JoinNode(name="my_join_node")
+ You accomplish the join step by using a ***JoinNode*** object, which waits
+ for each parallel task to complete and then passes the collection of outputs
+ from these nodes to the next node.
-edges=[
- ("START", parallel_task_A, my_join_node),
- ("START", parallel_task_B, my_join_node),
- ("START", parallel_task_C, my_join_node),
- (my_join_node, final_task_D),
-]
-```
+ ```python
+ from google.adk.workflow import JoinNode
-!!! warning "Caution: Stuck JoinNode from incomplete nodes"
+ my_join_node = JoinNode(name="my_join_node")
- The ***JoinNode*** object proceeds only after all its upstream nodes have
- provided an Event output. If one of the upstream nodes fails to provide output,
- the JoinNode is stuck and workflow execution stops. Make sure to include
- failsafe output from any node that outputs to a ***JoinNode***.
+ edges=[
+ ("START", parallel_task_A, my_join_node),
+ ("START", parallel_task_B, my_join_node),
+ ("START", parallel_task_C, my_join_node),
+ (my_join_node, final_task_D),
+ ]
+ ```
+
+ !!! warning "Caution: Stuck JoinNode from incomplete nodes"
+
+ The ***JoinNode*** object proceeds only after all its upstream nodes
+ have provided an Event output. If one of the upstream nodes fails to
+ provide output, the JoinNode is stuck and workflow execution stops.
+ Make sure to include failsafe output from any node that outputs to a
+ ***JoinNode***.
+
+=== "Go"
+
+ ADK Go v2.0.0 provides `workflow.NewJoinNode` for true fan-in in the
+ graph engine: fan-out edges from `workflow.Start` (or any shared source
+ node) feed in parallel to the join node, which waits for all of them to
+ complete before emitting a `map[string]any` keyed by predecessor node name
+ to the next node.
+
+ `workflow.EdgeBuilder` makes the fan-out / fan-in wiring concise with its
+ dedicated `AddFanOut` and `AddFanIn` helpers (as shown in the
+ [complex workflow example](https://github.com/google/adk-go/tree/v2/examples/workflow/complex)):
+
+ ```go
+ gatherNode := workflow.NewJoinNode("gather")
+
+ eb := workflow.NewEdgeBuilder()
+ eb.AddFanOut(workflow.Start, researchNodeA, researchNodeB, researchNodeC)
+ eb.AddFanIn(gatherNode, researchNodeA, researchNodeB, researchNodeC)
+ eb.Add(gatherNode, formatNode)
+ eb.Add(formatNode, synthesisNode)
+
+ rootAgent, _ := workflowagent.New(workflowagent.Config{
+ Name: "research_pipeline",
+ Edges: eb.Build(),
+ })
+ ```
+
+ The following snippet shows the complete fan-out / join pattern using
+ `workflow.NewJoinNode` and `EdgeBuilder.AddFanOut` / `AddFanIn`:
+
+ ```go
+ --8<-- "examples/go/snippets/graphs/routes/main.go:parallel-fan-out"
+ ```
+
+ !!! warning "Caution: Stuck JoinNode from incomplete nodes"
+
+ `workflow.NewJoinNode` proceeds only after every predecessor node has
+ emitted an `event.Output`. If a predecessor fails without emitting
+ output, the JoinNode is stuck and workflow execution stops. Attach a
+ `RetryConfig` to flaky predecessor nodes to guard against transient
+ failures.
## Nested workflows
When building more complex workflows, you may want to encapsulate the
functionality for specific tasks into reusable workflows. One or more
-***Workflow*** objects can be used as a node within the graph of another
-workflow agent to accomplish this goal.
+workflow agents can be used as a sub-agent within another workflow agent to
+accomplish this goal.

-**Figure 3.** Nested ***Workflows*** as nodes inside a parent ***Workflow***.
-
-The following code snippet shows how to implement a workflow agent with two
-nested more ***Workflow*** objects (workflow_B, workflow_C) as nodes in the
-graph:
-
-```python
-from google.adk import Workflow
-
-root_agent = Workflow(
- name="parent_workflow",
- edges=[
- ("START", task_A1, router),
- (router, {
- "RUN_WORKFLOW_B": workflow_B,
- "RUN_WORKFLOW_C": workflow_C,
- },
- ),
- ],
-)
-```
-
-### Nested workflow data output
-
-Output for nested Workflow objects works slightly differently from individual
-nodes. When the nested workflow completes one of its nodes, it transmits data
-to the next node in the nested workflow's graph *and* the system bubbles up the
-Event for that node to the parent workflow for process traceability. When the
-nested workflow completes the last node in its process, the parent node extracts
-data from the final leaf nodes and emits it as the output of the nested
-workflow.
+**Figure 3.** Nested workflow agents as sub-agents inside a parent workflow.
+
+=== "Python"
+
+ ```python
+ from google.adk import Workflow
+
+ root_agent = Workflow(
+ name="parent_workflow",
+ edges=[
+ ("START", task_A1, router),
+ (router, {
+ "RUN_WORKFLOW_B": workflow_B,
+ "RUN_WORKFLOW_C": workflow_C,
+ },
+ ),
+ ],
+ )
+ ```
+
+ #### Nested workflow data output
+
+ Output for nested Workflow objects works slightly differently from
+ individual nodes. When the nested workflow completes one of its nodes, it
+ transmits data to the next node in the nested workflow's graph *and* the
+ system bubbles up the Event for that node to the parent workflow for
+ process traceability. When the nested workflow completes the last node in
+ its process, the parent node extracts data from the final leaf nodes and
+ emits it as the output of the nested workflow.
+
+=== "Go"
+
+ ADK Go v2.0.0 supports nested workflows in two complementary ways:
+
+ **Graph engine** (`workflowagent` + `workflow.Edge`): A `workflowagent`
+ created with `workflowagent.New` is itself an `agent.Agent`, so it can
+ be wrapped with `workflow.NewAgentNode` and used as a node inside another
+ workflow's `edges` slice. The inner workflow runs to completion as a single
+ node from the outer graph's perspective, and its terminal output is emitted
+ as the node output on the outer graph's edge:
+
+ ```go
+ innerNode, _ := workflow.NewAgentNode(innerWorkflowAgent, workflow.NodeConfig{})
+
+ outerEdges := workflow.Chain(workflow.Start, outerStepNode, innerNode, finalNode)
+ rootAgent, _ := workflowagent.New(workflowagent.Config{
+ Name: "parent_workflow",
+ Edges: outerEdges,
+ })
+ ```
+
+ The following snippet shows both the inner and outer graph construction.
+ `workflow.NewAgentNode` wraps the inner `workflowagent` so it can be
+ placed in the outer graph's `workflow.Chain`:
+
+ ```go
+ --8<-- "examples/go/snippets/graphs/routes/main.go:nested-workflows"
+ ```
+
+## Loop and escalation exit
+
+A loop repeats a set of steps until a termination condition is met. In Python
+this is expressed as a back-edge in the `edges` graph that routes back to an
+earlier node. In ADK Go v2.0.0, the graph engine supports the same pattern
+directly: add an edge from a downstream node back to an earlier node with a
+route condition, and the engine re-activates the target node with a fresh
+lifecycle on each iteration.
+
+=== "Python"
+
+ ```python
+ def router(node_input: str):
+ """Route to task B or C based on node_input."""
+ if condition(node_input):
+ return Event(route="RUN_TASK_C")
+ return Event(route="RUN_TASK_B")
+
+ root_agent = Workflow(
+ name="routing_workflow",
+ edges=[
+ ("START", task_A_node, router),
+ (router,
+ {
+ "RUN_TASK_B": task_B_node,
+ "RUN_TASK_C": task_C_node,
+ },
+ ),
+ ],
+ )
+ ```
+
+=== "Go"
+
+ The following example uses the graph engine with `workflow.EdgeBuilder`.
+ The critic node returns a verdict, a router node sets `Event.Routes`, and
+ a back-edge from the refiner to the critic creates the loop. When the
+ critic is satisfied it routes to the terminal `done` node instead:
+
+ ```go
+ --8<-- "examples/go/snippets/graphs/routes/main.go:loop-escalate"
+ ```
diff --git a/docs/grounding/grounding_with_search.md b/docs/grounding/grounding_with_search.md
index 1b7a86f0f1..2df534670f 100644
--- a/docs/grounding/grounding_with_search.md
+++ b/docs/grounding/grounding_with_search.md
@@ -22,7 +22,7 @@ Before creating a grounded agent, you must have an existing Agent Search Data St
* For Java, ensure your application environment has Google Cloud default credentials configured (`GOOGLE_APPLICATION_CREDENTIALS`).
```env title=".env"
-GOOGLE_GENAI_USE_VERTEXAI=TRUE
+GOOGLE_GENAI_USE_ENTERPRISE=TRUE
GOOGLE_CLOUD_PROJECT=YOUR_PROJECT_ID
GOOGLE_CLOUD_LOCATION=LOCATION
```
diff --git a/docs/integrations/bigquery-agent-analytics.md b/docs/integrations/bigquery-agent-analytics.md
index 4bb6d35279..568fb4b619 100644
--- a/docs/integrations/bigquery-agent-analytics.md
+++ b/docs/integrations/bigquery-agent-analytics.md
@@ -146,7 +146,7 @@ shows the BigQuery view optionally created when
os.environ['GOOGLE_CLOUD_PROJECT'] = 'your-gcp-project-id'
os.environ['GOOGLE_CLOUD_LOCATION'] = 'us-central1'
- os.environ['GOOGLE_GENAI_USE_VERTEXAI'] = 'True'
+ os.environ['GOOGLE_GENAI_USE_ENTERPRISE'] = 'True'
plugin = BigQueryAgentAnalyticsPlugin(
project_id="your-gcp-project-id",
@@ -274,7 +274,7 @@ LIMIT 20;
# --- CRITICAL: Set environment variables BEFORE Gemini instantiation ---
os.environ['GOOGLE_CLOUD_PROJECT'] = PROJECT_ID
os.environ['GOOGLE_CLOUD_LOCATION'] = VERTEX_LOCATION
- os.environ['GOOGLE_GENAI_USE_VERTEXAI'] = 'True'
+ os.environ['GOOGLE_GENAI_USE_ENTERPRISE'] = 'True'
# --- Initialize the Plugin with Config ---
bq_config = BigQueryLoggerConfig(
@@ -1674,7 +1674,7 @@ DATASET_ID = os.environ.get("BQ_DATASET", "agent_analytics")
# region used by GOOGLE_CLOUD_LOCATION.
BQ_LOCATION = os.environ.get("BQ_LOCATION", "US")
-os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True"
+os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "True"
# --- Plugin ---
bq_analytics_plugin = BigQueryAgentAnalyticsPlugin(
diff --git a/docs/integrations/cloud-trace.md b/docs/integrations/cloud-trace.md
index e8d7ad2de0..2e08bfae08 100644
--- a/docs/integrations/cloud-trace.md
+++ b/docs/integrations/cloud-trace.md
@@ -44,7 +44,7 @@ working_dir/
os.environ.setdefault("GOOGLE_CLOUD_PROJECT", "{your-project-id}")
os.environ.setdefault("GOOGLE_CLOUD_LOCATION", "global")
- os.environ.setdefault("GOOGLE_GENAI_USE_VERTEXAI", "True")
+ os.environ.setdefault("GOOGLE_GENAI_USE_ENTERPRISE", "True")
# Define a tool function
diff --git a/docs/integrations/express-mode.md b/docs/integrations/express-mode.md
index 23df6db1ca..958cf6735d 100644
--- a/docs/integrations/express-mode.md
+++ b/docs/integrations/express-mode.md
@@ -47,7 +47,7 @@ With this approach, `Session` objects are handled as children of the
variables are set correctly, as shown below:
```env title="agent/.env"
-GOOGLE_GENAI_USE_VERTEXAI=TRUE
+GOOGLE_GENAI_USE_ENTERPRISE=TRUE
GOOGLE_API_KEY=PASTE_YOUR_ACTUAL_EXPRESS_MODE_API_KEY_HERE
```
@@ -90,7 +90,7 @@ the session object without any project or location.
```py
# Requires: pip install google-adk[vertexai]
# Plus environment variable setup:
-# GOOGLE_GENAI_USE_VERTEXAI=TRUE
+# GOOGLE_GENAI_USE_ENTERPRISE=TRUE
# GOOGLE_API_KEY=PASTE_YOUR_ACTUAL_EXPRESS_MODE_API_KEY_HERE
from google.adk.sessions import VertexAiSessionService
@@ -119,7 +119,7 @@ the memory object without any project or location.
```py
# Requires: pip install google-adk[vertexai]
# Plus environment variable setup:
-# GOOGLE_GENAI_USE_VERTEXAI=TRUE
+# GOOGLE_GENAI_USE_ENTERPRISE=TRUE
# GOOGLE_API_KEY=PASTE_YOUR_ACTUAL_EXPRESS_MODE_API_KEY_HERE
from google.adk.memory import VertexAiMemoryBankService
diff --git a/docs/integrations/galileo.md b/docs/integrations/galileo.md
index 53aa233239..3e4490f35d 100644
--- a/docs/integrations/galileo.md
+++ b/docs/integrations/galileo.md
@@ -38,7 +38,7 @@ Configure environment variables:
```env title="my_agent/.env"
# Gemini environment variables
-GOOGLE_GENAI_USE_VERTEXAI=0
+GOOGLE_GENAI_USE_ENTERPRISE=0
GOOGLE_API_KEY="YOUR_API_KEY"
# Galileo environment variables
diff --git a/docs/optimize/index.md b/docs/optimize/index.md
index 6c6501dfdc..ea9e7cd10c 100644
--- a/docs/optimize/index.md
+++ b/docs/optimize/index.md
@@ -255,6 +255,43 @@ Defaults to 3.
optimization results if desired.
Facilitates warm starts.
+### `GEPARootAgentOptimizer` {#geparootagentoptimizer}
+
+The
+[`GEPARootAgentOptimizer`](https://github.com/google/adk-python/blob/main/src/google/adk/optimization/gepa_root_agent_optimizer.py)
+improves both the instructions of the root agent and the instructions of skills
+provided to it via a
+[`SkillToolset`](https://github.com/google/adk-python/blob/main/src/google/adk/tools/skill_toolset.py)
+using the [GEPA](https://gepa-ai.github.io/gepa/) optimizer.
+In many ways it can be considered to be an extension of the
+[`GEPARootAgentPromptOptimizer`](#geparootagentpromptoptimizer).
+It expects the sampler to provide eval results as an
+[`UnstructuredSamplingResult`](#sampler-results).
+Its output is a subclass of [`OptimizerResult`](#agent-optimizer-results) which
+specifies a list of [optimized agents with scores](#agent-optimizer-results) and
+additional metrics collected during optimization.
+
+Note: The `GEPARootAgentOptimizer` does not improve any sub-agents or agent
+tools.
+
+You can configure the `GEPARootAgentOptimizer` with a
+`GEPARootAgentOptimizerConfig` that contains the following fields:
+
+* `optimizer_model` (optional): The model used to analyze evaluation results and
+optimize the agent.
+Defaults to `"gemini-3.5-flash"`.
+* `model_configuration` (optional): The configuration for the optimizer model.
+Defaults to a config with a `ThinkingLevel` of `HIGH`.
+* `max_metric_calls` (optional): The maximum number of evaluations to run during
+optimization.
+Defaults to 100.
+* `reflection_minibatch_size` (optional): The number of examples to use at a
+time to update the instructions.
+Defaults to 3.
+* `run_dir` (optional): The directory to save intermediate and final
+optimization results if desired.
+Facilitates warm starts.
+
### `SimplePromptOptimizer` {#simplepromptoptimizer}
The `SimplePromptOptimizer` is an automated, iterative prompt-tuning component designed
@@ -272,7 +309,7 @@ The optimizer automatically executes an asynchronous, four-stage feedback loop:
**Note:** The optimization loop does not mutate your initial agent instance in place. Upon completion, it returns an `OptimizerResult` containing the highest-scoring agent variation extracted during the process.
-### Configuration
+#### Configuration
Configure the behavior of the loop by passing a `SimplePromptOptimizerConfig` instance to the optimizer.
@@ -281,7 +318,7 @@ Configure the behavior of the loop by passing a `SimplePromptOptimizerConfig` in
| `num_iterations` | int | *Required* | The total number of optimization rounds to execute. |
| `batch_size` | int | *Required* | The number of evaluation sample cases processed by the sampler during each individual iteration. |
-### Implementation Example
+#### Implementation Example
Once your configuration is defined, run the optimization with:
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 000025244e..9966bb69e8 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -8,3 +8,10 @@ language. For detailed information on ADK releases, see these locations:
* [ADK Go release notes](https://github.com/google/adk-go/releases)
* [ADK Java release notes](https://github.com/google/adk-java/releases)
* [ADK Kotlin release notes](https://github.com/google/adk-kotlin/releases)
+
+!!! tip "ADK Go v2.0.0"
+
+ ADK Go v2.0.0 introduces graph-based and dynamic workflow support, a new
+ `workflow` package, agent execution modes, and Human-in-the-Loop tool
+ confirmation. See the [ADK 2.0 release page](/2.0/) for the full feature
+ list and Go 1.x migration guidance.
diff --git a/docs/streaming/dev-guide/part1.md b/docs/streaming/dev-guide/part1.md
index 7c788f08f7..3ef6fafe9b 100644
--- a/docs/streaming/dev-guide/part1.md
+++ b/docs/streaming/dev-guide/part1.md
@@ -206,10 +206,10 @@ One of ADK's most powerful features is its transparent support for both [Gemini
#### How Platform Selection Works
-ADK uses the `GOOGLE_GENAI_USE_VERTEXAI` environment variable to determine which Live API platform to use:
+ADK uses the `GOOGLE_GENAI_USE_ENTERPRISE` environment variable to determine which Live API platform to use:
-- `GOOGLE_GENAI_USE_VERTEXAI=FALSE` (or not set): Uses Gemini Live API via Google AI Studio
-- `GOOGLE_GENAI_USE_VERTEXAI=TRUE`: Uses Gemini Live API (Agent Platform) via Google Cloud
+- `GOOGLE_GENAI_USE_ENTERPRISE=FALSE` (or not set): Uses Gemini Live API via Google AI Studio
+- `GOOGLE_GENAI_USE_ENTERPRISE=TRUE`: Uses Gemini Live API (Agent Platform) via Google Cloud
This environment variable is read by the underlying `google-genai` SDK when ADK creates the LLM connection. No code changes are needed when switching platforms—only environment configuration changes.
@@ -217,7 +217,7 @@ This environment variable is read by the underlying `google-genai` SDK when ADK
```bash
# .env.development
-GOOGLE_GENAI_USE_VERTEXAI=FALSE
+GOOGLE_GENAI_USE_ENTERPRISE=FALSE
GOOGLE_API_KEY=your_api_key_here
```
@@ -232,7 +232,7 @@ GOOGLE_API_KEY=your_api_key_here
```bash
# .env.production
-GOOGLE_GENAI_USE_VERTEXAI=TRUE
+GOOGLE_GENAI_USE_ENTERPRISE=TRUE
GOOGLE_CLOUD_PROJECT=your_project_id
GOOGLE_CLOUD_LOCATION=us-central1
```
diff --git a/docs/streaming/streaming-tools.md b/docs/streaming/streaming-tools.md
index 35d7cd4d12..9eeaafcdef 100644
--- a/docs/streaming/streaming-tools.md
+++ b/docs/streaming/streaming-tools.md
@@ -68,7 +68,7 @@ Now let's define an agent that can monitor stock price changes and monitor the v
) -> AsyncGenerator[str, None]:
"""Monitor how many people are in the video streams."""
print("start monitor_video_stream!")
- client = Client(vertexai=False)
+ client = Client(enterprise=False)
prompt_text = (
"Count the number of people in this image. Just respond with a numeric"
" number."
diff --git a/docs/tutorials/agent-team.md b/docs/tutorials/agent-team.md
index b63c0c67b6..a79743a1f2 100644
--- a/docs/tutorials/agent-team.md
+++ b/docs/tutorials/agent-team.md
@@ -117,7 +117,7 @@ print(f"OpenAI API Key set: {'Yes' if os.environ.get('OPENAI_API_KEY') and os.en
print(f"Anthropic API Key set: {'Yes' if os.environ.get('ANTHROPIC_API_KEY') and os.environ['ANTHROPIC_API_KEY'] != 'YOUR_ANTHROPIC_API_KEY' else 'No (REPLACE PLACEHOLDER!)'}")
# Configure ADK to use API keys directly (not Agent Platform for this multi-model setup)
-os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "False"
+os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "False"
# @markdown **Security Note:** It's best practice to manage API keys securely (e.g., using Colab Secrets or environment variables) rather than hardcoding them directly in the notebook. Replace the placeholder strings above.
diff --git a/docs/workflows/collaboration.md b/docs/workflows/collaboration.md
index b0d690b2ca..bffae32a49 100644
--- a/docs/workflows/collaboration.md
+++ b/docs/workflows/collaboration.md
@@ -1,7 +1,7 @@
# Build collaborative agent teams
- Supported in ADKPython v2.0.0
+ Supported in ADKPython v2.0.0Go v2.0.0
Some complex tasks may require multiple agents with specific responsibilities
@@ -39,27 +39,40 @@ agent behavior.
The following code example shows how to set operating modes for
a small team of subagents and assign them to a coordinator agent:
-```python
-from google.adk import Agent
-
-weather_agent = Agent(
- name="weather_checker",
- mode="single_turn", # no user interaction
- tools=[get_weather, user_info, geocode_address],
-)
-flight_agent = Agent(
- name="flight_booker",
- mode="task", # can ask user questions
- input_schema=FlightInput,
- output_schema=FlightResult,
- tools=[search_flights, book_flight],
-)
-root = Agent(
- name="travel_planner", # coordinator agent
- sub_agents=[weather_agent, flight_agent],
- # Auto-injects: request_task_weather_checker, request_task_flight_booker
-)
-```
+=== "Python"
+
+ ```python
+ from google.adk import Agent
+
+ weather_agent = Agent(
+ name="weather_checker",
+ mode="single_turn", # no user interaction
+ tools=[get_weather, user_info, geocode_address],
+ )
+ flight_agent = Agent(
+ name="flight_booker",
+ mode="task", # can ask user questions
+ input_schema=FlightInput,
+ output_schema=FlightResult,
+ tools=[search_flights, book_flight],
+ )
+ root = Agent(
+ name="travel_planner", # coordinator agent
+ sub_agents=[weather_agent, flight_agent],
+ # Auto-injects: request_task_weather_checker, request_task_flight_booker
+ )
+ ```
+
+=== "Go"
+
+ In ADK Go v2.0.0, the `Mode` field on `llmagent.Config` accepts the same
+ mode strings as Python: `"chat"`, `"task"`, and `"single_turn"`. Declaring
+ `SubAgents` on the coordinator agent causes ADK to automatically generate
+ `request_task_` delegation tools, exactly as in Python.
+
+ ```go
+ --8<-- "examples/go/snippets/workflows/collaboration/main.go:get-started"
+ ```
When you run this workflow, the `travel_planner` coordinator agent automatically
identifies and assigns tasks to the subagents. When a subagent completes
@@ -139,10 +152,12 @@ Workflow Agent graph nodes, and with ***LlmAgent*** instances. However the
execution transfer behavior is different depending on the calling, or parent,
agent:
-**As a workflow graph node:** When a task agent is placed within a workflow
-graph, such as ***SequentialAgent***, ***ParallelAgent***, the agent executes
-its task. Upon completion, control automatically advances to the next node based
-on the logic of the workflow agent's graph.
+**As a workflow graph node:** When a task or single-turn agent is placed within
+a workflow graph — such as a ***SequentialAgent*** or ***ParallelAgent*** (Python
+and Go prebuilt agents), or wrapped with `workflow.NewAgentNode` in the ADK Go
+v2.0.0 graph engine — the agent executes its task. Upon completion, control
+automatically advances to the next node based on the logic of the workflow
+agent's graph.
**As a transferee from an LlmAgent:** When a parent ***LlmAgent*** transfers
control to a task agent via `request_task`, the task agent executes until it
diff --git a/examples/go/go.mod b/examples/go/go.mod
index 9da1ff2dce..62535953cf 100644
--- a/examples/go/go.mod
+++ b/examples/go/go.mod
@@ -1,9 +1,10 @@
module snippets
-go 1.25.1
+go 1.25.0
require (
- google.golang.org/adk v1.3.0
+ google.golang.org/adk v1.4.0
+ google.golang.org/adk/v2 v2.0.0
google.golang.org/genai v1.57.0
)
diff --git a/examples/go/go.sum b/examples/go/go.sum
index 3b64a84a66..62c956ceb5 100644
--- a/examples/go/go.sum
+++ b/examples/go/go.sum
@@ -105,8 +105,10 @@ golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
-google.golang.org/adk v1.3.0 h1:paUr9uM2qANnMUAQ4ydMXMCnM1HtymhDYl8y7gnKvqs=
-google.golang.org/adk v1.3.0/go.mod h1:R8tNFnI/eiBXHn7zJPJtqdiK/WXC+tVkyuZsXyNZXN4=
+google.golang.org/adk v1.4.0 h1:Qi4KB9YKD00/I5K9v3QsZ9ng5YiZQ7MfMgM8BZjNcsM=
+google.golang.org/adk v1.4.0/go.mod h1:R8tNFnI/eiBXHn7zJPJtqdiK/WXC+tVkyuZsXyNZXN4=
+google.golang.org/adk/v2 v2.0.0 h1:7eRbsnv0XkQPVctf8qtQ+KuO8XjkBrMNxznY6OA/sTs=
+google.golang.org/adk/v2 v2.0.0/go.mod h1:fPuMPT5s3LsWu97mdeFjTPZu/02tIALWRWeqHL2FWKE=
google.golang.org/api v0.279.0 h1:hsx2M2OaRcaKtVYK6vXEUnQvdjnend7ZYES+lYaot74=
google.golang.org/api v0.279.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ=
google.golang.org/genai v1.57.0 h1:qTyG2ynz5dQy2jF4CvZdLHHVslhR0heMue+zM1a4GNM=
diff --git a/examples/go/snippets/graphs/data-handling/main.go b/examples/go/snippets/graphs/data-handling/main.go
new file mode 100644
index 0000000000..f021642246
--- /dev/null
+++ b/examples/go/snippets/graphs/data-handling/main.go
@@ -0,0 +1,412 @@
+// Copyright 2025 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.
+
+// Package main demonstrates data-handling patterns for ADK Go v2 workflow agents.
+//
+// NOTE: This file requires google.golang.org/adk/v2 (the workflow package),
+// available in ADK Go v2.0.0 and higher.
+//
+// # Data flow in ADK Go v2
+//
+// ADK Go v2 provides two complementary data-passing mechanisms depending on
+// which agent style you use:
+//
+// ## workflow package (graph engine: FunctionNode / AgentNode / DynamicNode)
+//
+// Nodes communicate by setting fields on session.Event:
+//
+// - Event.Output (any): the node's typed return value, set automatically by
+// the framework when a FunctionNode returns a non-*genai.Content value.
+// Successor nodes receive this as their typed `input` parameter via
+// workflow.RunNode.
+// - Event.Routes ([]string): routing keys a node emits to select which edge
+// to follow. Set explicitly by an emitting function node using
+// session.NewEvent + ev.Routes = []string{"category"}.
+// - Event.NodeInfo (*session.NodeInfo): scheduler metadata (path,
+// MessageAsOutput, OutputFor). Set by the workflow engine; nodes do not
+// set this directly.
+// - Event.Content (*genai.Content): when a FunctionNode returns a string or
+// *genai.Content, the framework stores it here for the LLM / user stream.
+//
+// ## Prebuilt workflow agents (sequentialagent / parallelagent / loopagent)
+//
+// These agents communicate through session state:
+//
+// - llmagent.Config.OutputKey: the framework writes the agent's final text
+// response to state[OutputKey] after each turn.
+// - ctx.Session().State().Set / .Get: write/read arbitrary values from state
+// inside custom code.
+// - {key} in Instruction: the framework substitutes state["key"] into the
+// prompt before calling the model.
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "strings"
+
+ "google.golang.org/genai"
+
+ "google.golang.org/adk/v2/agent"
+ "google.golang.org/adk/v2/agent/llmagent"
+ "google.golang.org/adk/v2/agent/workflowagent"
+ "google.golang.org/adk/v2/agent/workflowagents/sequentialagent"
+ "google.golang.org/adk/v2/model"
+ "google.golang.org/adk/v2/model/gemini"
+ "google.golang.org/adk/v2/session"
+ "google.golang.org/adk/v2/workflow"
+)
+
+// --8<-- [start:event-output]
+// newEventOutputPipeline demonstrates the primary data-passing mechanism for
+// workflow package nodes: a FunctionNode returns a typed Go value, and the
+// framework automatically sets event.Output to that value. The successor node
+// receives it as its typed `input` parameter.
+//
+// This mirrors the Python pattern exactly:
+//
+// def my_function_node(node_input: str):
+// return Event(output=node_input.upper())
+//
+// In Go, the function simply returns the value — no Event construction needed.
+func newEventOutputPipeline() (agent.Agent, error) {
+ upperFn := func(_ agent.Context, input string) (string, error) {
+ return strings.ToUpper(input), nil
+ }
+
+ suffixFn := func(_ agent.Context, input string) (string, error) {
+ return input + " IS AWESOME!", nil
+ }
+
+ nodeA := workflow.NewFunctionNode("upper", upperFn, workflow.NodeConfig{})
+ nodeB := workflow.NewFunctionNode("suffix", suffixFn, workflow.NodeConfig{})
+
+ // workflow.Chain wires START → nodeA → nodeB. The output of nodeA is
+ // delivered as the typed input of nodeB via event.Output.
+ return workflowagent.New(workflowagent.Config{
+ Name: "event_output_pipeline",
+ Description: "Demonstrates Event.Output data flow between FunctionNodes.",
+ Edges: workflow.Chain(workflow.Start, nodeA, nodeB),
+ })
+}
+
+// --8<-- [end:event-output]
+
+// --8<-- [start:routing-output]
+// classifyAndRoute shows how to set event.Routes alongside event.Output from
+// an emitting FunctionNode. The function constructs a session.Event directly,
+// sets Routes to select the conditional edge, and sets Output to forward the
+// payload to the successor node.
+//
+// This mirrors the Python pattern:
+//
+// def router(node_input: str):
+// return Event(route="BUG")
+func classifyAndRoute(ctx agent.Context, msg string, emit func(*session.Event) error) (any, error) {
+ category := classifyMessage(msg)
+
+ ev := session.NewEvent(ctx, ctx.InvocationID())
+ ev.Routes = []string{category} // drives edge dispatch
+ ev.Output = msg // forwarded as typed input to the successor
+ if err := emit(ev); err != nil {
+ return nil, err
+ }
+ return nil, nil // nil suppresses the automatic terminal event
+}
+
+func classifyMessage(msg string) string {
+ switch {
+ case strings.Contains(strings.ToLower(msg), "bug"):
+ return "BUG"
+ case strings.Contains(strings.ToLower(msg), "help"):
+ return "CUSTOMER_SUPPORT"
+ default:
+ return "LOGISTICS"
+ }
+}
+
+func newRoutingPipeline() (agent.Agent, error) {
+ classifyNode := workflow.NewEmittingFunctionNode("classify", classifyAndRoute, workflow.NodeConfig{})
+
+ bugHandler := workflow.NewFunctionNode("bug_handler",
+ func(_ agent.Context, msg string) (string, error) {
+ return "Handling bug: " + msg, nil
+ }, workflow.NodeConfig{})
+
+ supportHandler := workflow.NewFunctionNode("support_handler",
+ func(_ agent.Context, msg string) (string, error) {
+ return "Handling support: " + msg, nil
+ }, workflow.NodeConfig{})
+
+ logisticsHandler := workflow.NewFunctionNode("logistics_handler",
+ func(_ agent.Context, msg string) (string, error) {
+ return "Handling logistics: " + msg, nil
+ }, workflow.NodeConfig{})
+
+ edges := workflow.Concat(
+ workflow.Chain(workflow.Start, classifyNode),
+ []workflow.Edge{
+ {From: classifyNode, To: bugHandler, Route: workflow.StringRoute("BUG")},
+ {From: classifyNode, To: supportHandler, Route: workflow.StringRoute("CUSTOMER_SUPPORT")},
+ {From: classifyNode, To: logisticsHandler, Route: workflow.StringRoute("LOGISTICS")},
+ },
+ )
+ return workflowagent.New(workflowagent.Config{
+ Name: "routing_pipeline",
+ Description: "Classifies and routes a message using Event.Routes.",
+ Edges: edges,
+ })
+}
+
+// --8<-- [end:routing-output]
+
+// --8<-- [start:structured-output]
+// newStructuredOutputPipeline shows how to pass a struct from one FunctionNode
+// to another. The framework serialises the return value into event.Output and
+// deserialises it back into the successor's typed input parameter.
+//
+// This is the Go equivalent of:
+//
+// class CityTime(BaseModel):
+// time_info: str
+// city: str
+//
+// def lookup_time_function(city: str):
+// return Event(output=CityTime(time_info="10:10 AM", city=city))
+//
+// def city_report(node_input: CityTime):
+// return Event(output=f"It is {node_input.time_info} in {node_input.city}.")
+type CityTime struct {
+ TimeInfo string `json:"time_info"`
+ City string `json:"city"`
+}
+
+func newStructuredOutputPipeline(ctx context.Context, geminiModel model.LLM) (agent.Agent, error) {
+ lookupTimeFn := func(_ agent.Context, city string) (CityTime, error) {
+ // Simulate looking up the current time in the city.
+ return CityTime{TimeInfo: "10:10 AM", City: city}, nil
+ }
+
+ cityReportAgent, err := llmagent.New(llmagent.Config{
+ Name: "city_report_agent",
+ Model: geminiModel,
+ Description: "Reports the city and current time from the previous node's output.",
+ // When wrapped as an AgentNode, the predecessor's event.Output
+ // is delivered as the agent's user content. The {key} template
+ // syntax is not required — the struct fields are provided inline.
+ Instruction: "Report the city time information you received in a friendly sentence.",
+ })
+ if err != nil {
+ return nil, fmt.Errorf("cityReportAgent: %w", err)
+ }
+
+ lookupTimeNode := workflow.NewFunctionNode("lookup_time", lookupTimeFn, workflow.NodeConfig{})
+ cityReportNode, err := workflow.NewAgentNode(cityReportAgent, workflow.NodeConfig{})
+ if err != nil {
+ return nil, fmt.Errorf("NewAgentNode: %w", err)
+ }
+
+ return workflowagent.New(workflowagent.Config{
+ Name: "city_time_pipeline",
+ Edges: workflow.Chain(workflow.Start, lookupTimeNode, cityReportNode),
+ SubAgents: []agent.Agent{cityReportAgent},
+ })
+}
+
+// --8<-- [end:structured-output]
+
+// --8<-- [start:output-key]
+// newOutputKeyPipeline demonstrates the OutputKey mechanism for the prebuilt
+// sequentialagent. When OutputKey is set on an llmagent.Config, the framework
+// automatically writes the agent's final text response to session state under
+// that key. Downstream agents read it by referencing {key} in their Instruction.
+//
+// This pattern applies to sequentialagent / parallelagent / loopagent.
+// For the workflow package (FunctionNode / AgentNode), use Event.Output instead.
+func newOutputKeyPipeline(ctx context.Context, geminiModel model.LLM) (agent.Agent, error) {
+ step1, err := llmagent.New(llmagent.Config{
+ Name: "step_1",
+ Model: geminiModel,
+ Description: "Transforms the user's text.",
+ Instruction: "Convert the user's message to uppercase. Output only the transformed text.",
+ OutputKey: "upper_result",
+ })
+ if err != nil {
+ return nil, fmt.Errorf("step1: %w", err)
+ }
+
+ step2, err := llmagent.New(llmagent.Config{
+ Name: "step_2",
+ Model: geminiModel,
+ Description: "Reports the transformed text.",
+ Instruction: "The transformed text is: {upper_result}. Report it to the user.",
+ })
+ if err != nil {
+ return nil, fmt.Errorf("step2: %w", err)
+ }
+
+ return sequentialagent.New(sequentialagent.Config{
+ AgentConfig: agent.Config{
+ Name: "output_key_pipeline",
+ SubAgents: []agent.Agent{step1, step2},
+ },
+ })
+}
+
+// --8<-- [end:output-key]
+
+// --8<-- [start:state-scopes]
+// stateScopes shows how session-state key prefixes control the lifetime and
+// visibility of stored values. This pattern applies to the prebuilt workflow
+// agents (sequentialagent / parallelagent / loopagent) and to tools and
+// callbacks. For the workflow package (FunctionNode / AgentNode), prefer
+// returning values directly via Event.Output.
+//
+// Available prefixes:
+//
+// session.KeyPrefixApp ("app:") – shared across all users and sessions
+// session.KeyPrefixUser ("user:") – tied to the user, shared across sessions
+// session.KeyPrefixTemp ("temp:") – discarded after the current invocation
+//
+// Keys with no prefix persist for the lifetime of the session.
+func stateScopes(ctx agent.Context) error {
+ st := ctx.Session().State()
+
+ // Session-scoped (no prefix) — persists for the life of this session.
+ if err := st.Set("attempts", 0); err != nil {
+ return fmt.Errorf("state.Set attempts: %w", err)
+ }
+
+ // App-scoped — shared across all users and sessions for this app.
+ if err := st.Set(session.KeyPrefixApp+"global_counter", 42); err != nil {
+ return fmt.Errorf("state.Set app:global_counter: %w", err)
+ }
+
+ // User-scoped — shared across all sessions belonging to this user.
+ if err := st.Set(session.KeyPrefixUser+"login_count", 1); err != nil {
+ return fmt.Errorf("state.Set user:login_count: %w", err)
+ }
+
+ // Temp-scoped — discarded after this invocation ends.
+ if err := st.Set(session.KeyPrefixTemp+"scratch", "ephemeral"); err != nil {
+ return fmt.Errorf("state.Set temp:scratch: %w", err)
+ }
+
+ return nil
+}
+
+// --8<-- [end:state-scopes]
+
+// --8<-- [start:input-output-schema]
+// FlightSearchInput is the typed input schema for the flight-search agent node.
+// workflow.NewAgentNodeTyped[FlightSearchInput, FlightSearchOutput] reflects
+// these structs into *jsonschema.Schema automatically — no hand-built schema
+// construction needed.
+type FlightSearchInput struct {
+ Origin string `json:"origin" jsonschema:"Departure airport code e.g. SFO"`
+ Destination string `json:"destination" jsonschema:"Arrival airport code e.g. CDG"`
+ DepartureDate string `json:"departure_date" jsonschema:"Travel date in YYYY-MM-DD format"`
+}
+
+// FlightSearchOutput is the typed output schema for the flight-search agent node.
+type FlightSearchOutput struct {
+ CheapestPrice string `json:"cheapest_price" jsonschema:"Cheapest available fare e.g. $450"`
+ FlightCount string `json:"flight_count" jsonschema:"Number of matching flights found"`
+}
+
+// newSchemaAgentPipeline demonstrates workflow.NewAgentNodeTyped, which infers
+// *jsonschema.Schema from the generic type parameters. This is the Go equivalent
+// of Python's:
+//
+// flight_searcher = Agent(
+// input_schema=FlightSearchInput,
+// output_schema=FlightSearchOutput,
+// ...
+// )
+//
+// The node's event.Output carries the structured result to the successor —
+// no OutputKey or state write is needed.
+func newSchemaAgentPipeline(ctx context.Context, geminiModel model.LLM) (agent.Agent, error) {
+ flightSearchAgent, err := llmagent.New(llmagent.Config{
+ Name: "flight_searcher",
+ Model: geminiModel,
+ Description: "Searches for available flights and returns structured results.",
+ Instruction: `You are a flight-search assistant. Respond ONLY with a JSON object.`,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("flightSearchAgent: %w", err)
+ }
+
+ synthAgent, err := llmagent.New(llmagent.Config{
+ Name: "trip_assistant",
+ Model: geminiModel,
+ Description: "Summarises flight search results for the user.",
+ Instruction: `You help users plan trips. Summarise the flight result you received.`,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("synthAgent: %w", err)
+ }
+
+ // NewAgentNodeTyped[In, Out] reflects FlightSearchInput and FlightSearchOutput
+ // into *jsonschema.Schema automatically. The node enforces the input schema
+ // and constrains the model reply to the output schema's shape.
+ flightNode, err := workflow.NewAgentNodeTyped[FlightSearchInput, FlightSearchOutput](flightSearchAgent, workflow.NodeConfig{})
+ if err != nil {
+ return nil, fmt.Errorf("flightNode: %w", err)
+ }
+
+ synthNode, err := workflow.NewAgentNode(synthAgent, workflow.NodeConfig{})
+ if err != nil {
+ return nil, fmt.Errorf("synthNode: %w", err)
+ }
+
+ return workflowagent.New(workflowagent.Config{
+ Name: "flight_booking_pipeline",
+ Edges: workflow.Chain(workflow.Start, flightNode, synthNode),
+ SubAgents: []agent.Agent{flightSearchAgent, synthAgent},
+ })
+}
+
+// --8<-- [end:input-output-schema]
+
+func main() {
+ ctx := context.Background()
+
+ if _, err := newEventOutputPipeline(); err != nil {
+ log.Printf("newEventOutputPipeline: %v", err)
+ }
+
+ if _, err := newRoutingPipeline(); err != nil {
+ log.Printf("newRoutingPipeline: %v", err)
+ }
+
+ model, err := gemini.NewModel(ctx, "gemini-flash-latest", &genai.ClientConfig{})
+ if err != nil {
+ log.Printf("gemini.NewModel: %v", err)
+ return
+ }
+
+ if _, err := newStructuredOutputPipeline(ctx, model); err != nil {
+ log.Printf("newStructuredOutputPipeline: %v", err)
+ }
+
+ if _, err := newOutputKeyPipeline(ctx, model); err != nil {
+ log.Printf("newOutputKeyPipeline: %v", err)
+ }
+
+ if _, err := newSchemaAgentPipeline(ctx, model); err != nil {
+ log.Printf("newSchemaAgentPipeline: %v", err)
+ }
+}
diff --git a/examples/go/snippets/graphs/dynamic/main.go b/examples/go/snippets/graphs/dynamic/main.go
new file mode 100644
index 0000000000..a87a8e6dcb
--- /dev/null
+++ b/examples/go/snippets/graphs/dynamic/main.go
@@ -0,0 +1,501 @@
+// Copyright 2025 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.
+// Package main demonstrates dynamic workflow patterns in ADK Go v2.
+//
+// NOTE: This file requires the google.golang.org/adk/v2/workflow package,
+// which is available in ADK Go v2.0.0 and higher. The workflow package is
+// not present in v1.x releases. The snippets in this file are based on the
+// examples found in https://github.com/google/adk-go/tree/main/examples/workflow.
+//
+// Key types and functions used in this file:
+//
+// - workflow.NewFunctionNode[IN, OUT] – wraps a plain Go function as a workflow node.
+// Equivalent to Python's @node decorator on a regular function.
+//
+// - workflow.NewDynamicNode[IN, OUT] – wraps an orchestrator function that calls
+// workflow.RunNode to schedule child nodes at runtime. Equivalent to
+// Python's @node(rerun_on_resume=True) on an async orchestrator.
+//
+// - workflow.RunNode[OUT] – executes a child node from inside a dynamic
+// node body and returns its typed output. Equivalent to ctx.run_node().
+//
+// - workflow.NewAgentNode – wraps an agent.Agent as a workflow Node so it
+// can be invoked via workflow.RunNode inside a dynamic orchestrator.
+//
+// - workflow.NewParallelWorker – runs a wrapped node concurrently for each
+// item in a list input. Equivalent to asyncio.gather() in Python.
+//
+// - workflow.ResumeOrRequestInput – collapses the re-entry HITL pattern:
+// pauses for input on the first pass and returns the human's reply on
+// resume. Equivalent to yielding RequestInput then checking ctx for reply.
+//
+// - workflow.WithRunID – option for workflow.RunNode that supplies a
+// stable custom identifier, equivalent to ctx.run_node(..., run_id=...).
+//
+// - workflowagent.New – creates an agent.Agent backed by a Workflow
+// engine. Use workflow.Chain to build the edges slice.
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+
+ "google.golang.org/genai"
+
+ "google.golang.org/adk/v2/agent"
+ "google.golang.org/adk/v2/agent/llmagent"
+ "google.golang.org/adk/v2/agent/workflowagent"
+ "google.golang.org/adk/v2/cmd/launcher"
+ "google.golang.org/adk/v2/cmd/launcher/full"
+ "google.golang.org/adk/v2/model/gemini"
+ "google.golang.org/adk/v2/session"
+ "google.golang.org/adk/v2/workflow"
+)
+
+// --8<-- [start:get-started]
+// helloNode is a simple FunctionNode that returns "Hello World".
+// In Python this would be written as:
+//
+// @node(name="hello_node")
+// def my_node(node_input: Any):
+// return "Hello World"
+//
+// In Go, workflow.NewFunctionNode wraps the same logic with the
+// required node interface, inferring input and output types from
+// the generic parameters.
+var helloNode = workflow.NewFunctionNode("hello_node",
+ func(_ agent.Context, _ string) (string, error) {
+ return "Hello World", nil
+ },
+ workflow.NodeConfig{},
+)
+
+// myWorkflow is a dynamic orchestrator node. It calls workflow.RunNode
+// to schedule helloNode as a child and returns its output.
+// In Python this would be:
+//
+// @node(rerun_on_resume=True)
+// async def my_workflow(ctx: Context, node_input: str) -> str:
+// result = await ctx.run_node(my_node, node_input="hello")
+// return result
+//
+// workflow.NewDynamicNode defaults RerunOnResume to &true, matching the
+// Python @node(rerun_on_resume=True) behaviour.
+var myWorkflow = workflow.NewDynamicNode[string, string]("my_workflow",
+ func(ctx agent.Context, _ string, _ func(*session.Event) error) (string, error) {
+ return workflow.RunNode[string](ctx, helloNode, "hello")
+ },
+ workflow.NodeConfig{},
+)
+
+func runGetStarted() error {
+ ctx := context.Background()
+
+ // workflowagent.New creates an agent.Agent backed by the workflow engine.
+ // workflow.Chain(workflow.Start, myWorkflow) produces the edges slice
+ // equivalent to Python's edges=[("START", my_workflow)].
+ wa, err := workflowagent.New(workflowagent.Config{
+ Name: "root_agent",
+ Description: "A minimal dynamic workflow.",
+ Edges: workflow.Chain(workflow.Start, myWorkflow),
+ })
+ if err != nil {
+ return fmt.Errorf("workflowagent.New: %w", err)
+ }
+
+ l := full.NewLauncher()
+ return l.Execute(ctx, &launcher.Config{
+ AgentLoader: agent.NewSingleLoader(wa),
+ }, os.Args[1:])
+}
+
+// --8<-- [end:get-started]
+
+// --8<-- [start:building-blocks-nodes]
+// myFunctionNode demonstrates the explicit NewFunctionNode constructor —
+// equivalent to wrapping a function in a FunctionNode manually in Python:
+//
+// success_node = FunctionNode(my_function_node, name="hello", rerun_on_resume=True)
+//
+// Creating the node directly (rather than via @node) is useful when you
+// need multiple nodes from the same function with different configurations,
+// or when wrapping functions from an external library.
+var myFunctionNode = workflow.NewFunctionNode("hello",
+ func(_ agent.Context, _ any) (string, error) {
+ return "Hello World", nil
+ },
+ workflow.NodeConfig{},
+)
+
+// myFormattingNode is a second function node that the dynamic orchestrator
+// calls in sequence, mirroring:
+//
+// result_formatted = await ctx.run_node(my_formatting_node, node_input=result)
+var myFormattingNode = workflow.NewFunctionNode("format",
+ func(_ agent.Context, in string) (string, error) {
+ return fmt.Sprintf("[formatted] %s", in), nil
+ },
+ workflow.NodeConfig{},
+)
+
+// --8<-- [end:building-blocks-nodes]
+
+// --8<-- [start:building-blocks-workflow]
+// orchestratorWorkflow is a dynamic node that schedules two children in
+// sequence via workflow.RunNode, equivalent to:
+//
+// @node(rerun_on_resume=True)
+// async def my_workflow(ctx):
+// result = await ctx.run_node(my_function_node, node_input="Hello")
+// result_formatted = await ctx.run_node(my_formatting_node, node_input=result)
+// return result_formatted
+var orchestratorWorkflow = workflow.NewDynamicNode[string, string]("my_workflow",
+ func(ctx agent.Context, _ string, _ func(*session.Event) error) (string, error) {
+ result, err := workflow.RunNode[string](ctx, myFunctionNode, "Hello")
+ if err != nil {
+ return "", err
+ }
+ return workflow.RunNode[string](ctx, myFormattingNode, result)
+ },
+ workflow.NodeConfig{},
+)
+
+// --8<-- [end:building-blocks-workflow]
+
+// --8<-- [start:data-handling]
+// newDataHandlingWorkflow demonstrates how to pass data between a dynamic
+// orchestrator and an LlmAgent-backed node. workflow.NewAgentNode wraps an
+// agent.Agent so it can be invoked via workflow.RunNode.
+//
+// In Python this mirrors:
+//
+// city_report_agent = Agent(name="city_report_agent", ...)
+// @node
+// async def city_workflow(ctx: Context):
+// city_time = await ctx.run_node(city_time_function, "Paris")
+// report_text = await ctx.run_node(city_report_agent, city_time)
+// return report_text
+func newDataHandlingWorkflow(ctx context.Context) (agent.Agent, error) {
+ model, err := gemini.NewModel(ctx, "gemini-flash-latest", &genai.ClientConfig{})
+ if err != nil {
+ return nil, fmt.Errorf("gemini.NewModel: %w", err)
+ }
+
+ // cityTimeNode is a FunctionNode that returns a formatted city-time string.
+ cityTimeNode := workflow.NewFunctionNode("city_time_function",
+ func(_ agent.Context, city string) (string, error) {
+ return fmt.Sprintf("10:10 AM in %s", city), nil
+ },
+ workflow.NodeConfig{},
+ )
+
+ // cityReportAgent is an LlmAgent that receives the city-time string and
+ // produces a human-friendly report.
+ cityReportAgent, err := llmagent.New(llmagent.Config{
+ Name: "city_report_agent",
+ Model: model,
+ Description: "Reports city time information.",
+ Instruction: "Output the data provided by the previous node in a friendly sentence.",
+ })
+ if err != nil {
+ return nil, fmt.Errorf("llmagent.New (cityReport): %w", err)
+ }
+
+ // workflow.NewAgentNode wraps cityReportAgent so it can be called from
+ // inside a dynamic node via workflow.RunNode.
+ cityReportNode, err := workflow.NewAgentNode(cityReportAgent, workflow.NodeConfig{})
+ if err != nil {
+ return nil, fmt.Errorf("workflow.NewAgentNode: %w", err)
+ }
+
+ cityWorkflow := workflow.NewDynamicNode[string, string]("city_workflow",
+ func(ctx agent.Context, _ string, _ func(*session.Event) error) (string, error) {
+ cityTime, err := workflow.RunNode[string](ctx, cityTimeNode, "Paris")
+ if err != nil {
+ return "", err
+ }
+ return workflow.RunNode[string](ctx, cityReportNode, cityTime)
+ },
+ workflow.NodeConfig{},
+ )
+
+ return workflowagent.New(workflowagent.Config{
+ Name: "data_handling_workflow",
+ SubAgents: []agent.Agent{cityReportAgent},
+ Edges: workflow.Chain(workflow.Start, cityWorkflow),
+ })
+}
+
+// --8<-- [end:data-handling]
+
+// --8<-- [start:loop-route]
+// newLoopWorkflow demonstrates an iterative loop inside a dynamic node.
+// The orchestrator body uses a plain Go for loop to keep calling the
+// lintCheckNode until there are no findings — equivalent to Python's:
+//
+// @node
+// async def code_workflow(ctx: Context, user_request: str):
+// code = await ctx.run_node(coder_agent, user_request)
+// check_resp = await ctx.run_node(compile_lint_check, code)
+// while check_resp.findings:
+// code = await ctx.run_node(fixer_agent, ...)
+// check_resp = await ctx.run_node(compile_lint_check, code)
+// return code
+func newLoopWorkflow(ctx context.Context) (agent.Agent, error) {
+ model, err := gemini.NewModel(ctx, "gemini-flash-latest", &genai.ClientConfig{})
+ if err != nil {
+ return nil, fmt.Errorf("gemini.NewModel: %w", err)
+ }
+
+ coderAgent, err := llmagent.New(llmagent.Config{
+ Name: "generator_agent",
+ Model: model,
+ Description: "Writes Go code for the user request.",
+ Instruction: "Write Go code for the user request. Output only the code.",
+ OutputKey: "generated_code",
+ })
+ if err != nil {
+ return nil, fmt.Errorf("llmagent.New (coder): %w", err)
+ }
+
+ coderNode, err := workflow.NewAgentNode(coderAgent, workflow.NodeConfig{})
+ if err != nil {
+ return nil, fmt.Errorf("workflow.NewAgentNode (coder): %w", err)
+ }
+
+ // lintCheckNode simulates a lint/compile check. It returns an empty
+ // string when there are no findings, signalling the loop to exit.
+ lintCheckNode := workflow.NewFunctionNode("lint_reviewer",
+ func(_ agent.Context, code string) (string, error) {
+ // Simulate a lint check: return findings or empty string when clean.
+ if len(code) < 50 {
+ return "Code is too short; add error handling.", nil
+ }
+ return "", nil // no findings — loop exits
+ },
+ workflow.NodeConfig{},
+ )
+
+ fixerAgent, err := llmagent.New(llmagent.Config{
+ Name: "fixer_agent",
+ Model: model,
+ Description: "Refactors code based on lint findings.",
+ Instruction: "Refactor the provided code to address the review findings. Output only the improved code.",
+ })
+ if err != nil {
+ return nil, fmt.Errorf("llmagent.New (fixer): %w", err)
+ }
+
+ fixerNode, err := workflow.NewAgentNode(fixerAgent, workflow.NodeConfig{})
+ if err != nil {
+ return nil, fmt.Errorf("workflow.NewAgentNode (fixer): %w", err)
+ }
+
+ codeWorkflow := workflow.NewDynamicNode[string, string]("code_workflow",
+ func(ctx agent.Context, userRequest string, _ func(*session.Event) error) (string, error) {
+ code, err := workflow.RunNode[string](ctx, coderNode, userRequest)
+ if err != nil {
+ return "", err
+ }
+
+ findings, err := workflow.RunNode[string](ctx, lintCheckNode, code)
+ if err != nil {
+ return "", err
+ }
+
+ // Loop until the lint check reports no findings.
+ for findings != "" {
+ code, err = workflow.RunNode[string](ctx, fixerNode, code)
+ if err != nil {
+ return "", err
+ }
+ findings, err = workflow.RunNode[string](ctx, lintCheckNode, code)
+ if err != nil {
+ return "", err
+ }
+ }
+ return code, nil
+ },
+ workflow.NodeConfig{},
+ )
+
+ return workflowagent.New(workflowagent.Config{
+ Name: "code_pipeline",
+ SubAgents: []agent.Agent{coderAgent, fixerAgent},
+ Edges: workflow.Chain(workflow.Start, codeWorkflow),
+ })
+}
+
+// --8<-- [end:loop-route]
+
+// --8<-- [start:parallel-route]
+// newParallelWorkflow demonstrates parallel execution using
+// workflow.NewParallelWorker. The worker node runs a wrapped child node
+// concurrently for each element in a list input, collecting results.
+//
+// This is the Go equivalent of using asyncio.gather in Python:
+//
+// @node(rerun_on_resume=True)
+// async def parallel_supervisor(ctx, node_input, real_node):
+// tasks = [ctx.run_node(real_node, item) for item in node_input]
+// results = await asyncio.gather(*tasks)
+// return results
+func newParallelWorkflow() (agent.Agent, error) {
+ // workerNode processes a single item. NewParallelWorker will call it
+ // once per element of the list input, concurrently.
+ workerNode := workflow.NewFunctionNode("worker",
+ func(_ agent.Context, item string) (string, error) {
+ return fmt.Sprintf("processed: %s", item), nil
+ },
+ workflow.NodeConfig{},
+ )
+
+ // NewParallelWorker wraps workerNode so it runs concurrently for each
+ // element of a []string input. maxConcurrency=0 means unlimited.
+ parallelWorker, err := workflow.NewParallelWorker(
+ "parallel_supervisor",
+ workerNode,
+ 0, // maxConcurrency: 0 = unlimited
+ workflow.NodeConfig{},
+ )
+ if err != nil {
+ return nil, fmt.Errorf("workflow.NewParallelWorker: %w", err)
+ }
+
+ return workflowagent.New(workflowagent.Config{
+ Name: "parallel_workflow",
+ Description: "Runs a worker node in parallel for each item in the input list.",
+ Edges: workflow.Chain(workflow.Start, parallelWorker),
+ })
+}
+
+// --8<-- [end:parallel-route]
+
+// --8<-- [start:human-input]
+// newHITLWorkflow demonstrates the re-entry HITL pattern using
+// workflow.ResumeOrRequestInput. On the first pass the node emits a
+// RequestInput event and returns ErrNodeInterrupted (pausing the workflow).
+// After the human replies, the same node is re-run from the top
+// (RerunOnResume=&true) and ResumeOrRequestInput returns the human's reply.
+//
+// In Python this is equivalent to:
+//
+// @node(rerun_on_resume=True)
+// async def get_user_approval(ctx, node_input):
+// yield RequestInput(message="Please approve this request (Yes/No)")
+//
+// @node(rerun_on_resume=True)
+// async def handle_process(ctx, node_input):
+// user_response = await ctx.run_node(get_user_approval)
+// if user_response.lower() == "yes":
+// return "Approved"
+// return "Denied"
+func newHITLWorkflow() (agent.Agent, error) {
+ rerun := true
+
+ // approvalNode pauses on the first pass to ask the user for a Yes/No
+ // approval, then resolves their decision on resume.
+ // workflow.ResumeOrRequestInput handles both phases.
+ approvalNode := workflow.NewEmittingFunctionNode[any, any]("get_user_approval",
+ func(nc agent.Context, _ any, emit func(*session.Event) error) (any, error) {
+ // ResumeOrRequestInput: on first pass, emits the prompt and
+ // returns ErrNodeInterrupted. On re-run after the human replies,
+ // it returns the reply payload directly.
+ reply, err := workflow.ResumeOrRequestInput(nc, emit, session.RequestInput{
+ InterruptID: "user_approval",
+ Message: "Please approve this request (Yes/No)",
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ response, _ := reply.(string)
+ if response == "" {
+ response = "No"
+ }
+ if response == "yes" || response == "Yes" {
+ return "Approved", nil
+ }
+ return "Denied", nil
+ },
+ workflow.NodeConfig{RerunOnResume: &rerun},
+ )
+
+ return workflowagent.New(workflowagent.Config{
+ Name: "hitl_workflow",
+ Description: "Pauses for user approval before completing a task.",
+ Edges: workflow.Chain(workflow.Start, approvalNode),
+ })
+}
+
+// --8<-- [end:human-input]
+
+// --8<-- [start:custom-execution-ids]
+// newCustomIDWorkflow demonstrates supplying stable custom run IDs via
+// workflow.WithRunID — equivalent to Python's:
+//
+// task = ctx.run_node(process_order, order, run_id=f"order-{order.order_id}")
+//
+// Custom run IDs must contain at least one non-numeric character to avoid
+// collision with auto-generated sequential integer IDs.
+func newCustomIDWorkflow() (agent.Agent, error) {
+ processOrderNode := workflow.NewFunctionNode("process_order",
+ func(_ agent.Context, orderID string) (string, error) {
+ return fmt.Sprintf("processed order %s", orderID), nil
+ },
+ workflow.NodeConfig{},
+ )
+
+ orders := []string{"ord-001", "ord-002", "ord-003"}
+
+ processAllOrders := workflow.NewDynamicNode[any, []string]("process_all_orders",
+ func(ctx agent.Context, _ any, _ func(*session.Event) error) ([]string, error) {
+ results := make([]string, 0, len(orders))
+ for _, orderID := range orders {
+ // WithRunID supplies a stable, deterministic identifier for
+ // each child invocation. IDs must contain at least one
+ // non-numeric character to avoid collision with the
+ // auto-generated sequential counter IDs.
+ result, err := workflow.RunNode[string](
+ ctx,
+ processOrderNode,
+ orderID,
+ workflow.WithRunID(fmt.Sprintf("order-%s", orderID)),
+ )
+ if err != nil {
+ return nil, fmt.Errorf("process order %s: %w", orderID, err)
+ }
+ results = append(results, result)
+ }
+ return results, nil
+ },
+ workflow.NodeConfig{},
+ )
+
+ return workflowagent.New(workflowagent.Config{
+ Name: "custom_id_workflow",
+ Description: "Processes orders with stable per-order execution IDs.",
+ Edges: workflow.Chain(workflow.Start, processAllOrders),
+ })
+}
+
+// --8<-- [end:custom-execution-ids]
+
+func main() {
+ if err := runGetStarted(); err != nil {
+ log.Fatalf("runGetStarted: %v", err)
+ }
+}
diff --git a/examples/go/snippets/graphs/human-input/main.go b/examples/go/snippets/graphs/human-input/main.go
new file mode 100644
index 0000000000..488a5c0d39
--- /dev/null
+++ b/examples/go/snippets/graphs/human-input/main.go
@@ -0,0 +1,336 @@
+// Copyright 2025 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.
+
+// Package main demonstrates Human-in-the-Loop (HITL) patterns in ADK Go v2.
+//
+// NOTE: This file requires google.golang.org/adk/v2, available in ADK Go
+// v2.0.0 and higher.
+//
+// # Graph HITL (primary pattern for /graphs/ pages)
+//
+// In ADK Go v2, the primary way to add a human input node to a graph-based
+// workflow is workflow.NewEmittingFunctionNode with workflow.ResumeOrRequestInput.
+// This is the direct Go equivalent of the Python RequestInput node:
+//
+// - On the first pass the node emits a session.RequestInput event
+// (surfaced via Event.RequestedInput) and returns ErrNodeInterrupted,
+// pausing the workflow.
+// - The workflow resumes after the client sends a reply. The node is
+// re-invoked from the top (RerunOnResume defaults to &true on dynamic
+// nodes; set it explicitly on EmittingFunctionNode), and
+// workflow.ResumeOrRequestInput returns the human's reply payload.
+//
+// # Tool-confirmation (secondary pattern, LLM-agent feature)
+//
+// Tool-confirmation (RequireConfirmation / ctx.RequestConfirmation) is a
+// separate LLM-agent mechanism for yes/no approval prompts before a tool
+// executes. It is not graph-node based.
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+
+ "google.golang.org/genai"
+
+ "google.golang.org/adk/v2/agent"
+ "google.golang.org/adk/v2/agent/llmagent"
+ "google.golang.org/adk/v2/agent/workflowagent"
+ "google.golang.org/adk/v2/model/gemini"
+ "google.golang.org/adk/v2/session"
+ "google.golang.org/adk/v2/tool"
+ "google.golang.org/adk/v2/tool/functiontool"
+ "google.golang.org/adk/v2/workflow"
+)
+
+const (
+ appName = "hitl_demo"
+ userID = "demo_user"
+ modelName = "gemini-flash-latest"
+)
+
+// --8<-- [start:graph-hitl-get-started]
+// newGraphHITLWorkflow demonstrates a graph HITL node using
+// workflow.NewEmittingFunctionNode and workflow.ResumeOrRequestInput.
+//
+// This is the Go equivalent of the Python RequestInput node:
+//
+// def step1(): # Human input step
+// yield RequestInput(message="Enter a number:")
+//
+// def step2(node_input):
+// return node_input * 2
+//
+// root_agent = Workflow(
+// name="root_agent",
+// edges=[('START', step1, step2)],
+// )
+//
+// On the first pass, step1Node emits a RequestInput event and pauses the
+// workflow (ErrNodeInterrupted). After the human replies, the node is re-run
+// and ResumeOrRequestInput returns the reply, which flows as typed input to
+// step2Node via event.Output.
+func newGraphHITLWorkflow() (agent.Agent, error) {
+ rerun := true
+
+ // step1Node: pauses for human input on the first pass, returns the
+ // human's reply on resume. workflow.ResumeOrRequestInput handles both
+ // phases — no manual re-entry bookkeeping needed.
+ step1Node := workflow.NewEmittingFunctionNode[any, string]("step1",
+ func(ctx agent.Context, _ any, emit func(*session.Event) error) (string, error) {
+ reply, err := workflow.ResumeOrRequestInput(ctx, emit, session.RequestInput{
+ InterruptID: "enter_number",
+ Message: "Enter a number:",
+ })
+ if err != nil {
+ // ErrNodeInterrupted on first pass — workflow pauses here.
+ return "", err
+ }
+ // On resume, reply is the human's text response.
+ number, _ := reply.(string)
+ return number, nil
+ },
+ workflow.NodeConfig{RerunOnResume: &rerun},
+ )
+
+ // step2Node: receives the human's input as its typed string input via
+ // event.Output and doubles the number.
+ step2Node := workflow.NewFunctionNode("step2",
+ func(_ agent.Context, input string) (string, error) {
+ return fmt.Sprintf("You entered: %s (doubled: %s%s)", input, input, input), nil
+ },
+ workflow.NodeConfig{},
+ )
+
+ return workflowagent.New(workflowagent.Config{
+ Name: "root_agent",
+ Description: "Pauses for a number from the user, then doubles it.",
+ Edges: workflow.Chain(workflow.Start, step1Node, step2Node),
+ })
+}
+
+// --8<-- [end:graph-hitl-get-started]
+
+// --8<-- [start:graph-hitl-with-payload]
+// ItineraryItem represents a single activity in a travel plan.
+type ItineraryItem struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+}
+
+// newItineraryReviewWorkflow demonstrates a graph HITL node that sends a
+// structured payload alongside the input prompt so the client can render
+// additional context for the user. This mirrors Python's:
+//
+// async def get_user_feedback(node_input: ActivitiesList):
+// yield RequestInput(
+// message="Which items appeal to you?",
+// payload=node_input,
+// response_schema=UserFeedback,
+// )
+func newItineraryReviewWorkflow() (agent.Agent, error) {
+ rerun := true
+
+ // buildItineraryNode: generates an itinerary and passes it to the HITL
+ // node as its typed output via event.Output.
+ buildItineraryNode := workflow.NewFunctionNode("build_itinerary",
+ func(_ agent.Context, _ any) ([]ItineraryItem, error) {
+ return []ItineraryItem{
+ {Name: "Eiffel Tower", Description: "Iconic iron lattice tower."},
+ {Name: "Louvre Museum", Description: "World's largest art museum."},
+ {Name: "Seine River Cruise", Description: "Scenic boat tour of Paris."},
+ }, nil
+ },
+ workflow.NodeConfig{},
+ )
+
+ // reviewNode: sends the itinerary as payload alongside the prompt so the
+ // client can display it. On resume, the human's selection is returned.
+ reviewNode := workflow.NewEmittingFunctionNode[[]ItineraryItem, string]("get_user_feedback",
+ func(ctx agent.Context, itinerary []ItineraryItem, emit func(*session.Event) error) (string, error) {
+ reply, err := workflow.ResumeOrRequestInput(ctx, emit, session.RequestInput{
+ InterruptID: "itinerary_review",
+ Message: fmt.Sprintf("Here is your recommended itinerary (%d activities). Which items appeal to you?", len(itinerary)),
+ Payload: itinerary, // structured payload rendered by the client
+ })
+ if err != nil {
+ // ErrNodeInterrupted on first pass — workflow pauses here.
+ return "", err
+ }
+ feedback, _ := reply.(string)
+ return feedback, nil
+ },
+ workflow.NodeConfig{RerunOnResume: &rerun},
+ )
+
+ // finalNode: receives the user's feedback and produces a confirmation.
+ finalNode := workflow.NewFunctionNode("finalize",
+ func(_ agent.Context, feedback string) (string, error) {
+ return fmt.Sprintf("Itinerary finalised with your feedback: %q", feedback), nil
+ },
+ workflow.NodeConfig{},
+ )
+
+ return workflowagent.New(workflowagent.Config{
+ Name: "concierge_workflow",
+ Description: "Builds an itinerary, asks the user for feedback, then finalises.",
+ Edges: workflow.Chain(workflow.Start, buildItineraryNode, reviewNode, finalNode),
+ })
+}
+
+// --8<-- [end:graph-hitl-with-payload]
+
+// --8<-- [start:simple-hitl]
+// DoubleNumberArgs holds the input for the doubleNumber tool.
+type DoubleNumberArgs struct {
+ Number int `json:"number" jsonschema:"The number to double."`
+}
+
+// DoubleNumberResults holds the output of the doubleNumber tool.
+type DoubleNumberResults struct {
+ Result int `json:"result"`
+}
+
+// doubleNumber is a tool that doubles the given number.
+// Because RequireConfirmation is true, the framework automatically pauses
+// execution and emits an "adk_request_confirmation" event to the client before
+// running the tool. The client must reply with a FunctionResponse confirming
+// or denying the action.
+func doubleNumber(_ agent.Context, args DoubleNumberArgs) (DoubleNumberResults, error) {
+ return DoubleNumberResults{Result: args.Number * 2}, nil
+}
+
+// newSimpleHITLAgent creates an LLM agent with a tool that always requires
+// user confirmation before it executes (tool-confirmation pattern).
+func newSimpleHITLAgent(ctx context.Context) (agent.Agent, error) {
+ model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{})
+ if err != nil {
+ return nil, fmt.Errorf("failed to create model: %w", err)
+ }
+
+ doubleNumberTool, err := functiontool.New(
+ functiontool.Config{
+ Name: "double_number",
+ Description: "Doubles the given number. Requires user approval before running.",
+ RequireConfirmation: true,
+ },
+ doubleNumber,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to create tool: %w", err)
+ }
+
+ return llmagent.New(llmagent.Config{
+ Name: "double_number_agent",
+ Model: model,
+ Instruction: "You are a helpful assistant. When asked to double a number, use the double_number tool.",
+ Tools: []tool.Tool{doubleNumberTool},
+ })
+}
+
+// --8<-- [end:simple-hitl]
+
+// --8<-- [start:hitl-with-hint]
+// BookFlightArgs holds the input for the bookFlight tool.
+type BookFlightArgs struct {
+ Origin string `json:"origin" jsonschema:"Departure airport code."`
+ Destination string `json:"destination" jsonschema:"Arrival airport code."`
+ Date string `json:"date" jsonschema:"Travel date in YYYY-MM-DD format."`
+}
+
+// BookFlightResults holds the outcome of the bookFlight tool.
+type BookFlightResults struct {
+ Status string `json:"status"`
+ ConfirmNumber string `json:"confirm_number,omitempty"`
+}
+
+// bookFlight is a tool that pauses for human approval before completing a
+// booking (tool-confirmation pattern with a custom hint message).
+func bookFlight(ctx agent.Context, args BookFlightArgs) (BookFlightResults, error) {
+ if confirmation := ctx.ToolConfirmation(); confirmation != nil {
+ if !confirmation.Confirmed {
+ return BookFlightResults{Status: "Booking cancelled by user."}, nil
+ }
+ return BookFlightResults{
+ Status: "Booking confirmed.",
+ ConfirmNumber: "FLT-20251031",
+ }, nil
+ }
+
+ hint := fmt.Sprintf(
+ "The agent wants to book a flight from %s to %s on %s. Do you approve?",
+ args.Origin, args.Destination, args.Date,
+ )
+ if err := ctx.RequestConfirmation(hint, nil); err != nil {
+ return BookFlightResults{}, fmt.Errorf("failed to request confirmation: %w", err)
+ }
+ return BookFlightResults{Status: "Awaiting user approval."}, nil
+}
+
+// newHITLWithHintAgent creates an LLM agent whose bookFlight tool manually
+// requests confirmation with a descriptive hint (tool-confirmation pattern).
+func newHITLWithHintAgent(ctx context.Context) (agent.Agent, error) {
+ model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{})
+ if err != nil {
+ return nil, fmt.Errorf("failed to create model: %w", err)
+ }
+
+ bookFlightTool, err := functiontool.New(
+ functiontool.Config{
+ Name: "book_flight",
+ Description: "Books a flight between two airports on a given date.",
+ },
+ bookFlight,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to create tool: %w", err)
+ }
+
+ return llmagent.New(llmagent.Config{
+ Name: "flight_booking_agent",
+ Model: model,
+ Instruction: "You are a flight booking assistant. Help the user book flights.",
+ Tools: []tool.Tool{bookFlightTool},
+ })
+}
+
+// --8<-- [end:hitl-with-hint]
+
+func main() {
+ graphAgent, err := newGraphHITLWorkflow()
+ if err != nil {
+ log.Fatalf("Failed to create graph HITL workflow: %v", err)
+ }
+ log.Printf("Created graph HITL workflow: %s", graphAgent.Name())
+
+ itineraryAgent, err := newItineraryReviewWorkflow()
+ if err != nil {
+ log.Fatalf("Failed to create itinerary review workflow: %v", err)
+ }
+ log.Printf("Created itinerary review workflow: %s", itineraryAgent.Name())
+
+ ctx := context.Background()
+ simpleAgent, err := newSimpleHITLAgent(ctx)
+ if err != nil {
+ log.Fatalf("Failed to create simple HITL agent: %v", err)
+ }
+ log.Printf("Created simple HITL agent: %s", simpleAgent.Name())
+
+ hintAgent, err := newHITLWithHintAgent(ctx)
+ if err != nil {
+ log.Fatalf("Failed to create hint HITL agent: %v", err)
+ }
+ log.Printf("Created hint HITL agent: %s", hintAgent.Name())
+}
diff --git a/examples/go/snippets/graphs/index/main.go b/examples/go/snippets/graphs/index/main.go
new file mode 100644
index 0000000000..72c1632b64
--- /dev/null
+++ b/examples/go/snippets/graphs/index/main.go
@@ -0,0 +1,200 @@
+// Copyright 2025 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.
+
+// Package main provides snippet examples for graph-based workflow agents in ADK Go v2.
+//
+// NOTE: This file requires google.golang.org/adk/v2 (the workflow package),
+// available in ADK Go v2.0.0 and higher.
+//
+// Both snippets use the v2 graph engine (workflow.NewFunctionNode +
+// workflowagent.New) rather than the prebuilt workflow agents from v1.x.
+// This mirrors the Python Workflow(edges=[...]) API directly:
+//
+// - workflow.Chain(workflow.Start, nodeA, nodeB) — sequential edges
+// - workflow.NewEmittingFunctionNode + ev.Routes + []workflow.Edge — routing
+// - workflow.StringRoute("category") — conditional edge matcher
+package main
+
+import (
+ "fmt"
+ "log"
+ "strings"
+
+ "google.golang.org/adk/v2/agent"
+ "google.golang.org/adk/v2/agent/workflowagent"
+ "google.golang.org/adk/v2/session"
+ "google.golang.org/adk/v2/workflow"
+)
+
+// --8<-- [start:sequential-get-started]
+// cityTime holds the data passed from the lookup step to the report step.
+type cityTime struct {
+ City string
+ TimeInfo string
+}
+
+// newSequentialGetStarted builds a three-node sequential workflow using the
+// v2 graph engine. Each node is a workflow.NewFunctionNode whose return value
+// is automatically wrapped in session.Event.Output and forwarded to the next
+// node as its typed input.
+//
+// This is the Go equivalent of the Python Workflow example:
+//
+// root_agent = Workflow(
+// name="root_agent",
+// edges=[("START", city_generator_agent, lookup_time_function,
+// city_report_agent, completed_message_function)],
+// )
+func newSequentialGetStarted() (agent.Agent, error) {
+ // Step 1: return a city name. The string is set as event.Output and
+ // becomes the typed input of the next node.
+ cityGeneratorNode := workflow.NewFunctionNode("city_generator_agent",
+ func(_ agent.Context, _ any) (string, error) {
+ return "Tokyo", nil
+ },
+ workflow.NodeConfig{},
+ )
+
+ // Step 2: receive the city name and return structured time data.
+ lookupTimeNode := workflow.NewFunctionNode("lookup_time_function",
+ func(_ agent.Context, city string) (cityTime, error) {
+ return cityTime{City: city, TimeInfo: "10:10 AM"}, nil
+ },
+ workflow.NodeConfig{},
+ )
+
+ // Step 3: receive the cityTime struct and produce the final report string.
+ cityReportNode := workflow.NewFunctionNode("city_report_agent",
+ func(_ agent.Context, ct cityTime) (string, error) {
+ return fmt.Sprintf("It is %s in %s right now.\nWORKFLOW COMPLETED.",
+ ct.TimeInfo, ct.City), nil
+ },
+ workflow.NodeConfig{},
+ )
+
+ // workflow.Chain wires START → cityGeneratorNode → lookupTimeNode → cityReportNode.
+ // Data flows through event.Output: no session state writes needed.
+ return workflowagent.New(workflowagent.Config{
+ Name: "root_agent",
+ Description: "Sequential workflow: generate city → look up time → report.",
+ Edges: workflow.Chain(workflow.Start, cityGeneratorNode, lookupTimeNode, cityReportNode),
+ })
+}
+
+// --8<-- [end:sequential-get-started]
+
+// --8<-- [start:process-pipeline]
+// classifyMessage is the router node. It emits ev.Routes to select which
+// branch to follow — the Go equivalent of Python's:
+//
+// def router(node_input: str):
+// return Event(route=["BUG"])
+func classifyMessage(ctx agent.Context, msg string, emit func(*session.Event) error) (any, error) {
+ // In a real workflow this step calls an LLM; here we classify by keyword.
+ category := "LOGISTICS"
+ lower := strings.ToLower(msg)
+ switch {
+ case strings.Contains(lower, "bug") || strings.Contains(lower, "error"):
+ category = "BUG"
+ case strings.Contains(lower, "help") || strings.Contains(lower, "support"):
+ category = "CUSTOMER_SUPPORT"
+ }
+
+ ev := session.NewEvent(ctx, ctx.InvocationID())
+ ev.Routes = []string{category} // drives edge dispatch
+ ev.Output = msg // forward original message to the chosen handler
+ if err := emit(ev); err != nil {
+ return nil, err
+ }
+ return nil, nil // nil suppresses the automatic terminal event
+}
+
+// newProcessPipeline builds a classification + conditional-routing workflow
+// using the v2 graph engine. The classifyMessage emitting node sets
+// ev.Routes, and the graph engine dispatches to the matching handler via
+// workflow.StringRoute.
+//
+// This is the Go equivalent of the Python Workflow example:
+//
+// root_agent = Workflow(
+// name="routing_workflow",
+// edges=[
+// ("START", process_message, router),
+// (router, {
+// "BUG": response_1_bug,
+// "CUSTOMER_SUPPORT": response_2_support,
+// "LOGISTICS": response_3_logistics,
+// }),
+// ],
+// )
+func newProcessPipeline() (agent.Agent, error) {
+ classifyNode := workflow.NewEmittingFunctionNode(
+ "process_message", classifyMessage, workflow.NodeConfig{},
+ )
+
+ bugNode := workflow.NewFunctionNode("response_1_bug",
+ func(_ agent.Context, _ any) (string, error) {
+ return "Handling bug...", nil
+ },
+ workflow.NodeConfig{},
+ )
+
+ supportNode := workflow.NewFunctionNode("response_2_support",
+ func(_ agent.Context, _ any) (string, error) {
+ return "Handling customer support...", nil
+ },
+ workflow.NodeConfig{},
+ )
+
+ logisticsNode := workflow.NewFunctionNode("response_3_logistics",
+ func(_ agent.Context, _ any) (string, error) {
+ return "Handling logistics...", nil
+ },
+ workflow.NodeConfig{},
+ )
+
+ // workflow.Concat merges the sequential chain with the conditional edges.
+ // Each workflow.Edge carries a workflow.StringRoute matcher that the engine
+ // checks against ev.Routes emitted by classifyNode.
+ edges := workflow.Concat(
+ workflow.Chain(workflow.Start, classifyNode),
+ []workflow.Edge{
+ {From: classifyNode, To: bugNode, Route: workflow.StringRoute("BUG")},
+ {From: classifyNode, To: supportNode, Route: workflow.StringRoute("CUSTOMER_SUPPORT")},
+ {From: classifyNode, To: logisticsNode, Route: workflow.StringRoute("LOGISTICS")},
+ },
+ )
+
+ return workflowagent.New(workflowagent.Config{
+ Name: "routing_workflow",
+ Description: "Classifies a message and routes it to the appropriate handler.",
+ Edges: edges,
+ })
+}
+
+// --8<-- [end:process-pipeline]
+
+func main() {
+ seqAgent, err := newSequentialGetStarted()
+ if err != nil {
+ log.Fatalf("Failed to create sequential agent: %v", err)
+ }
+ log.Printf("Created sequential workflow agent: %s", seqAgent.Name())
+
+ pipelineAgent, err := newProcessPipeline()
+ if err != nil {
+ log.Fatalf("Failed to create process pipeline: %v", err)
+ }
+ log.Printf("Created process pipeline agent: %s", pipelineAgent.Name())
+}
diff --git a/examples/go/snippets/graphs/routes/main.go b/examples/go/snippets/graphs/routes/main.go
new file mode 100644
index 0000000000..7da692b138
--- /dev/null
+++ b/examples/go/snippets/graphs/routes/main.go
@@ -0,0 +1,399 @@
+// Copyright 2025 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.
+
+// Package main demonstrates graph routing patterns in ADK Go v2 using the
+// graph engine: workflow.NewFunctionNode, workflow.NewAgentNode, workflow.Chain,
+// workflow.Concat, workflow.NewEdgeBuilder, workflow.NewJoinNode, and
+// workflowagent.New.
+//
+// NOTE: This file requires google.golang.org/adk/v2 (the workflow package),
+// available in ADK Go v2.0.0 and higher.
+//
+// This file contains five snippet regions used in docs/graphs/routes.md:
+//
+// function-node – workflow.NewFunctionNode as a graph node
+// sequential-nodes – sequential route using workflow.Chain
+// parallel-fan-out – fan-out/join using workflow.NewJoinNode + EdgeBuilder
+// nested-workflows – inner workflowagent wrapped as workflow.NewAgentNode
+// loop-escalate – back-edge loop using workflow.EdgeBuilder.AddRoute
+package main
+
+import (
+ "fmt"
+ "log"
+ "strings"
+
+ "google.golang.org/adk/v2/agent"
+ "google.golang.org/adk/v2/agent/workflowagent"
+ "google.golang.org/adk/v2/session"
+ "google.golang.org/adk/v2/workflow"
+)
+
+// --8<-- [start:function-node]
+// newFunctionNodePipeline demonstrates workflow.NewFunctionNode as the primary
+// v2 node type. A FunctionNode wraps a plain Go function: the function returns
+// a typed value, and the framework automatically wraps it in a session.Event,
+// setting event.Output. The successor node receives this value as its typed
+// input parameter.
+//
+// This is the direct Go equivalent of the Python FunctionNode:
+//
+// def my_function_node(node_input: str):
+// return Event(output=node_input.upper())
+func newFunctionNodePipeline() (agent.Agent, error) {
+ upperFn := func(_ agent.Context, input string) (string, error) {
+ return strings.ToUpper(input), nil
+ }
+
+ suffixFn := func(_ agent.Context, input string) (string, error) {
+ return input + " IS AWESOME!", nil
+ }
+
+ // workflow.NewFunctionNode wraps each function as a graph node.
+ // workflow.Chain wires them in order: START → upper → suffix.
+ // The output of upperFn is delivered as the typed input of suffixFn
+ // via event.Output — no session state writes are needed.
+ nodeA := workflow.NewFunctionNode("upper", upperFn, workflow.NodeConfig{})
+ nodeB := workflow.NewFunctionNode("suffix", suffixFn, workflow.NodeConfig{})
+
+ return workflowagent.New(workflowagent.Config{
+ Name: "function_node_pipeline",
+ Description: "Demonstrates workflow.NewFunctionNode data flow via Event.Output.",
+ Edges: workflow.Chain(workflow.Start, nodeA, nodeB),
+ })
+}
+
+// --8<-- [end:function-node]
+
+// --8<-- [start:sequential-nodes]
+// newSequentialNodes builds a two-step sequential workflow using the v2 graph
+// engine. workflow.Chain wires the nodes in order; each node's typed return
+// value is forwarded to the next node via event.Output.
+//
+// This is the Go equivalent of:
+//
+// edges=[("START", task_A_node, task_B_node)]
+func newSequentialNodes() (agent.Agent, error) {
+ // task_A_node: transforms the user's input.
+ taskANode := workflow.NewFunctionNode("task_A_node",
+ func(_ agent.Context, input string) (string, error) {
+ return "Summary: " + strings.TrimSpace(input), nil
+ },
+ workflow.NodeConfig{},
+ )
+
+ // task_B_node: receives task A's output as its typed input and produces
+ // the final result. No session state reads needed.
+ taskBNode := workflow.NewFunctionNode("task_B_node",
+ func(_ agent.Context, summary string) (string, error) {
+ return strings.ToUpper(summary), nil
+ },
+ workflow.NodeConfig{},
+ )
+
+ return workflowagent.New(workflowagent.Config{
+ Name: "sequential_workflow",
+ Description: "Runs task A then task B in order via workflow.Chain.",
+ Edges: workflow.Chain(workflow.Start, taskANode, taskBNode),
+ })
+}
+
+// --8<-- [end:sequential-nodes]
+
+// --8<-- [start:parallel-fan-out]
+// newParallelFanOut builds a fan-out / join workflow using the v2 graph engine.
+// Three research nodes run in parallel from Start; workflow.NewJoinNode waits
+// for all of them to complete and emits a map[nodeName]output to the format
+// node, which assembles the results for a synthesis node.
+//
+// Graph topology:
+//
+// START ─┬─> research_A ──┐
+// ├─> research_B ──┼─> gather (JoinNode) ─> format ─> synthesis
+// └─> research_C ──┘
+//
+// Python equivalent:
+//
+// edges=[
+// ("START", research_A, my_join_node),
+// ("START", research_B, my_join_node),
+// ("START", research_C, my_join_node),
+// (my_join_node, format_node),
+// (format_node, synthesis_node),
+// ]
+func newParallelFanOut() (agent.Agent, error) {
+ researchA := workflow.NewFunctionNode("research_A",
+ func(_ agent.Context, _ any) (string, error) {
+ return "Fact about renewable energy.", nil
+ },
+ workflow.NodeConfig{},
+ )
+ researchB := workflow.NewFunctionNode("research_B",
+ func(_ agent.Context, _ any) (string, error) {
+ return "Fact about electric vehicles.", nil
+ },
+ workflow.NodeConfig{},
+ )
+ researchC := workflow.NewFunctionNode("research_C",
+ func(_ agent.Context, _ any) (string, error) {
+ return "Fact about carbon capture.", nil
+ },
+ workflow.NodeConfig{},
+ )
+
+ // workflow.NewJoinNode waits for all predecessors (research_A, research_B,
+ // research_C) to complete and emits a map[nodeName]output to its successor.
+ gatherNode := workflow.NewJoinNode("gather")
+
+ // formatNode receives map[string]any from gatherNode and assembles a
+ // combined prompt string.
+ formatNode := workflow.NewFunctionNode("format",
+ func(_ agent.Context, results map[string]any) (string, error) {
+ return fmt.Sprintf("A: %v\nB: %v\nC: %v",
+ results["research_A"],
+ results["research_B"],
+ results["research_C"],
+ ), nil
+ },
+ workflow.NodeConfig{},
+ )
+
+ synthesisNode := workflow.NewFunctionNode("synthesis",
+ func(_ agent.Context, prompt string) (string, error) {
+ return "Combined report: " + prompt, nil
+ },
+ workflow.NodeConfig{},
+ )
+
+ // EdgeBuilder.AddFanOut fans workflow.Start out to all three research nodes.
+ // EdgeBuilder.AddFanIn routes all three research nodes into gatherNode.
+ eb := workflow.NewEdgeBuilder()
+ eb.AddFanOut(workflow.Start, researchA, researchB, researchC)
+ eb.AddFanIn(gatherNode, researchA, researchB, researchC)
+ eb.Add(gatherNode, formatNode)
+ eb.Add(formatNode, synthesisNode)
+
+ return workflowagent.New(workflowagent.Config{
+ Name: "fan_out_workflow",
+ Description: "Parallel research fan-out with JoinNode barrier and synthesis.",
+ Edges: eb.Build(),
+ })
+}
+
+// --8<-- [end:parallel-fan-out]
+
+// --8<-- [start:nested-workflows]
+// newNestedWorkflows shows how to nest one workflowagent inside another using
+// the v2 graph engine. The inner workflowagent is wrapped with
+// workflow.NewAgentNode and placed as a node in the outer graph's edge slice.
+// From the outer graph's perspective the inner workflow is a single node that
+// runs to completion before the edge to finalNode is followed.
+//
+// Python equivalent:
+//
+// root_agent = Workflow(
+// name="parent_workflow",
+// edges=[("START", task_A1, workflow_B, final_node)],
+// )
+func newNestedWorkflows() (agent.Agent, error) {
+ // --- Inner workflow B ---
+ innerStep1 := workflow.NewFunctionNode("inner_step_1",
+ func(_ agent.Context, input string) (string, error) {
+ return "[ES] " + input, nil // simulate translation to Spanish
+ },
+ workflow.NodeConfig{},
+ )
+ innerStep2 := workflow.NewFunctionNode("inner_step_2",
+ func(_ agent.Context, spanish string) (string, error) {
+ return "[EN] " + spanish, nil // simulate translation back to English
+ },
+ workflow.NodeConfig{},
+ )
+
+ // workflowB is a self-contained inner graph.
+ workflowB, err := workflowagent.New(workflowagent.Config{
+ Name: "workflow_B",
+ Description: "Translates input to Spanish then back to English.",
+ Edges: workflow.Chain(workflow.Start, innerStep1, innerStep2),
+ })
+ if err != nil {
+ return nil, fmt.Errorf("workflowB: %w", err)
+ }
+
+ // --- Outer graph ---
+ taskA1 := workflow.NewFunctionNode("task_A1",
+ func(_ agent.Context, input string) (string, error) {
+ return "Summary: " + strings.TrimSpace(input), nil
+ },
+ workflow.NodeConfig{},
+ )
+
+ finalNode := workflow.NewFunctionNode("final_node",
+ func(_ agent.Context, result string) (string, error) {
+ return "Final: " + result, nil
+ },
+ workflow.NodeConfig{},
+ )
+
+ // workflow.NewAgentNode wraps workflowB so it can be placed as a node
+ // in the outer graph's edges slice.
+ innerNode, err := workflow.NewAgentNode(workflowB, workflow.NodeConfig{})
+ if err != nil {
+ return nil, fmt.Errorf("NewAgentNode(workflowB): %w", err)
+ }
+
+ return workflowagent.New(workflowagent.Config{
+ Name: "parent_workflow",
+ Description: "Runs task_A1 then the nested workflow_B then final_node.",
+ Edges: workflow.Chain(workflow.Start, taskA1, innerNode, finalNode),
+ SubAgents: []agent.Agent{workflowB},
+ })
+}
+
+// --8<-- [end:nested-workflows]
+
+// --8<-- [start:loop-escalate]
+// draft carries the working document through the refinement loop.
+type draft struct {
+ Text string `json:"text"`
+}
+
+// criticResult is emitted by the critic node with the review verdict and
+// optional suggestions. The router reads Verdict to set Event.Routes.
+type criticResult struct {
+ Verdict string `json:"verdict"` // "REFINE" or "DONE"
+ Suggestions string `json:"suggestions"` // non-empty when Verdict == "REFINE"
+}
+
+// writeDraft is the initial writer node: produces the first draft from the
+// user's topic. Its typed return value becomes the input to the critic node
+// via Event.Output — no session state writes needed.
+func writeDraft(_ agent.Context, topic string) (draft, error) {
+ // In a real workflow this would call an LLM; here we return a stub.
+ return draft{Text: "Draft about " + topic + ": placeholder content."}, nil
+}
+
+// reviewDraft is the critic node: inspects the draft and returns a verdict.
+// "DONE" exits the loop; "REFINE" triggers a back-edge to the refiner.
+func reviewDraft(_ agent.Context, d draft) (criticResult, error) {
+ // Simulate a critic: approve once the draft contains "improved".
+ if strings.Contains(d.Text, "improved") {
+ return criticResult{Verdict: "DONE"}, nil
+ }
+ return criticResult{
+ Verdict: "REFINE",
+ Suggestions: "Add more detail and mark the text as improved.",
+ }, nil
+}
+
+// routeVerdict reads the critic's verdict and sets Event.Routes so the
+// graph engine dispatches to either the refiner or the done node.
+// Returning nil suppresses the automatic terminal event.
+func routeVerdict(ctx agent.Context, r criticResult, emit func(*session.Event) error) (any, error) {
+ ev := session.NewEvent(ctx, ctx.InvocationID())
+ ev.Routes = []string{r.Verdict}
+ ev.Output = r // forward the full result to the chosen successor
+ if err := emit(ev); err != nil {
+ return nil, err
+ }
+ return nil, nil
+}
+
+// refineDraft applies the critic's suggestions and returns the improved draft.
+// Its output feeds back to the critic node via the back-edge.
+func refineDraft(_ agent.Context, r criticResult) (draft, error) {
+ return draft{Text: "improved draft incorporating: " + r.Suggestions}, nil
+}
+
+// reportDone is the terminal node, reached only when the critic is satisfied.
+func reportDone(_ agent.Context, r criticResult) (string, error) {
+ return "Refinement complete. Final verdict: " + r.Verdict, nil
+}
+
+// newLoopEscalate builds an iterative document-refinement workflow using the
+// graph engine. The critic node emits a route ("REFINE" or "DONE") and the
+// engine dispatches to either the refiner (which loops back to the critic via
+// a back-edge) or the terminal done node.
+//
+// Graph topology:
+//
+// START → writer → critic → router ─┬─ "REFINE" → refiner ──┐
+// └─ "DONE" → done │
+// ▲_______________________________┘ (back-edge)
+//
+// Python equivalent:
+//
+// edges=[
+// ("START", writer_node, critic_node, router),
+// (router, {"REFINE": refiner_node, "DONE": done_node}),
+// (refiner_node, critic_node), # back-edge creates the loop
+// ]
+func newLoopEscalate() (agent.Agent, error) {
+ writerNode := workflow.NewFunctionNode("writer", writeDraft, workflow.NodeConfig{})
+ criticNode := workflow.NewFunctionNode("critic", reviewDraft, workflow.NodeConfig{})
+ routerNode := workflow.NewEmittingFunctionNode("router", routeVerdict, workflow.NodeConfig{})
+ refinerNode := workflow.NewFunctionNode("refiner", refineDraft, workflow.NodeConfig{})
+ doneNode := workflow.NewFunctionNode("done", reportDone, workflow.NodeConfig{})
+
+ // Build the edges. The back-edge from refinerNode to criticNode creates
+ // the loop; the graph engine re-activates criticNode with a fresh
+ // lifecycle on each iteration.
+ eb := workflow.NewEdgeBuilder()
+ eb.Add(workflow.Start, writerNode)
+ eb.Add(writerNode, criticNode)
+ eb.Add(criticNode, routerNode)
+ eb.AddRoute(routerNode, refinerNode, workflow.StringRoute("REFINE"))
+ eb.AddRoute(routerNode, doneNode, workflow.StringRoute("DONE"))
+ eb.Add(refinerNode, criticNode) // back-edge: loop back for another review
+
+ return workflowagent.New(workflowagent.Config{
+ Name: "iterative_writer",
+ Description: "Writes then iteratively refines a document using a critic/refiner loop.",
+ Edges: eb.Build(),
+ })
+}
+
+// --8<-- [end:loop-escalate]
+
+func main() {
+ fnPipeline, err := newFunctionNodePipeline()
+ if err != nil {
+ log.Fatalf("newFunctionNodePipeline: %v", err)
+ }
+ log.Printf("created %s", fnPipeline.Name())
+
+ seqAgent, err := newSequentialNodes()
+ if err != nil {
+ log.Fatalf("newSequentialNodes: %v", err)
+ }
+ log.Printf("created %s", seqAgent.Name())
+
+ parallelAgent, err := newParallelFanOut()
+ if err != nil {
+ log.Fatalf("newParallelFanOut: %v", err)
+ }
+ log.Printf("created %s", parallelAgent.Name())
+
+ nestedAgent, err := newNestedWorkflows()
+ if err != nil {
+ log.Fatalf("newNestedWorkflows: %v", err)
+ }
+ log.Printf("created %s", nestedAgent.Name())
+
+ loopAgent, err := newLoopEscalate()
+ if err != nil {
+ log.Fatalf("newLoopEscalate: %v", err)
+ }
+ log.Printf("created %s", loopAgent.Name())
+}
diff --git a/examples/go/snippets/workflows/collaboration/main.go b/examples/go/snippets/workflows/collaboration/main.go
new file mode 100644
index 0000000000..6ea9d75c5f
--- /dev/null
+++ b/examples/go/snippets/workflows/collaboration/main.go
@@ -0,0 +1,154 @@
+// Copyright 2025 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.
+
+// Package main demonstrates collaborative agent team patterns in ADK Go v2.
+//
+// NOTE: This file requires google.golang.org/adk/v2, available in ADK Go
+// v2.0.0 and higher.
+//
+// # Agent collaboration modes in ADK Go v2
+//
+// The Mode field on llmagent.Config controls how a subagent behaves when
+// invoked by a coordinator agent. Three modes are available:
+//
+// - "chat" (ModeChat, default): full user interaction; agent controls
+// flow until it explicitly calls transfer_to_agent.
+// - "task" (ModeTask): agent may ask the user clarifying questions and
+// automatically returns control to the parent when it calls complete_task.
+// - "single_turn" (ModeSingleTurn): no user interaction; executes one turn
+// and returns automatically; can run in parallel with peer agents.
+//
+// When a coordinator llmagent declares SubAgents, ADK automatically generates
+// request_task_ tools for each subagent, wiring the delegation pattern.
+//
+// When an llmagent is used as a node in the v2 workflow graph engine
+// (workflow.NewAgentNode), the engine automatically applies ModeSingleTurn
+// if no mode is configured on the agent.
+package main
+
+import (
+ "context"
+ "log"
+
+ "google.golang.org/genai"
+
+ "google.golang.org/adk/v2/agent"
+ "google.golang.org/adk/v2/agent/llmagent"
+ "google.golang.org/adk/v2/model/gemini"
+ "google.golang.org/adk/v2/tool"
+ "google.golang.org/adk/v2/tool/functiontool"
+)
+
+// --8<-- [start:get-started]
+// Stub tool functions — in a real agent these call external services.
+func getWeather(_ agent.Context, _ struct{ City string }) (string, error) {
+ return "Sunny, 22°C", nil
+}
+
+func searchFlights(_ agent.Context, _ struct{ Origin, Destination string }) (string, error) {
+ return "3 flights found", nil
+}
+
+func bookFlight(_ agent.Context, _ struct{ FlightID string }) (string, error) {
+ return "Flight booked", nil
+}
+
+// newCollaborativeTeam builds a coordinator agent with two subagents, each
+// configured with a different collaboration mode. This is the Go equivalent of:
+//
+// weather_agent = Agent(name="weather_checker", mode="single_turn", ...)
+// flight_agent = Agent(name="flight_booker", mode="task", ...)
+// root = Agent(name="travel_planner", sub_agents=[weather_agent, flight_agent])
+func newCollaborativeTeam(ctx context.Context) (agent.Agent, error) {
+ model, err := gemini.NewModel(ctx, "gemini-flash-latest", &genai.ClientConfig{})
+ if err != nil {
+ return nil, err
+ }
+
+ getWeatherTool, err := functiontool.New(functiontool.Config{
+ Name: "get_weather",
+ Description: "Returns the current weather for a city.",
+ }, getWeather)
+ if err != nil {
+ return nil, err
+ }
+
+ searchFlightsTool, err := functiontool.New(functiontool.Config{
+ Name: "search_flights",
+ Description: "Searches for available flights between two airports.",
+ }, searchFlights)
+ if err != nil {
+ return nil, err
+ }
+
+ bookFlightTool, err := functiontool.New(functiontool.Config{
+ Name: "book_flight",
+ Description: "Books a specific flight by ID.",
+ }, bookFlight)
+ if err != nil {
+ return nil, err
+ }
+
+ // weatherAgent runs in ModeSingleTurn: no user interaction, executes one
+ // turn and returns automatically. Equivalent to mode="single_turn" in Python.
+ weatherAgent, err := llmagent.New(llmagent.Config{
+ Name: "weather_checker",
+ Model: model,
+ Mode: llmagent.ModeSingleTurn,
+ Description: "Checks the current weather for a given city.",
+ Instruction: "Use the get_weather tool to look up the current weather.",
+ Tools: []tool.Tool{getWeatherTool},
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // flightAgent runs in ModeTask: may ask the user clarifying questions and
+ // automatically returns control to the coordinator when done. Equivalent to
+ // mode="task" in Python.
+ flightAgent, err := llmagent.New(llmagent.Config{
+ Name: "flight_booker",
+ Model: model,
+ Mode: llmagent.ModeTask,
+ Description: "Searches for and books flights.",
+ Instruction: "Help the user find and book a flight using the available tools.",
+ Tools: []tool.Tool{searchFlightsTool, bookFlightTool},
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // The coordinator agent declares SubAgents. ADK automatically generates
+ // request_task_weather_checker and request_task_flight_booker tools so the
+ // coordinator can delegate work to each subagent.
+ return llmagent.New(llmagent.Config{
+ Name: "travel_planner",
+ Model: model,
+ Description: "Coordinator agent that delegates to weather and flight subagents.",
+ Instruction: "Help the user plan their trip. Use the weather checker and flight booker as needed.",
+ SubAgents: []agent.Agent{weatherAgent, flightAgent},
+ })
+}
+
+// --8<-- [end:get-started]
+
+func main() {
+ ctx := context.Background()
+
+ rootAgent, err := newCollaborativeTeam(ctx)
+ if err != nil {
+ log.Fatalf("newCollaborativeTeam: %v", err)
+ }
+ log.Printf("created coordinator agent: %s", rootAgent.Name())
+}
diff --git a/examples/java/demos/patent-search-agent/README.md b/examples/java/demos/patent-search-agent/README.md
index b24713be46..cb34965f2d 100644
--- a/examples/java/demos/patent-search-agent/README.md
+++ b/examples/java/demos/patent-search-agent/README.md
@@ -6,7 +6,7 @@ Using ADK Java SDK to perform the popular Patent Search (Contextual Search) use
2. Set env variables:
```
-export GOOGLE_GENAI_USE_VERTEXAI=FALSE
+export GOOGLE_GENAI_USE_ENTERPRISE=FALSE
export GOOGLE_API_KEY=""
```
3. Update the placeholders in the code with values from your project (like PROJECT_ID etc.)
diff --git a/examples/python/notebooks/express-mode-weather-agent.ipynb b/examples/python/notebooks/express-mode-weather-agent.ipynb
index 8655e292a0..e699dcf726 100644
--- a/examples/python/notebooks/express-mode-weather-agent.ipynb
+++ b/examples/python/notebooks/express-mode-weather-agent.ipynb
@@ -102,7 +102,7 @@
"express_mode_api_key = \"YOUR-EXPRESS-MODE-API-KEY\" # @param {type:\"string\"}\n",
"os.environ[\"GOOGLE_API_KEY\"] = express_mode_api_key\n",
"# Set vertex to true\n",
- "os.environ[\"GOOGLE_GENAI_USE_VERTEXAI\"] = \"True\"\n",
+ "os.environ[\"GOOGLE_GENAI_USE_ENTERPRISE\"] = \"True\"\n",
"\n",
"# --- Verify Keys (Optional Check) ---\n",
"print(\"API Keys Set:\")\n",
diff --git a/examples/python/notebooks/shop_agent.ipynb b/examples/python/notebooks/shop_agent.ipynb
index 2a635a136d..da48cb8c54 100644
--- a/examples/python/notebooks/shop_agent.ipynb
+++ b/examples/python/notebooks/shop_agent.ipynb
@@ -147,14 +147,14 @@
"from getpass import getpass\n",
"\n",
"# Set environment variables required for running ADK (with Gemini API Key)\n",
- "os.environ[\"GOOGLE_GENAI_USE_VERTEXAI\"] = \"False\"\n",
+ "os.environ[\"GOOGLE_GENAI_USE_ENTERPRISE\"] = \"False\"\n",
"os.environ[\"GOOGLE_API_KEY\"] = getpass(\"Enter your Gemini API Key: \")\n",
"\n",
"# To use Agent Platform instead of Gemini API Key in Colab Enterprise or Cloud Workbench, use the following:\n",
"# [PROJECT_ID] = !gcloud config list --format \"value(core.project)\"\n",
"# os.environ[\"GOOGLE_CLOUD_PROJECT\"] = PROJECT_ID\n",
"# os.environ[\"GOOGLE_CLOUD_LOCATION\"] = \"us-central1\"\n",
- "# os.environ[\"GOOGLE_GENAI_USE_VERTEXAI\"] = \"True\""
+ "# os.environ[\"GOOGLE_GENAI_USE_ENTERPRISE\"] = \"True\""
]
},
{
diff --git a/examples/python/snippets/streaming/adk-streaming-ws/tests/test_log_20251029_151045.md b/examples/python/snippets/streaming/adk-streaming-ws/tests/test_log_20251029_151045.md
index 9ab2fd4737..1fda526d86 100644
--- a/examples/python/snippets/streaming/adk-streaming-ws/tests/test_log_20251029_151045.md
+++ b/examples/python/snippets/streaming/adk-streaming-ws/tests/test_log_20251029_151045.md
@@ -13,7 +13,7 @@
## Configuration
```env
-GOOGLE_GENAI_USE_VERTEXAI=TRUE
+GOOGLE_GENAI_USE_ENTERPRISE=TRUE
GOOGLE_CLOUD_PROJECT=gcp-samples-ic0
GOOGLE_CLOUD_LOCATION=us-central1
DEMO_AGENT_MODEL=gemini-live-2.5-flash-preview-native-audio-09-2025
diff --git a/examples/python/tutorial/agent_team/adk-tutorial/readme.md b/examples/python/tutorial/agent_team/adk-tutorial/readme.md
index fccd597b70..f527c11630 100644
--- a/examples/python/tutorial/agent_team/adk-tutorial/readme.md
+++ b/examples/python/tutorial/agent_team/adk-tutorial/readme.md
@@ -63,7 +63,7 @@ Before running any agent step, you **must** configure your API keys.
**Example `.env` content:**
```dotenv
# Set to False to use API keys directly (required for multi-model)
- GOOGLE_GENAI_USE_VERTEXAI=FALSE
+ GOOGLE_GENAI_USE_ENTERPRISE=FALSE
# --- Replace with your actual keys ---
GOOGLE_API_KEY=PASTE_YOUR_ACTUAL_GOOGLE_API_KEY_HERE
diff --git a/examples/python/tutorial/agent_team/adk_tutorial.ipynb b/examples/python/tutorial/agent_team/adk_tutorial.ipynb
index e17a5678fc..4eec2a6391 100644
--- a/examples/python/tutorial/agent_team/adk_tutorial.ipynb
+++ b/examples/python/tutorial/agent_team/adk_tutorial.ipynb
@@ -148,7 +148,7 @@
")\n",
"\n",
"# Configure ADK to use API keys directly (not Agent Platform for this multi-model setup)\n",
- "os.environ[\"GOOGLE_GENAI_USE_VERTEXAI\"] = \"False\"\n",
+ "os.environ[\"GOOGLE_GENAI_USE_ENTERPRISE\"] = \"False\"\n",
"\n",
"\n",
"# @markdown **Security Note:** It's best practice to manage API keys securely (e.g., using Colab Secrets or environment variables) rather than hardcoding them directly in the notebook. Replace the placeholder strings above."
diff --git a/overrides/main.html b/overrides/main.html
index 0fa0b27727..2d78c6fc13 100644
--- a/overrides/main.html
+++ b/overrides/main.html
@@ -49,10 +49,8 @@
{% block announce %}
- ADK Python 2.0 GA
+ ADK Go 2.0 GA
- is LIVE with graph workflows and collaborative agents, and check out
- ADK Kotlin!
+ is LIVE with graph workflows and collaborative agents! Get started.
{% endblock %}
diff --git a/tools/go-snippets/files_to_test.txt b/tools/go-snippets/files_to_test.txt
index 65c901179c..1509350390 100644
--- a/tools/go-snippets/files_to_test.txt
+++ b/tools/go-snippets/files_to_test.txt
@@ -46,3 +46,9 @@ snippets/sessions/memory_example/memory_example.go
snippets/tools-custom/customer_support_agent/main.go
snippets/get-started/multi_tool_agent/main.go
snippets/runtime/triggers/event_processing_agent.go
+snippets/graphs/human-input/main.go
+snippets/graphs/index/main.go
+snippets/graphs/routes/main.go
+snippets/graphs/data-handling/main.go
+snippets/graphs/dynamic/main.go
+snippets/workflows/collaboration/main.go
diff --git a/tools/go-snippets/runner.sh b/tools/go-snippets/runner.sh
index ee3acca0b6..6850dfe76b 100755
--- a/tools/go-snippets/runner.sh
+++ b/tools/go-snippets/runner.sh
@@ -84,10 +84,15 @@ get_command_for_action() {
local command=""
if [ "${action}" == "build" ]; then
- # For 'build', extract only the file paths, ignoring any arguments.
- # 'go build' does not accept application arguments, so they must be stripped.
- local files_to_build=$(echo "${line}" | awk '{for(i=1;i<=NF;i++) if($i ~ /\.go$/) printf "%s ", $i}')
- command="go build ${files_to_build}"
+ # For 'build', build by package directory rather than by individual file.
+ # File-mode `go build pkg/main.go` ignores Go build constraints (e.g.
+ # //go:build tags), so a constrained file would be compiled regardless.
+ # Building the package directory honors those constraints. Each line lists
+ # files from a single package directory, and `go build ./dir/` is
+ # non-recursive, so this matches the intended target. Arguments are dropped
+ # because `go build` does not accept application arguments.
+ local dirs_to_build=$(echo "${line}" | awk '{for(i=1;i<=NF;i++) if($i ~ /\.go$/){d=$i; if (sub(/\/[^\/]*$/,"",d)) print "./"d"/"; else print "./"}}' | sort -u | tr '\n' ' ')
+ command="go build ${dirs_to_build}"
elif [ "${action}" == "run" ]; then
# For 'run', use the line as is, as 'go run' will pass arguments to the application.
command="go run ${line}"
@@ -137,7 +142,7 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
# Update to the latest version of the ADK.
# This ensures that we are always testing against the most recent release.
- execute_and_check "(cd examples/go && go get google.golang.org/adk@latest)" "Updating google.golang.org/adk to latest"
+ execute_and_check "(cd examples/go && go get google.golang.org/adk/v2@latest)" "Updating google.golang.org/adk/v2 to latest"
if [ ${EXIT_CODE} -ne 0 ]; then
exit ${EXIT_CODE} # Exit immediately if go get failed.
fi