* 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>
19 KiB
Data handling for agent workflows
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.