mirror of
https://github.com/google/adk-docs.git
synced 2026-09-14 16:16:59 +08:00
1203686bb4
* Add TypeScript tabs to the graph workflow pages The five /graphs/ pages documented graph workflows for Python and Go only, so a TypeScript reader had to infer the API from the Python tab — which does not translate: TypeScript has no `@node` decorator, schemas are Zod objects rather than pydantic models, state is written through `ctx.state` instead of returned on an event, and a user-facing message is the event's `content` rather than a `message` field. Every section that has a Python tab now has a TypeScript tab before the Go one, backed by 26 snippet files under examples/typescript/snippets/graphs/. The snippets are ported from the runnable samples in adk-js (samples/workflows/), which already map 1:1 to these section anchors, and they all type-check against the adk-js workflow API. The tabs also call out the behaviours that are easy to get wrong and have no Python equivalent: `ctx.runNode()` resolves to a node result rather than the output, and does not throw when a child interrupts; a second event carrying `output` silently overwrites the first; `LlmAgent.inputSchema` is not the node's input contract inside a graph. * Drop inline comments from the graph workflow snippets The `//` annotations inside the snippet regions duplicated the prose that already introduces each tab, and they were the first thing a reader saw in a rendered sample rather than the API itself. Removes the 83 `//` comments inside the `--8<--` regions across all 26 files. JSDoc blocks stay, since they document what a function or schema is rather than annotating a line; the Apache headers and the per-file orientation comments above each region are untouched, and neither renders on the docs site anyway. Verified comment-only: compiling every file before and after with `tsc --removeComments` produces byte-identical `.js` and `.d.ts` output across all 52 emitted files. * Use single-quoted strings in the graph workflow snippets The 26 files landed double-quoted, which reads as a deliberate choice next to the existing TypeScript snippets under examples/typescript/snippets/ — those are predominantly single-quoted (49 of 68 imports). The repo has no prettier config, so nothing enforces either style; this just stops the new directory looking different from its neighbours. Formatting only: `prettier --no-config --single-quote`, and every changed line differs from the original by a quote character alone. Compiling before and after with `tsc --removeComments` produces output whose only differences are the same quote swaps, since tsc preserves the source quote style. * Restore the upstream titleCase guard in the nested workflow snippet Porting samples/workflows/routes/nested_workflow inlined the `titleCase` helper and dropped the check that a character's uppercase form is a single code point, along with the comment explaining why it is there. That changed behaviour for word-initial characters whose uppercase expands: "first draft" became "FIrst Draft" and "ßeta test" became "SSeta Test", where upstream leaves both alone. Restores the helper, the guard and the rationale. Because the helper sits inside the --8<-- region, the explanation now renders on the page as well, so the next person to touch it can see what the guard is for. Also restores an unused `_ctx` parameter in the user_message snippet, the only other place the port had drifted from upstream. Verified by compiling each of the 26 snippets and its upstream counterpart at adk-v2.0.0 with `tsc --removeComments` and comparing the emitted JavaScript: all 26 are now semantically identical to samples/workflows/. * Address review: plainer wording, and lift shared cautions out of the tabs Wording, across all TypeScript tabs: - No sentence starts with code syntax. "`route` is independent of..." becomes "The `route` value is independent of...", and the same for the other cases. - Removed informal and editorial phrasing: "earn their keep", "dropped straight into", "reach for", "hands you", "two things to know going in", "kick the children off", "fails loudly". - Spelled out "/" as "and" in the `inputSchema` and `outputSchema` sentence. - Described the `ctx.runNode()` interrupt behaviour in full rather than only as "does not throw": it returns normally with `interruptIds` populated and `output` undefined, and an orchestrator that skips the check continues with a value the user never supplied. - Explained what a JoinNode waits for instead of referring to "the barrier". - Tied the `rerunOnResume` option back to the code sample it follows, and introduced the two orchestrator details by saying when they matter. Structure: - The "Response schema input limitations" note appeared in both the Python and TypeScript tabs. Replaced both with one language-neutral note after the code examples. - The "Stuck JoinNode" caution appeared in all three tabs. Replaced them with one caution after the code examples, stating the rule that every node feeding a join must produce an output. - Moved the unbounded-cycle caution out of the TypeScript tab to the end of the section, since it is not language specific. Snippet header comments got the same wording pass. Verified afterwards: the 26 snippets still type-check, all 53 snippet includes resolve, and every snippet is still semantically identical to samples/workflows/ at adk-v2.0.0. * Apply suggestion from @joefernandez --------- Co-authored-by: Joe Fernandez <931947+joefernandez@users.noreply.github.com>
515 lines
19 KiB
Markdown
515 lines
19 KiB
Markdown
# Data handling for agent workflows
|
|
|
|
<div class="language-support-tag">
|
|
<span class="lst-supported">Supported in ADK</span><span class="lst-python">Python v2.0.0</span><span class="lst-typescript">TypeScript v2.0.0</span><span class="lst-go">Go v2.0.0</span>
|
|
</div>
|
|
|
|
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. 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.
|
|
|
|
=== "TypeScript"
|
|
|
|
In ADK TypeScript v2.0.0, nodes exchange data through events. The key
|
|
fields for node data handling are:
|
|
|
|
- **`output`**: the value passed to the next node. Return a value
|
|
directly and ADK wraps it in an event, or set the field explicitly
|
|
with `createEvent({output})`.
|
|
- **`content`**: a message for the user. The runtime renders this
|
|
field, but the graph does not pass it to the next node.
|
|
- **`route`**: the routing keys that select which conditional edge to
|
|
follow.
|
|
|
|
Session state is separate from the event. A node reads and writes state
|
|
through `ctx.state`, and the accumulated delta is attached to that
|
|
node's events. State keys can carry a prefix that controls their
|
|
lifetime and scope:
|
|
|
|
| Prefix | Scope |
|
|
|---|---|
|
|
| `app:` | Shared across all users and sessions for the app |
|
|
| `user:` | Tied to the user, shared across their sessions |
|
|
| `temp:` | Discarded after the current invocation ends |
|
|
| *(none)* | Persists for the lifetime of the 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.
|
|
|
|
=== "TypeScript"
|
|
|
|
There are three equivalent ways to produce a node's output: return a
|
|
value directly, return `createEvent({output})`, or yield events from an
|
|
async generator to stream progress alongside the result.
|
|
|
|
```typescript
|
|
--8<-- "examples/typescript/snippets/graphs/data-handling/node_output.ts:node-output"
|
|
```
|
|
|
|
!!! warning "Caution: emit `output` from one event per execution"
|
|
|
|
A node can yield any number of events carrying `output`, and ADK
|
|
does not raise an error in this case. Each event overwrites the
|
|
previous one, and the successor node receives only the final value.
|
|
Use `content` for progress messages instead.
|
|
|
|
=== "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.
|
|
|
|
=== "TypeScript"
|
|
|
|
The `output` field is not limited to text. Any serializable value is
|
|
passed to the next node, which receives it as a typed object, with no
|
|
JSON parsing or state reads required. Attaching an `outputSchema` to the
|
|
producing node, or an `inputSchema` to the consuming node, makes the
|
|
contract explicit and validates it at runtime:
|
|
|
|
```typescript
|
|
--8<-- "examples/typescript/snippets/graphs/data-handling/structured_output.ts:structured-output"
|
|
```
|
|
|
|
=== "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")
|
|
```
|
|
|
|
=== "TypeScript"
|
|
|
|
The `route` value is independent of `output`, so one event can both
|
|
select a branch and forward a payload to it. The `DEFAULT_ROUTE` setting
|
|
catches any value that no other branch matched:
|
|
|
|
```typescript
|
|
--8<-- "examples/typescript/snippets/graphs/data-handling/routing_output.ts:routing-output"
|
|
```
|
|
|
|
=== "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...")
|
|
```
|
|
|
|
=== "TypeScript"
|
|
|
|
A message for the user is the event's `content` field. The runtime
|
|
renders `content`, but the graph does not pass it to the next node. Use
|
|
`content` for the user and `output` for the next node. A node can emit
|
|
both by sending two events, where only one carries `output`:
|
|
|
|
```typescript
|
|
--8<-- "examples/typescript/snippets/graphs/data-handling/user_message.ts:user-message"
|
|
```
|
|
|
|
=== "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.
|
|
|
|
=== "TypeScript"
|
|
|
|
Write state through `ctx.state` rather than returning it. A write is
|
|
visible to every later node in the same run, and is committed with the
|
|
writing node's events:
|
|
|
|
```typescript
|
|
--8<-- "examples/typescript/snippets/graphs/data-handling/session_state.ts:session-state"
|
|
```
|
|
|
|
!!! warning "Caution: `state` data limitations"
|
|
|
|
Session state is a lightweight key-value store. Do not use it to
|
|
move large payloads between nodes; use artifacts or a database tool
|
|
instead. When only the next node needs a value, pass it along the
|
|
edge as node `output`. Use state when a value must outlive the run,
|
|
or be read by a tool, a callback, or `{key}` instruction templating.
|
|
|
|
=== "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],
|
|
...
|
|
)
|
|
```
|
|
|
|
=== "TypeScript"
|
|
|
|
Schemas are Zod objects or a genai `Schema`. The location of the schema
|
|
determines its effect:
|
|
|
|
- The `LlmAgent.outputSchema` option requires the model to answer in
|
|
that shape.
|
|
- The `LlmAgent.inputSchema` option applies only when the agent is
|
|
exposed as a tool. Inside a graph, set the schema that validates a
|
|
node's input on the node itself, using `node(agent, {inputSchema})`.
|
|
|
|
Agents in a graph must run in `single_turn` mode, which is the default,
|
|
or `task` mode.
|
|
|
|
```typescript
|
|
--8<-- "examples/typescript/snippets/graphs/data-handling/schemas.ts:schemas"
|
|
```
|
|
|
|
=== "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
|
|
|
|
=== "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 <CityTime.time_info from lookup_time_function> in
|
|
<CityTime.city from lookup_time_function> right now.
|
|
""",
|
|
)
|
|
|
|
root_agent = Workflow(
|
|
name="root_agent",
|
|
edges=[
|
|
(START, city_generator_agent, lookup_time_function, city_report_agent)
|
|
],
|
|
)
|
|
```
|
|
|
|
=== "TypeScript"
|
|
|
|
Two data-selection forms are available inside an agent instruction:
|
|
|
|
- The `{Class.field}` form reads a field from this node's input.
|
|
- The `<Class.field from source_node>` form reads a field from a named
|
|
predecessor's output. Use this form when several upstream nodes
|
|
share a field name.
|
|
|
|
Both forms are distinct from `{state_key}`, which reads session state.
|
|
The `Class.` prefix is documentation only; resolution uses the field
|
|
name after the dot.
|
|
|
|
```typescript
|
|
--8<-- "examples/typescript/snippets/graphs/data-handling/structured_access.ts:structured-access"
|
|
```
|
|
|
|
=== "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).
|