Files
google__adk-docs/docs/graphs/data-handling.md
Alexey Kalenkevich 1203686bb4 Add TypeScript tabs to the graph workflow pages (#2167)
* 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>
2026-08-28 15:39:52 -07:00

19 KiB

Data handling for agent workflows

Supported in ADKPython v2.0.0TypeScript 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. 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.