diff --git a/docs/graphs/data-handling.md b/docs/graphs/data-handling.md
index ff02684041..5ddadc744f 100644
--- a/docs/graphs/data-handling.md
+++ b/docs/graphs/data-handling.md
@@ -1,7 +1,7 @@
# Data handling for agent workflows
- Supported in ADKPython v2.0.0Go v2.0.0
+ Supported in ADKPython v2.0.0TypeScript v2.0.0Go v2.0.0
Structuring and managing data between agents and graph-based nodes is critical
@@ -28,6 +28,31 @@ receives it as its typed input.
- **`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
@@ -89,6 +114,23 @@ Each step in a workflow produces output for its successor.
***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.
@@ -131,6 +173,18 @@ Each step in a workflow produces output for its successor.
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
@@ -159,6 +213,16 @@ Each step in a workflow produces output for its successor.
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
@@ -184,6 +248,17 @@ Each step in a workflow produces output for its successor.
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
@@ -239,6 +314,24 @@ inside tools and callbacks regardless of which agent style you use.
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
@@ -306,6 +399,24 @@ accepted and produced by any agent node.
)
```
+=== "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
@@ -368,6 +479,23 @@ accepted and produced by any agent node.
)
```
+=== "TypeScript"
+
+ Two data-selection forms are available inside an agent instruction:
+
+ - The `{Class.field}` form reads a field from this node's input.
+ - The `` 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
diff --git a/docs/graphs/dynamic.md b/docs/graphs/dynamic.md
index d176affd09..8568d6db84 100644
--- a/docs/graphs/dynamic.md
+++ b/docs/graphs/dynamic.md
@@ -1,7 +1,7 @@
# Dynamic agent workflows
- Supported in ADKPython v2.0.0Go v2.0.0
+ Supported in ADKPython v2.0.0TypeScript v2.0.0Go v2.0.0
The ADK framework provides a programmatic way to define workflows as a more
@@ -66,6 +66,26 @@ workflow containing a single node with a function:
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.
+=== "TypeScript"
+
+ TypeScript has no `@node` decorator. Use the `node(fn, options)` factory
+ function instead. The `ctx.runNode()` method is the equivalent of
+ `ctx.run_node()`:
+
+ ```typescript
+ --8<-- "examples/typescript/snippets/graphs/dynamic/get_started.ts:get-started"
+ ```
+
+ When you write an orchestrator node, two details affect how you read
+ results and how the workflow behaves after a pause:
+
+ - The `ctx.runNode()` method resolves to a node result, not to the
+ output value. Read the `.output` property to get the value.
+ - An orchestrator that calls `ctx.runNode()` must set
+ `rerunOnResume: true`. This setting causes the node body to re-run
+ on resume, so already-finished children are replayed from their
+ checkpoints instead of being executed again.
+
=== "Go"
In Go, `workflow.NewFunctionNode` replaces the `@node` decorator and
@@ -125,6 +145,31 @@ run within a workflow.
same function with different configurations, or if you are managing node
references in a registry for advanced orchestration.
+=== "TypeScript"
+
+ There are two ways to build a node: the `node(fn, options)` factory
+ function, and the explicit `new FunctionNode(name, fn, config)`
+ constructor. Use the constructor when you are wrapping a function from
+ another library, need several differently configured nodes from one
+ function, or keep node references in a registry for advanced
+ orchestration.
+
+ ```typescript
+ --8<-- "examples/typescript/snippets/graphs/dynamic/nodes.ts:node-forms"
+ ```
+
+ In this code sample, the most important option is `rerunOnResume`, which
+ controls what happens when a workflow resumes after a human-in-the-loop
+ pause:
+
+ - **`true` (re-entry):** the node body re-runs from the top. Use this
+ setting for any orchestrator that calls `ctx.runNode()`. The body
+ re-executes, and already-completed child activations are skipped
+ automatically.
+ - **`false` (handoff, the leaf default):** the resume payload is
+ routed to the node's successor as input, bypassing the interrupted
+ node.
+
=== "Go"
In Go, `workflow.NewFunctionNode[IN, OUT]` wraps a plain function as a
@@ -196,6 +241,16 @@ execution logic (order and paths) for those nodes.
)
```
+=== "TypeScript"
+
+ The orchestrator is an async function that awaits `ctx.runNode()` for
+ each child step. Wrap it as a node with `rerunOnResume: true` and use it
+ as the graph's only edge:
+
+ ```typescript
+ --8<-- "examples/typescript/snippets/graphs/dynamic/nodes.ts:workflows"
+ ```
+
=== "Go"
`workflow.NewDynamicNode` creates an orchestrator whose body calls
@@ -264,6 +319,20 @@ manually read and write session state keys for data transfer.
return report_text
```
+=== "TypeScript"
+
+ The `ctx.runNode()` function returns the child's result directly, so
+ there are no session-state keys to read and write to move a value one
+ step downstream. This function accepts any node-like value, including an
+ `LlmAgent`, without wrapping it in `node()` first:
+
+ ```typescript
+ --8<-- "examples/typescript/snippets/graphs/dynamic/data_handling.ts:data-handling"
+ ```
+
+ Schemas work the same way as in a graph. Attach them to the nodes you
+ run, as shown in the [sequence route](#sequence-route) section.
+
=== "Go"
In Go, `workflow.NewAgentNode` wraps an `agent.Agent` so it can be
@@ -305,6 +374,15 @@ as you can with graph-based workflows.
return report_text
```
+=== "TypeScript"
+
+ A sequential route awaits `ctx.runNode()` calls one after another. Each
+ call finishes before the next one starts:
+
+ ```typescript
+ --8<-- "examples/typescript/snippets/graphs/dynamic/sequence_route.ts:sequence-route"
+ ```
+
=== "Go"
Call `workflow.RunNode` sequentially inside a `NewDynamicNode` body —
@@ -366,6 +444,18 @@ workflows offer much more flexibility to define the routing logic you need.
return code
```
+=== "TypeScript"
+
+ Dynamic workflows can help keep workflow logic simple by defining an
+ iteration as an ordinary loop rather than a back-edge in a graph. Values
+ are held in local variables, and state is written only where an agent's
+ instruction template needs to read it back. Unlike a graph cycle, the
+ loop is bounded by its loop condition:
+
+ ```typescript
+ --8<-- "examples/typescript/snippets/graphs/dynamic/loop_route.ts:loop-route"
+ ```
+
=== "Go"
In Go, the loop is a plain `for` loop inside the dynamic node body. The
@@ -412,6 +502,27 @@ Dynamic workflows in ADK can support parallel execution.
only failed or interrupted worker nodes are re-executed, including
parallel worker nodes.
+=== "TypeScript"
+
+ The `ctx.runNode()` method returns a promise, so starting every child
+ before awaiting any of them runs the children concurrently, and
+ `Promise.all` collects the results. Run IDs are assigned in call order,
+ so start the children in a synchronous loop to keep the IDs
+ deterministic across a resume:
+
+ ```typescript
+ --8<-- "examples/typescript/snippets/graphs/dynamic/parallel_route.ts:parallel-route"
+ ```
+
+ !!! tip "Tip: prefer the built-in parallel worker"
+
+ To run one node over each item in a list, use
+ `node(worker, {parallelWorker: true, maxParallelWorkers: 4})`. This
+ option performs the fan-out and bounds concurrency, which defaults
+ to 8. Use the manual approach shown above when you need custom
+ scheduling or partial-failure handling. On resume, only failed or
+ interrupted workers re-execute in both cases.
+
=== "Go"
In Go, `workflow.NewParallelWorker` wraps a child node and runs it
@@ -469,6 +580,25 @@ Dynamic workflows in ADK can also include human input or human in the loop
Parent nodes in dynamic workflows that call `ctx.run_node` must set
`rerun_on_resume=True` to handle interruptions properly.
+=== "TypeScript"
+
+ The leaf node returns a `RequestInput` to pause the workflow, and keeps
+ the default `rerunOnResume: false` so the reply becomes its output. The
+ orchestrator that calls it must set `rerunOnResume: true`:
+
+ ```typescript
+ --8<-- "examples/typescript/snippets/graphs/dynamic/human_input.ts:human-input"
+ ```
+
+ !!! important "Important: check `interruptIds` before deciding"
+
+ The `ctx.runNode()` method does **not** throw an error when a child
+ node interrupts. It returns normally, with the `interruptIds`
+ property of the result populated and the `output` property still
+ `undefined`. Check `interruptIds` before you use the result. An
+ orchestrator that skips this check treats the missing output as an
+ answer and continues with a value the user never supplied.
+
=== "Go"
In Go, use `workflow.NewEmittingFunctionNode` with
@@ -546,6 +676,16 @@ and logically remain the same for the input.
least one non-numeric character to avoid collisions with these
auto-generated IDs.
+=== "TypeScript"
+
+ Pass a `runId` as a trailing option to `ctx.runNode()`. The ID must
+ contain at least one non-numeric character so it does not collide with
+ the auto-generated sequential IDs:
+
+ ```typescript
+ --8<-- "examples/typescript/snippets/graphs/dynamic/custom_run_ids.ts:custom-execution-ids"
+ ```
+
=== "Go"
In Go, pass `workflow.WithRunID("order-x")` as a trailing option to
diff --git a/docs/graphs/human-input.md b/docs/graphs/human-input.md
index ca3d0a42d3..ce63964ec3 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.0Go v2.0.0
+ Supported in ADKPython v2.0.0TypeScript v2.0.0Go v2.0.0
Being able to request human input for data input, decision verification, or
@@ -39,6 +39,23 @@ the input process more predictable and reliable.
system receives an input from a user. Once the system receives input from the
user, that input is passed to the next node.
+=== "TypeScript"
+
+ In ADK TypeScript v2.0.0, a human input node yields a `RequestInput`.
+ The `step1` node pauses the workflow until the user replies, and the
+ reply is passed to the next node as its input. A human-in-the-loop node
+ does not require a model, which makes the pause deterministic.
+
+ ```typescript
+ --8<-- "examples/typescript/snippets/graphs/human-input/get_started.ts:get-started"
+ ```
+
+ This implementation shows the default `rerunOnResume: false` handoff:
+ the interrupted node does not re-run. It completes with the user's reply
+ as its output. A node that calls `ctx.runNode()` needs
+ `rerunOnResume: true` instead. For more information, see
+ [human input in dynamic workflows](/graphs/dynamic/#human-input).
+
=== "Go"
In ADK Go v2.0.0, a HITL graph node is built with
@@ -69,13 +86,27 @@ the input process more predictable and reliable.
request.
- **`response_schema`:** A data structure the human response must conform to.
- !!! note "Note: Response schema input limitations"
+=== "TypeScript"
- 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.
+ The `RequestInput` class takes the following configuration options:
+
+ - **`message`:** Text shown to the user explaining what is being
+ asked.
+ - **`payload`:** Structured data sent with the prompt, so a client can
+ render additional context.
+ - **`responseSchema`:** The shape the reply is expected to take. The
+ schema travels on the interrupt as
+ `functionCall.args.response_schema`, which a client reads to render
+ a form for the reply.
+
+ The `rerunOnResume` option on the node controls what happens when the
+ reply arrives:
+
+ - **`false`** (the leaf default): the reply is routed to the node's
+ successor as input, bypassing the interrupted node.
+ - **`true`**: the node body re-runs from the top. This setting is
+ required for any node that calls `ctx.runNode()`, so it can deliver
+ cached child results on resume.
=== "Go"
@@ -106,6 +137,13 @@ the input process more predictable and reliable.
include a UI or a downstream agent node to validate the response before
acting on it.
+!!! note "Note: Response schema input limitations"
+
+ A response schema does not reformat a human reply to fit the specified
+ structure. The reply must already be in that format. For a better user
+ experience, collect structured data in your client interface, or place an
+ agent node after the pause to convert the reply into the required format.
+
## Human input examples
The following code examples demonstrate more detailed human input requests.
@@ -149,6 +187,16 @@ The following code examples demonstrate more detailed human input requests.
)
```
+=== "TypeScript"
+
+ The following three-node graph builds a structured itinerary, sends it
+ as `payload` with the prompt so a client can render it, and then acts on
+ the user's feedback:
+
+ ```typescript
+ --8<-- "examples/typescript/snippets/graphs/human-input/payload_and_schema.ts:payload-and-schema"
+ ```
+
=== "Go"
The following code sample shows a three-node graph: a builder node generates
@@ -190,6 +238,18 @@ specific tool call.
yield RequestInput(message=input_message, response_schema=str)
```
+=== "TypeScript"
+
+ Set `requireConfirmation: true` on a `FunctionTool` to make the agent
+ pause for approval before that tool runs. A graph human-in-the-loop node
+ serves a different purpose: instead of confirming a tool call, it can
+ start the workflow by asking the user for input. The
+ `responseSchema: z.string()` option requests a plain text reply:
+
+ ```typescript
+ --8<-- "examples/typescript/snippets/graphs/human-input/initial_prompt.ts:initial-prompt"
+ ```
+
=== "Go"
Set `RequireConfirmation: true` in `functiontool.Config` for a static
diff --git a/docs/graphs/index.md b/docs/graphs/index.md
index 3211edc40d..c71848bcf6 100644
--- a/docs/graphs/index.md
+++ b/docs/graphs/index.md
@@ -1,7 +1,7 @@
# Graph-based agent workflows
- Supported in ADKPython v2.0.0Go v2.0.0
+ Supported in ADKPython v2.0.0TypeScript v2.0.0Go v2.0.0
Graph-based agent workflows in ADK let you build agents with more precise control,
@@ -101,6 +101,20 @@ function, and the final agent reports the information.
)
```
+=== "TypeScript"
+
+ In ADK TypeScript v2.0.0, a `Workflow` takes an `edges` array. Each row
+ lists the nodes to run in order. The `node()` function wraps a function,
+ an agent, a tool, or another `Workflow` as a graph node, and sets the
+ node's name and its `inputSchema` and `outputSchema` contracts. Schemas
+ are Zod objects or a genai `Schema`. Each node's return value is passed
+ to the next node as its input, so you do not need to write to session
+ state.
+
+ ```typescript
+ --8<-- "examples/typescript/snippets/graphs/index/get_started.ts:get-started"
+ ```
+
=== "Go"
In ADK Go v2.0.0, sequential workflows use the graph engine:
@@ -194,6 +208,19 @@ translated into a graph-based agent:
)
```
+=== "TypeScript"
+
+ In ADK TypeScript v2.0.0, a router node returns an event carrying a
+ `route` value, created with `createEvent({route})`. A second edge row
+ maps each route value to the node that handles it. Setting `route` to an
+ array dispatches to every matching branch, which lets the classifier in
+ this example return more than one category. The `DEFAULT_ROUTE` setting
+ catches any value that no branch matched.
+
+ ```typescript
+ --8<-- "examples/typescript/snippets/graphs/index/process_pipeline.ts:process-pipeline"
+ ```
+
=== "Go"
In ADK Go v2.0.0, conditional routing uses `workflow.NewEmittingFunctionNode`
diff --git a/docs/graphs/routes.md b/docs/graphs/routes.md
index d05bcdf86b..b4186e455e 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.0Go v2.0.0
+ Supported in ADKPython v2.0.0TypeScript v2.0.0Go v2.0.0
Graph-based workflows in ADK define agent logic as a graph of execution nodes
@@ -35,6 +35,25 @@ agents.
)
```
+=== "TypeScript"
+
+ ```typescript
+ export const rootAgent = new Workflow({
+ name: 'routing_workflow',
+ edges: [
+ ['START', processMessage, router],
+ [
+ router,
+ {
+ 'output-1': response1,
+ 'output-2': response2,
+ 'output-3': response3,
+ },
+ ],
+ ],
+ });
+ ```
+
=== "Go"
ADK Go v2.0.0 provides the following approach to graph-based
@@ -91,6 +110,19 @@ objects.
return Event(output=input_text_modified)
```
+=== "TypeScript"
+
+ In ADK TypeScript v2.0.0, the primary node type is a `FunctionNode`,
+ created by passing a function to `node()`. A handler always takes
+ `(ctx, input)` parameters; ADK does not inject values by parameter name.
+ Returning a value directly wraps it in the event's `output` field.
+ Returning `createEvent({output})` is the explicit form, which you need
+ when you also set `route` or `content`:
+
+ ```typescript
+ --8<-- "examples/typescript/snippets/graphs/routes/function_node.ts:function-node"
+ ```
+
=== "Go"
In ADK Go v2.0.0, the primary node type is `workflow.NewFunctionNode`.
@@ -136,6 +168,24 @@ A sequential route runs each node once, in the listed order.
task_C_node)] # 3 nodes run in order
```
+=== "TypeScript"
+
+ An `edges` row that starts with `'START'` runs each listed node once, in
+ order, and passes every node's return value to the next node:
+
+ ```typescript
+ edges: [['START', taskANode]] // a single node
+ edges: [['START', taskANode, taskBNode, taskCNode]] // three, in order
+ ```
+
+ Listing `'START'` in more than one row creates parallel paths instead.
+ For more information, see
+ [fan out and join](#parallel-tasks-fan-out-and-join-paths).
+
+ ```typescript
+ --8<-- "examples/typescript/snippets/graphs/routes/sequence.ts:sequence"
+ ```
+
=== "Go"
`workflow.Chain(workflow.Start, nodeA, nodeB, nodeC)` wires nodes into a
@@ -185,6 +235,19 @@ A sequential route runs each node once, in the listed order.
)
```
+=== "TypeScript"
+
+ Branching requires a node that emits a `route` value, and an edge row
+ that maps each route value to the node that handles it. Route values can
+ be strings, numbers, or booleans. The `DEFAULT_ROUTE` setting matches
+ when no other route on the same source node matches. A branch target can
+ be any node-like value: in this example, `taskBNode` is an `LlmAgent`
+ and `taskCNode` is a function.
+
+ ```typescript
+ --8<-- "examples/typescript/snippets/graphs/routes/branches.ts:branches"
+ ```
+
=== "Go"
In ADK Go v2.0.0, conditional dispatch uses the `workflow` graph engine.
@@ -281,13 +344,15 @@ before passing results to the next step.
]
```
- !!! warning "Caution: Stuck JoinNode from incomplete nodes"
+=== "TypeScript"
- 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***.
+ A `JoinNode` is the fan-in barrier. This logic mechanism waits for every
+ predecessor task to complete, and then passes its successor a record
+ keyed by predecessor node name:
+
+ ```typescript
+ --8<-- "examples/typescript/snippets/graphs/routes/fan_out_join.ts:fan-out-join"
+ ```
=== "Go"
@@ -323,13 +388,14 @@ before passing results to the next step.
--8<-- "examples/go/snippets/graphs/routes/main.go:parallel-fan-out"
```
- !!! warning "Caution: Stuck JoinNode from incomplete nodes"
+!!! warning "Caution: nodes that feed a JoinNode must produce output"
- `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.
+ A `JoinNode` releases only after all of its predecessor nodes finish.
+ Make sure that every node that feeds a join has an output of its own, and attach a
+ retry configuration to any node that can fail. A predecessor that
+ finishes without an output leaves the join with no value for that
+ branch, and the resulting failure appears downstream, away from the node
+ that caused it.
## Nested workflows
@@ -370,6 +436,20 @@ accomplish this goal.
its process, the parent node extracts data from the final leaf nodes and
emits it as the output of the nested workflow.
+=== "TypeScript"
+
+ A `Workflow` is itself a node, so you can use one inside another
+ workflow's edges to encapsulate a reusable sub-process:
+
+ ```typescript
+ --8<-- "examples/typescript/snippets/graphs/routes/nested_workflow.ts:nested-workflow"
+ ```
+
+ **Nested workflow data output.** While the inner workflow runs, each of
+ its node events bubbles up to the parent for traceability. When it
+ finishes, the output of its terminal node becomes the output of the
+ nested-workflow node.
+
=== "Go"
ADK Go v2.0.0 supports nested workflows in two complementary ways:
@@ -434,6 +514,16 @@ lifecycle on each iteration.
)
```
+=== "TypeScript"
+
+ A loop is a back-edge: a downstream node routes back to an earlier node,
+ and the engine re-activates that node with a fresh lifecycle on each
+ iteration. The loop exits when the router selects the terminal branch:
+
+ ```typescript
+ --8<-- "examples/typescript/snippets/graphs/routes/loop_escalation.ts:loop-escalation"
+ ```
+
=== "Go"
The following example uses the graph engine with `workflow.EdgeBuilder`.
@@ -444,3 +534,10 @@ lifecycle on each iteration.
```go
--8<-- "examples/go/snippets/graphs/routes/main.go:loop-escalate"
```
+
+!!! warning "Caution: unbounded graph cycles"
+
+ A graph cycle is not bounded automatically. Make sure the exit condition
+ eventually becomes true, or express the iteration as a
+ [dynamic workflow](/graphs/dynamic/#loop-route), where the loop runs in
+ your own code and you control its bound.
diff --git a/examples/typescript/snippets/graphs/data-handling/node_output.ts b/examples/typescript/snippets/graphs/data-handling/node_output.ts
new file mode 100644
index 0000000000..051fdcb499
--- /dev/null
+++ b/examples/typescript/snippets/graphs/data-handling/node_output.ts
@@ -0,0 +1,47 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// A node hands data to its successor through the event's `output` field.
+
+// --8<-- [start:node-output]
+import { createEvent, node, NodeContext, Workflow } from '@google/adk';
+
+const returnRawValue = node(
+ (_ctx: NodeContext, nodeInput: string) => nodeInput.toUpperCase(),
+ { name: 'return_raw_value' },
+);
+
+const returnEventOutput = node(
+ (_ctx: NodeContext, nodeInput: string) =>
+ createEvent({ output: `${nodeInput}!` }),
+ { name: 'return_event_output' },
+);
+
+const yieldProgressThenOutput = node(
+ async function* (_ctx: NodeContext, nodeInput: string) {
+ yield createEvent({
+ content: { role: 'model', parts: [{ text: 'Working on it...' }] },
+ });
+ yield createEvent({ output: `<<${nodeInput}>>` });
+ },
+ { name: 'yield_progress_then_output' },
+);
+
+export const rootAgent = new Workflow({
+ name: 'node_output_workflow',
+ edges: [
+ ['START', returnRawValue, returnEventOutput, yieldProgressThenOutput],
+ ],
+});
+// --8<-- [end:node-output]
diff --git a/examples/typescript/snippets/graphs/data-handling/routing_output.ts b/examples/typescript/snippets/graphs/data-handling/routing_output.ts
new file mode 100644
index 0000000000..666b6d5106
--- /dev/null
+++ b/examples/typescript/snippets/graphs/data-handling/routing_output.ts
@@ -0,0 +1,59 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// `route` drives conditional edge dispatch. It is independent of `output`, so a
+// router can select a branch AND forward a payload in the same event.
+
+// --8<-- [start:routing-output]
+import {
+ createEvent,
+ DEFAULT_ROUTE,
+ node,
+ NodeContext,
+ Workflow,
+} from '@google/adk';
+
+const router = node(
+ (_ctx: NodeContext, nodeInput: string) =>
+ createEvent({
+ route: /bug|crash|error/i.test(nodeInput) ? 'BUG' : 'OTHER',
+ output: nodeInput,
+ }),
+ { name: 'router' },
+);
+
+const handleBug = node(
+ (_ctx: NodeContext, nodeInput: string) => `Filed a bug for: ${nodeInput}`,
+ { name: 'handle_bug' },
+);
+
+const handleAnythingElse = node(
+ (_ctx: NodeContext, nodeInput: string) => `No bug detected in: ${nodeInput}`,
+ { name: 'handle_anything_else' },
+);
+
+export const rootAgent = new Workflow({
+ name: 'routing_output_workflow',
+ edges: [
+ ['START', router],
+ [
+ router,
+ {
+ BUG: handleBug,
+ [DEFAULT_ROUTE]: handleAnythingElse,
+ },
+ ],
+ ],
+});
+// --8<-- [end:routing-output]
diff --git a/examples/typescript/snippets/graphs/data-handling/schemas.ts b/examples/typescript/snippets/graphs/data-handling/schemas.ts
new file mode 100644
index 0000000000..ecd8290636
--- /dev/null
+++ b/examples/typescript/snippets/graphs/data-handling/schemas.ts
@@ -0,0 +1,122 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Schemas constrain what a node accepts and produces. Use a Zod object, or a
+// genai `Schema`.
+//
+// Where the schema goes:
+// - `LlmAgent.outputSchema` forces the model to answer in that shape.
+// - `LlmAgent.inputSchema` is only consulted when the agent is exposed as a
+// tool. Inside a graph, the schema that VALIDATES a node's input belongs on
+// the node: `node(agent, {inputSchema})`.
+
+// --8<-- [start:schemas]
+import {
+ FunctionTool,
+ LlmAgent,
+ node,
+ NodeContext,
+ Workflow,
+} from '@google/adk';
+import { z } from 'zod';
+
+const flightSearchInputSchema = z.object({
+ origin: z.string().describe('Origin airport code, e.g. "SFO".'),
+ destination: z.string().describe('Destination airport code, e.g. "CDG".'),
+ departureDate: z.string().describe('Departure date, e.g. "2026-03-15".'),
+ passengers: z.number().describe('Number of passengers.'),
+});
+type FlightSearchInput = z.infer;
+
+const flightSchema = z.object({
+ carrier: z.string(),
+ flightNumber: z.string(),
+ price: z.number(),
+});
+
+const flightSearchOutputSchema = z.object({
+ flights: z.array(flightSchema),
+ cheapestPrice: z.number(),
+});
+type FlightSearchOutput = z.infer;
+
+/** Stands in for a real flight-search API. */
+const searchFlightsApi = new FunctionTool({
+ name: 'search_flights_api',
+ description: 'Searches available flights for a route and date.',
+ parameters: flightSearchInputSchema,
+ execute: ({ origin, destination }) => [
+ {
+ carrier: 'AF',
+ flightNumber: `AF${origin.length}${destination.length}0`,
+ price: 812.4,
+ },
+ {
+ carrier: 'UA',
+ flightNumber: `UA${origin.length}${destination.length}1`,
+ price: 947.0,
+ },
+ ],
+});
+
+const parseRequest = node(
+ (_ctx: NodeContext, nodeInput: string): FlightSearchInput => {
+ const codes = nodeInput.toUpperCase().match(/\b[A-Z]{3}\b/g) ?? [];
+ const date = nodeInput.match(/\d{4}-\d{2}-\d{2}/)?.[0];
+ const passengers = Number(
+ nodeInput.match(/(\d+)\s*(people|pax|passengers?)/i)?.[1],
+ );
+ return {
+ origin: codes[0] ?? 'SFO',
+ destination: codes[1] ?? 'CDG',
+ departureDate: date ?? '2026-03-15',
+ passengers: Number.isFinite(passengers) ? passengers : 1,
+ };
+ },
+ { name: 'parse_request', outputSchema: flightSearchInputSchema },
+);
+
+const flightSearcher = new LlmAgent({
+ name: 'flight_searcher',
+ model: 'gemini-flash-latest',
+ mode: 'single_turn',
+ instruction:
+ 'Search for available flights with the search_flights_api tool and report ' +
+ 'every flight it returns plus the cheapest price.',
+ inputSchema: flightSearchInputSchema,
+ outputSchema: flightSearchOutputSchema,
+ tools: [searchFlightsApi],
+});
+
+const renderResults = node(
+ (_ctx: NodeContext, results: FlightSearchOutput) =>
+ `Cheapest: $${results.cheapestPrice}\n` +
+ results.flights
+ .map((f) => ` ${f.carrier} ${f.flightNumber} — $${f.price}`)
+ .join('\n'),
+ { name: 'render_results', inputSchema: flightSearchOutputSchema },
+);
+
+export const rootAgent = new Workflow({
+ name: 'flight_workflow',
+ edges: [
+ [
+ 'START',
+ parseRequest,
+ node(flightSearcher, { inputSchema: flightSearchInputSchema }),
+ renderResults,
+ ],
+ ],
+});
+// --8<-- [end:schemas]
diff --git a/examples/typescript/snippets/graphs/data-handling/session_state.ts b/examples/typescript/snippets/graphs/data-handling/session_state.ts
new file mode 100644
index 0000000000..2398173c7a
--- /dev/null
+++ b/examples/typescript/snippets/graphs/data-handling/session_state.ts
@@ -0,0 +1,51 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// A node takes an explicit `(ctx, input)` pair and reads and writes session
+// state through `ctx.state`. A write is visible to every later node in the same
+// run, and is committed with the writing node's events.
+
+// --8<-- [start:session-state]
+import { node, NodeContext, Workflow } from '@google/adk';
+
+const initStateNode = node(
+ (ctx: NodeContext, nodeInput: string) => {
+ ctx.state.set('topic', nodeInput.trim());
+ ctx.state.set('temp:started_at', new Date().toISOString());
+ ctx.state.set('attempts', 0);
+ },
+ { name: 'init_state_node' },
+);
+
+const taskAttemptNode = node(
+ (ctx: NodeContext) => {
+ const attempts = ctx.state.get('attempts') ?? 0;
+ ctx.state.set('attempts', attempts + 1);
+ },
+ { name: 'task_attempt_node' },
+);
+
+const readStateNode = node(
+ (ctx: NodeContext) =>
+ `attempts state: ${ctx.state.get('attempts')} ` +
+ `(topic: ${ctx.state.get('topic')}, ` +
+ `started: ${ctx.state.get('temp:started_at')})`,
+ { name: 'read_state_node' },
+);
+
+export const rootAgent = new Workflow({
+ name: 'session_state_workflow',
+ edges: [['START', initStateNode, taskAttemptNode, readStateNode]],
+});
+// --8<-- [end:session-state]
diff --git a/examples/typescript/snippets/graphs/data-handling/structured_access.ts b/examples/typescript/snippets/graphs/data-handling/structured_access.ts
new file mode 100644
index 0000000000..6e9c22b1dc
--- /dev/null
+++ b/examples/typescript/snippets/graphs/data-handling/structured_access.ts
@@ -0,0 +1,69 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Two data-selection forms are available inside an agent instruction:
+//
+// {Class.field} reads a field off THIS node's input
+// reads a field off a named predecessor's
+// output — more restrictive, and unambiguous
+// when several upstream nodes share a field
+//
+// Both are distinct from `{state_key}`, which reads session state.
+
+// --8<-- [start:structured-access]
+import { LlmAgent, node, NodeContext, Workflow } from '@google/adk';
+import { z } from 'zod';
+
+const cityTimeSchema = z.object({
+ timeInfo: z.string().describe('Time information.'),
+ city: z.string().describe('City name.'),
+});
+type CityTime = z.infer;
+
+const cityGeneratorAgent = new LlmAgent({
+ name: 'city_generator_agent',
+ model: 'gemini-flash-latest',
+ instruction: 'Return the name of a random city. Return only the name.',
+});
+
+/** Simulates returning the current time in the specified city. */
+const lookupTimeFunction = node(
+ (_ctx: NodeContext, city: string): CityTime => ({
+ timeInfo: '10:10 AM',
+ city: city.trim(),
+ }),
+ { name: 'lookup_time_function', outputSchema: cityTimeSchema },
+);
+
+const cityReportAgent = new LlmAgent({
+ name: 'city_report_agent',
+ model: 'gemini-flash-latest',
+ instruction:
+ 'Return a sentence in the following format: It is ' +
+ ' in ' +
+ ' right now.',
+});
+
+export const rootAgent = new Workflow({
+ name: 'root_agent',
+ edges: [
+ [
+ 'START',
+ cityGeneratorAgent,
+ lookupTimeFunction,
+ node(cityReportAgent, { inputSchema: cityTimeSchema }),
+ ],
+ ],
+});
+// --8<-- [end:structured-access]
diff --git a/examples/typescript/snippets/graphs/data-handling/structured_output.ts b/examples/typescript/snippets/graphs/data-handling/structured_output.ts
new file mode 100644
index 0000000000..8df086d131
--- /dev/null
+++ b/examples/typescript/snippets/graphs/data-handling/structured_output.ts
@@ -0,0 +1,47 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// `output` is not limited to text — any serializable value flows to the next
+// node, which receives it as a typed object.
+
+// --8<-- [start:structured-output]
+import { createEvent, node, NodeContext, Workflow } from '@google/adk';
+import { z } from 'zod';
+
+const cityInfoSchema = z.object({
+ cityName: z.string(),
+ cityTime: z.string(),
+});
+type CityInfo = z.infer;
+
+const emitStructuredOutput = node(
+ async function* () {
+ yield createEvent({
+ output: { cityName: 'Paris', cityTime: '10:10 AM' } satisfies CityInfo,
+ });
+ },
+ { name: 'emit_structured_output', outputSchema: cityInfoSchema },
+);
+
+const consumeStructuredOutput = node(
+ (_ctx: NodeContext, cityInfo: CityInfo) =>
+ `It is ${cityInfo.cityTime} in ${cityInfo.cityName} right now.`,
+ { name: 'consume_structured_output', inputSchema: cityInfoSchema },
+);
+
+export const rootAgent = new Workflow({
+ name: 'structured_output_workflow',
+ edges: [['START', emitStructuredOutput, consumeStructuredOutput]],
+});
+// --8<-- [end:structured-output]
diff --git a/examples/typescript/snippets/graphs/data-handling/user_message.ts b/examples/typescript/snippets/graphs/data-handling/user_message.ts
new file mode 100644
index 0000000000..a31b4128cc
--- /dev/null
+++ b/examples/typescript/snippets/graphs/data-handling/user_message.ts
@@ -0,0 +1,51 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// A message for the human is the event's `content`: the runtime renders it, and
+// the graph does NOT forward it as node input. `content` is for the user,
+// `output` is for the next node.
+
+// --8<-- [start:user-message]
+import { createEvent, node, NodeContext, Workflow } from '@google/adk';
+
+/** Emits a user-facing message: `content`, with no `output`. */
+const message = (text: string) =>
+ createEvent({ content: { role: 'model', parts: [{ text }] } });
+
+const userMessage = node(
+ async function* (_ctx: NodeContext, nodeInput: string) {
+ yield message(`Beginning research process for "${nodeInput}"...`);
+ },
+ { name: 'user_message' },
+);
+
+const research = node(
+ async function* (_ctx: NodeContext) {
+ yield message('Gathering sources...');
+ yield createEvent({ output: ['source-a', 'source-b', 'source-c'] });
+ },
+ { name: 'research' },
+);
+
+const report = node(
+ (_ctx: NodeContext, sources: string[]) =>
+ `Research complete. ${sources.length} sources: ${sources.join(', ')}.`,
+ { name: 'report' },
+);
+
+export const rootAgent = new Workflow({
+ name: 'user_message_workflow',
+ edges: [['START', userMessage, research, report]],
+});
+// --8<-- [end:user-message]
diff --git a/examples/typescript/snippets/graphs/dynamic/custom_run_ids.ts b/examples/typescript/snippets/graphs/dynamic/custom_run_ids.ts
new file mode 100644
index 0000000000..c3422ec739
--- /dev/null
+++ b/examples/typescript/snippets/graphs/dynamic/custom_run_ids.ts
@@ -0,0 +1,64 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// ADK gives every child execution a deterministic id derived from the parent id
+// and a per-node-name counter ("1", "2", "3", ...). Those ids are how a resumed
+// or retried workflow recognises work that already completed and skips it.
+//
+// Avoid custom run ids. The one legitimate case is a REORDERABLE collection,
+// where position is not stable but identity is — key the run id off the item's
+// own id, as below.
+
+// --8<-- [start:custom-execution-ids]
+import { node, NodeContext, Workflow } from '@google/adk';
+
+interface Order {
+ orderId: string;
+ cartItems: string[];
+}
+
+/** Stands in for loading orders from a database. */
+async function getOrders(): Promise {
+ return [
+ { orderId: 'a91', cartItems: ['keyboard', 'mouse'] },
+ { orderId: 'b02', cartItems: ['monitor'] },
+ { orderId: 'c73', cartItems: ['dock', 'cable', 'hub'] },
+ ];
+}
+
+const processOrder = node(
+ (_ctx: NodeContext, order: Order) =>
+ `order ${order.orderId}: ${order.cartItems.length} item(s) shipped`,
+ { name: 'process_order' },
+);
+
+const processAllOrders = node(
+ async (ctx: NodeContext) => {
+ const orders = await getOrders();
+
+ const processTasks = orders.map((order) =>
+ ctx.runNode(processOrder, order, { runId: `order-${order.orderId}` }),
+ );
+
+ const results = await Promise.all(processTasks);
+ return results.map((result) => result.output).join('\n');
+ },
+ { name: 'process_all_orders', rerunOnResume: true },
+);
+
+export const rootAgent = new Workflow({
+ name: 'root_agent',
+ edges: [['START', processAllOrders]],
+});
+// --8<-- [end:custom-execution-ids]
diff --git a/examples/typescript/snippets/graphs/dynamic/data_handling.ts b/examples/typescript/snippets/graphs/dynamic/data_handling.ts
new file mode 100644
index 0000000000..bb320fac53
--- /dev/null
+++ b/examples/typescript/snippets/graphs/dynamic/data_handling.ts
@@ -0,0 +1,57 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Passing data in a dynamic workflow is simpler than in a graph: `ctx.runNode()`
+// returns the child's result directly, so there are no session-state keys to
+// read and write to move a value one step downstream.
+
+// --8<-- [start:data-handling]
+import { LlmAgent, node, NodeContext, Workflow } from '@google/adk';
+
+const draftAgent = new LlmAgent({
+ name: 'draft_agent',
+ model: 'gemini-flash-latest',
+ instruction: 'Write a short draft for the user request.',
+});
+
+const formatFunctionNode = node(
+ (_ctx: NodeContext, rawDraft: string) =>
+ rawDraft
+ .split('\n')
+ .map((line) => line.trim())
+ .filter(Boolean)
+ .map((line) => `| ${line}`)
+ .join('\n'),
+ { name: 'format_function_node' },
+);
+
+const editorialWorkflow = node(
+ async (ctx: NodeContext, userRequest: string) => {
+ const rawDraft = await ctx.runNode(draftAgent, userRequest);
+
+ const formattedText = await ctx.runNode(
+ formatFunctionNode,
+ rawDraft.output,
+ );
+
+ return formattedText.output;
+ },
+ { name: 'editorial_workflow', rerunOnResume: true },
+);
+
+export const rootAgent = new Workflow({
+ name: 'root_agent',
+ edges: [['START', editorialWorkflow]],
+});
+// --8<-- [end:data-handling]
diff --git a/examples/typescript/snippets/graphs/dynamic/get_started.ts b/examples/typescript/snippets/graphs/dynamic/get_started.ts
new file mode 100644
index 0000000000..ab87638ac3
--- /dev/null
+++ b/examples/typescript/snippets/graphs/dynamic/get_started.ts
@@ -0,0 +1,36 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// A dynamic workflow drops the static edge graph and orchestrates in plain
+// code: an outer node calls `ctx.runNode(child, input)` to execute children in
+// whatever order your loops and conditionals dictate.
+
+// --8<-- [start:get-started]
+import { node, NodeContext, Workflow } from '@google/adk';
+
+const myNode = node(() => 'Hello World', { name: 'hello_node' });
+
+const myWorkflow = node(
+ async (ctx: NodeContext, _nodeInput: string) => {
+ const result = await ctx.runNode(myNode, 'hello');
+ return result.output;
+ },
+ { name: 'my_workflow', rerunOnResume: true },
+);
+
+export const rootAgent = new Workflow({
+ name: 'root_agent',
+ edges: [['START', myWorkflow]],
+});
+// --8<-- [end:get-started]
diff --git a/examples/typescript/snippets/graphs/dynamic/human_input.ts b/examples/typescript/snippets/graphs/dynamic/human_input.ts
new file mode 100644
index 0000000000..fe589ce049
--- /dev/null
+++ b/examples/typescript/snippets/graphs/dynamic/human_input.ts
@@ -0,0 +1,60 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// A parent node that calls `ctx.runNode` must set `rerunOnResume: true`, or it
+// cannot handle an interrupt raised by a child. The leaf keeps
+// `rerunOnResume: false`: on resume it does not re-run its body, it completes
+// with the human's reply as its output.
+
+// --8<-- [start:human-input]
+import { node, NodeContext, RequestInput, Workflow } from '@google/adk';
+
+/**
+ * Pauses the workflow and waits for user input.
+ *
+ * `rerunOnResume: false` (the default, spelled out here because it is the
+ * point) is what makes this a one-liner: the reply is handed to the node as
+ * its output instead of the body running a second time to collect it.
+ */
+const getUserApproval = node(
+ () => new RequestInput({ message: 'Please approve this request (Yes/No)' }),
+ { name: 'get_user_approval', rerunOnResume: false },
+);
+
+/** The orchestrator calling the interactive step. */
+const handleProcess = node(
+ async (ctx: NodeContext, nodeInput: unknown) => {
+ const approval = await ctx.runNode(getUserApproval, nodeInput);
+
+ if (approval.interruptIds.length > 0) {
+ return undefined;
+ }
+
+ const userResponse = String(approval.output ?? '')
+ .trim()
+ .toLowerCase();
+
+ if (userResponse === 'yes') {
+ return 'Approved';
+ }
+ return 'Denied';
+ },
+ { name: 'handle_process', rerunOnResume: true },
+);
+
+export const rootAgent = new Workflow({
+ name: 'root_agent',
+ edges: [['START', handleProcess]],
+});
+// --8<-- [end:human-input]
diff --git a/examples/typescript/snippets/graphs/dynamic/loop_route.ts b/examples/typescript/snippets/graphs/dynamic/loop_route.ts
new file mode 100644
index 0000000000..588aef14d7
--- /dev/null
+++ b/examples/typescript/snippets/graphs/dynamic/loop_route.ts
@@ -0,0 +1,83 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// A dynamic workflow defines the iteration as an ordinary loop rather than a
+// back-edge in a graph. Values are held in local variables, and state is
+// written only where an agent's instruction template needs to read it back
+// (`{code}`, `{findings}`).
+
+// --8<-- [start:loop-route]
+import { LlmAgent, node, NodeContext, Workflow } from '@google/adk';
+
+/** Safety bound on the refine loop. */
+const MAX_FIX_ROUNDS = 3;
+
+const coderAgent = new LlmAgent({
+ name: 'generator_agent',
+ model: 'gemini-flash-latest',
+ instruction: 'Write TypeScript code for the user request. Output code only.',
+});
+
+/** Simulates a compile / lint pass. Empty findings means "clean". */
+const compileLintCheck = node(
+ (_ctx: NodeContext, code: string) => {
+ const findings: string[] = [];
+ if (!/\/\*\*/.test(code)) {
+ findings.push('every function needs a JSDoc comment');
+ }
+ if (!/\)\s*:\s*\w/.test(code)) {
+ findings.push('add return type annotations');
+ }
+ return { findings: findings.join('; ') };
+ },
+ { name: 'lint_reviewer' },
+);
+
+const fixerAgent = new LlmAgent({
+ name: 'fixer_agent',
+ model: 'gemini-flash-latest',
+ instruction: `Refactor current code {code}.
+ Based on compile & lint review: {findings}
+ Output code only.`,
+});
+
+const codeWorkflow = node(
+ async (ctx: NodeContext, userRequest: string) => {
+ let code = (await ctx.runNode(coderAgent, userRequest)).output as string;
+ let checkResp = (await ctx.runNode(compileLintCheck, code)).output as {
+ findings: string;
+ };
+
+ for (let round = 0; checkResp.findings && round < MAX_FIX_ROUNDS; round++) {
+ ctx.state.set('code', code);
+ ctx.state.set('findings', checkResp.findings);
+
+ code = (
+ await ctx.runNode(fixerAgent, { code, findings: checkResp.findings })
+ ).output as string;
+ checkResp = (await ctx.runNode(compileLintCheck, code)).output as {
+ findings: string;
+ };
+ }
+
+ return code;
+ },
+ { name: 'code_workflow', rerunOnResume: true },
+);
+
+export const rootAgent = new Workflow({
+ name: 'root_agent',
+ edges: [['START', codeWorkflow]],
+});
+// --8<-- [end:loop-route]
diff --git a/examples/typescript/snippets/graphs/dynamic/nodes.ts b/examples/typescript/snippets/graphs/dynamic/nodes.ts
new file mode 100644
index 0000000000..d6090e929c
--- /dev/null
+++ b/examples/typescript/snippets/graphs/dynamic/nodes.ts
@@ -0,0 +1,52 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// The two ways to build a node, and the orchestrator that composes them.
+
+// --8<-- [start:node-forms]
+import { FunctionNode, node, NodeContext, Workflow } from '@google/adk';
+
+/** The plain function both node forms wrap. */
+function myFunctionNode(_ctx: NodeContext, nodeInput: unknown): string {
+ return `Hello ${nodeInput ?? 'World'}`;
+}
+
+const helloNode = node(myFunctionNode, { name: 'hello_node' });
+
+const successNode = new FunctionNode('hello', myFunctionNode, {
+ rerunOnResume: true,
+});
+// --8<-- [end:node-forms]
+
+// --8<-- [start:workflows]
+const myFormattingNode = node(
+ (_ctx: NodeContext, nodeInput: string) => `>> ${nodeInput.trim()} <<`,
+ { name: 'my_formatting_node' },
+);
+
+const myWorkflow = node(
+ async (ctx: NodeContext, nodeInput: unknown) => {
+ const greeted = await ctx.runNode(helloNode, nodeInput);
+ const again = await ctx.runNode(successNode, greeted.output);
+ const formatted = await ctx.runNode(myFormattingNode, again.output);
+ return formatted.output;
+ },
+ { name: 'my_workflow', rerunOnResume: true },
+);
+
+export const rootAgent = new Workflow({
+ name: 'root_agent',
+ edges: [['START', myWorkflow]],
+});
+// --8<-- [end:workflows]
diff --git a/examples/typescript/snippets/graphs/dynamic/parallel_route.ts b/examples/typescript/snippets/graphs/dynamic/parallel_route.ts
new file mode 100644
index 0000000000..86bdff371a
--- /dev/null
+++ b/examples/typescript/snippets/graphs/dynamic/parallel_route.ts
@@ -0,0 +1,63 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// `ctx.runNode()` returns a promise, so starting every child before awaiting any
+// of them runs them concurrently, and `Promise.all` gathers the results.
+//
+// Prefer the built-in when the shape is "map one node over a list":
+// node(worker, {parallelWorker: true, maxParallelWorkers: 4})
+// It does the fan-out for you and bounds concurrency (default 8). Hand-rolling
+// it, as below, is for when you need custom scheduling or partial-failure
+// handling.
+
+// --8<-- [start:parallel-route]
+import { node, NodeContext, Workflow } from '@google/adk';
+
+const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
+
+/** The worker run once per list item. */
+const realNode = node(
+ async (_ctx: NodeContext, item: string) => {
+ await sleep(200);
+ return { item, length: item.length };
+ },
+ { name: 'analyze_item' },
+);
+
+const parallelSupervisor = node(
+ async (ctx: NodeContext, nodeInput: string) => {
+ const items = nodeInput
+ .split(',')
+ .map((item) => item.trim())
+ .filter(Boolean);
+
+ const tasks = items.map((item) => ctx.runNode(realNode, item));
+ const results = await Promise.all(tasks);
+
+ return results.map((result) => result.output);
+ },
+ { name: 'parallel_supervisor', rerunOnResume: true },
+);
+
+const summarize = node(
+ (_ctx: NodeContext, results: Array<{ item: string; length: number }>) =>
+ results.map((r) => `${r.item}: ${r.length} chars`).join('\n'),
+ { name: 'summarize' },
+);
+
+export const rootAgent = new Workflow({
+ name: 'root_agent',
+ edges: [['START', parallelSupervisor, summarize]],
+});
+// --8<-- [end:parallel-route]
diff --git a/examples/typescript/snippets/graphs/dynamic/sequence_route.ts b/examples/typescript/snippets/graphs/dynamic/sequence_route.ts
new file mode 100644
index 0000000000..64fe505ed8
--- /dev/null
+++ b/examples/typescript/snippets/graphs/dynamic/sequence_route.ts
@@ -0,0 +1,68 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// A sequential route in a dynamic workflow is just awaiting `ctx.runNode()`
+// calls one after another — each finishes before the next starts. Schemas work
+// the same as in a graph: attach them to the nodes you run.
+
+// --8<-- [start:sequence-route]
+import { LlmAgent, node, NodeContext, Workflow } from '@google/adk';
+import { z } from 'zod';
+
+const cityTimeSchema = z.object({
+ timeInfo: z.string().describe('Time information.'),
+ city: z.string().describe('City name.'),
+});
+type CityTime = z.infer;
+
+const cityGeneratorAgent = new LlmAgent({
+ name: 'city_generator_agent',
+ model: 'gemini-flash-latest',
+ instruction: 'Return the name of a random city. Return only the name.',
+});
+
+/** Simulates returning the current time in a specified city. */
+const cityTimeFunction = node(
+ (_ctx: NodeContext, city: string): CityTime => ({
+ timeInfo: '10:10 AM',
+ city: city.trim(),
+ }),
+ { name: 'city_time_function', outputSchema: cityTimeSchema },
+);
+
+const cityReportAgent = node(
+ new LlmAgent({
+ name: 'city_report_agent',
+ model: 'gemini-flash-latest',
+ instruction: 'Output the data provided by the previous node as a sentence.',
+ }),
+ { inputSchema: cityTimeSchema },
+);
+
+const cityWorkflow = node(
+ async (ctx: NodeContext) => {
+ const city = await ctx.runNode(cityGeneratorAgent);
+ const cityTime = await ctx.runNode(cityTimeFunction, city.output);
+ const reportText = await ctx.runNode(cityReportAgent, cityTime.output);
+
+ return reportText.output;
+ },
+ { name: 'city_workflow', rerunOnResume: true },
+);
+
+export const rootAgent = new Workflow({
+ name: 'root_agent',
+ edges: [['START', cityWorkflow]],
+});
+// --8<-- [end:sequence-route]
diff --git a/examples/typescript/snippets/graphs/human-input/get_started.ts b/examples/typescript/snippets/graphs/human-input/get_started.ts
new file mode 100644
index 0000000000..f6d9ef54a2
--- /dev/null
+++ b/examples/typescript/snippets/graphs/human-input/get_started.ts
@@ -0,0 +1,45 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// `step1` pauses the workflow until the user replies; the reply is then handed
+// to the next node as its input. This is the default `rerunOnResume: false`
+// handoff: the interrupted node does NOT re-run — it completes with the user's
+// reply as its output. A HITL node needs no model, which makes the pause fully
+// deterministic.
+
+// --8<-- [start:get-started]
+import { node, NodeContext, RequestInput, Workflow } from '@google/adk';
+
+const step1 = node(
+ async function* () {
+ yield new RequestInput({ message: 'Enter a number:' });
+ },
+ { name: 'step1' },
+);
+
+const step2 = node(
+ (_ctx: NodeContext, nodeInput: string | number) => {
+ const value = Number(nodeInput);
+ return Number.isFinite(value)
+ ? value * 2
+ : `"${nodeInput}" is not a number.`;
+ },
+ { name: 'step2' },
+);
+
+export const rootAgent = new Workflow({
+ name: 'root_agent',
+ edges: [['START', step1, step2]],
+});
+// --8<-- [end:get-started]
diff --git a/examples/typescript/snippets/graphs/human-input/initial_prompt.ts b/examples/typescript/snippets/graphs/human-input/initial_prompt.ts
new file mode 100644
index 0000000000..c0d4ac7c5f
--- /dev/null
+++ b/examples/typescript/snippets/graphs/human-input/initial_prompt.ts
@@ -0,0 +1,66 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// A human-input node as the FIRST step of a workflow: instead of guessing what
+// the user wants, the graph opens by asking, pauses, and then routes the reply
+// into the rest of the process.
+//
+// `responseSchema: z.string()` asks for a plain text reply. Nothing coerces the
+// human's answer into that shape; the schema tells a client what to collect.
+
+// --8<-- [start:initial-prompt]
+import { node, NodeContext, RequestInput, Workflow } from '@google/adk';
+import { z } from 'zod';
+
+/** Asks the user for itinerary information. */
+const initialPrompt = node(
+ async function* () {
+ const inputMessage = `
+ 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 new RequestInput({
+ message: inputMessage,
+ responseSchema: z.string(),
+ });
+ },
+ { name: 'initial_prompt' },
+);
+
+const buildItinerary = node(
+ (_ctx: NodeContext, nodeInput: string) => {
+ const [city = 'your city'] = nodeInput.split(',');
+ return (
+ `Personalized itinerary for ${city.trim()}:\n` +
+ ' 1. Morning walk through the old town\n' +
+ ' 2. Lunch at a neighbourhood favourite\n' +
+ ' 3. An afternoon activity matched to your hobby\n\n' +
+ `(based on: ${nodeInput.trim()})`
+ );
+ },
+ { name: 'build_itinerary' },
+);
+
+export const rootAgent = new Workflow({
+ name: 'concierge_workflow',
+ edges: [['START', initialPrompt, buildItinerary]],
+});
+// --8<-- [end:initial-prompt]
diff --git a/examples/typescript/snippets/graphs/human-input/payload_and_schema.ts b/examples/typescript/snippets/graphs/human-input/payload_and_schema.ts
new file mode 100644
index 0000000000..42003d7105
--- /dev/null
+++ b/examples/typescript/snippets/graphs/human-input/payload_and_schema.ts
@@ -0,0 +1,95 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// `RequestInput` takes three configuration options:
+// message text shown to the user explaining what is being asked
+// payload structured data sent alongside the prompt, so a client can
+// render richer context (here, the full itinerary)
+// responseSchema the shape the reply is expected to take
+//
+// `RequestInput` does NOT reformat a human reply to fit `responseSchema` — the
+// reply must already be in that shape.
+
+// --8<-- [start:payload-and-schema]
+import { node, NodeContext, RequestInput, Workflow } from '@google/adk';
+import { z } from 'zod';
+
+/**
+ * Itinerary is a list of activities. Each activity has a name and a
+ * description.
+ */
+const activitiesListSchema = z.object({
+ itinerary: z.array(z.object({ name: z.string(), description: z.string() })),
+});
+type ActivitiesList = z.infer;
+
+/** Expected response structure from the user. */
+const userFeedbackSchema = z.object({
+ userResponse: z.string(),
+});
+
+const buildItinerary = node(
+ (_ctx: NodeContext, city: string): ActivitiesList => {
+ const place = city.trim() || 'your city';
+ return {
+ itinerary: [
+ { name: 'Morning walk', description: `A stroll through old ${place}.` },
+ { name: 'Local lunch', description: `Regional food in ${place}.` },
+ { name: 'Museum visit', description: `The main museum of ${place}.` },
+ ],
+ };
+ },
+ { name: 'build_itinerary', outputSchema: activitiesListSchema },
+);
+
+/**
+ * Retrieves the user's thoughts on the agent's initial itinerary in order to
+ * either expand on it, change the list, or exit the loop.
+ */
+const getUserFeedback = node(
+ async function* (_ctx: NodeContext, nodeInput: ActivitiesList) {
+ const rendered = nodeInput.itinerary
+ .map((a, i) => ` ${i + 1}. ${a.name} — ${a.description}`)
+ .join('\n');
+
+ yield new RequestInput({
+ message:
+ `Here is your recommended base itinerary:\n${rendered}\n\n` +
+ 'Which of these items appeal to you (if any)?',
+ payload: nodeInput,
+ responseSchema: userFeedbackSchema,
+ });
+ },
+ { name: 'get_user_feedback' },
+);
+
+const applyFeedback = node(
+ (_ctx: NodeContext, nodeInput: unknown) => {
+ const feedback =
+ typeof nodeInput === 'string'
+ ? nodeInput
+ : String(
+ (nodeInput as { userResponse?: unknown } | null)?.userResponse ??
+ JSON.stringify(nodeInput),
+ );
+ return `Noted. Building the final itinerary around: ${feedback}`;
+ },
+ { name: 'apply_feedback' },
+);
+
+export const rootAgent = new Workflow({
+ name: 'concierge_workflow',
+ edges: [['START', buildItinerary, getUserFeedback, applyFeedback]],
+});
+// --8<-- [end:payload-and-schema]
diff --git a/examples/typescript/snippets/graphs/index/get_started.ts b/examples/typescript/snippets/graphs/index/get_started.ts
new file mode 100644
index 0000000000..8ef8672fe7
--- /dev/null
+++ b/examples/typescript/snippets/graphs/index/get_started.ts
@@ -0,0 +1,79 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// A sequential graph workflow that alternates between model reasoning and plain
+// code: an agent names a random city, a code function looks up the time there,
+// a second agent reports it, and a final function appends a completion message.
+
+// --8<-- [start:get-started]
+import {
+ createEvent,
+ LlmAgent,
+ node,
+ NodeContext,
+ Workflow,
+} from '@google/adk';
+import { z } from 'zod';
+
+const cityGeneratorAgent = new LlmAgent({
+ name: 'city_generator_agent',
+ model: 'gemini-flash-latest',
+ instruction: `Return the name of a random city.
+ Return only the name, nothing else.`,
+});
+
+/** The structured payload handed from the lookup node to the report agent. */
+const cityTimeSchema = z.object({
+ timeInfo: z.string().describe('Time information.'),
+ city: z.string().describe('City name.'),
+});
+type CityTime = z.infer;
+
+/** Simulates returning the current time in the specified city. */
+function lookupTimeFunction(_ctx: NodeContext, nodeInput: string): CityTime {
+ return { timeInfo: '10:10 AM', city: nodeInput.trim() };
+}
+
+const cityReportAgent = new LlmAgent({
+ name: 'city_report_agent',
+ model: 'gemini-flash-latest',
+ instruction: `Output the following line:
+ It is {CityTime.timeInfo} in {CityTime.city} right now.`,
+});
+
+function completedMessageFunction(_ctx: NodeContext, nodeInput: string) {
+ return createEvent({
+ content: {
+ role: 'model',
+ parts: [{ text: `${nodeInput}\n WORKFLOW COMPLETED.` }],
+ },
+ });
+}
+
+export const rootAgent = new Workflow({
+ name: 'root_agent',
+ edges: [
+ [
+ 'START',
+ cityGeneratorAgent,
+ node(lookupTimeFunction, {
+ name: 'lookup_time_function',
+ outputSchema: cityTimeSchema,
+ }),
+ node(cityReportAgent, { inputSchema: cityTimeSchema }),
+ node(completedMessageFunction, { name: 'completed_message_function' }),
+ ],
+ ],
+});
+// --8<-- [end:get-started]
diff --git a/examples/typescript/snippets/graphs/index/process_pipeline.ts b/examples/typescript/snippets/graphs/index/process_pipeline.ts
new file mode 100644
index 0000000000..433ddd67b7
--- /dev/null
+++ b/examples/typescript/snippets/graphs/index/process_pipeline.ts
@@ -0,0 +1,87 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// A prompt-based agent turned into a graph: one agent classifies the message,
+// a router node emits the categories as routes, and the graph dispatches to the
+// matching handler(s). Because the classifier may return more than one
+// category, the router emits an ARRAY of routes — every matching branch fires.
+
+// --8<-- [start:process-pipeline]
+import {
+ createEvent,
+ DEFAULT_ROUTE,
+ LlmAgent,
+ node,
+ NodeContext,
+ Workflow,
+} from '@google/adk';
+
+/** The routes this graph has edges for. */
+const ROUTES = ['BUG', 'CUSTOMER_SUPPORT', 'LOGISTICS'] as const;
+
+const processMessage = new LlmAgent({
+ 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.
+ Reply with the categories only, nothing else.`,
+});
+
+const router = node(
+ (_ctx: NodeContext, nodeInput: string) => {
+ const text = String(nodeInput).toUpperCase();
+ const matched = ROUTES.filter((route) =>
+ new RegExp(`\\b${route}\\b`).test(text),
+ );
+ return createEvent({ route: matched.length > 0 ? matched : DEFAULT_ROUTE });
+ },
+ { name: 'router' },
+);
+
+/** Emits a user-facing message: `content`, with no `output`. */
+const message = (text: string) =>
+ createEvent({ content: { role: 'model', parts: [{ text }] } });
+
+const response1Bug = node(() => message('Handling bug...'), {
+ name: 'response_1_bug',
+});
+const response2Support = node(() => message('Handling customer support...'), {
+ name: 'response_2_support',
+});
+const response3Logistics = node(() => message('Handling logistics...'), {
+ name: 'response_3_logistics',
+});
+const responseUnknown = node(
+ (_ctx: NodeContext, nodeInput: string) =>
+ message(`Could not classify that (classifier said: ${nodeInput}).`),
+ { name: 'response_unknown' },
+);
+
+export const rootAgent = new Workflow({
+ name: 'routing_workflow',
+ edges: [
+ ['START', processMessage, router],
+ [
+ router,
+ {
+ BUG: response1Bug,
+ CUSTOMER_SUPPORT: response2Support,
+ LOGISTICS: response3Logistics,
+ [DEFAULT_ROUTE]: responseUnknown,
+ },
+ ],
+ ],
+});
+// --8<-- [end:process-pipeline]
diff --git a/examples/typescript/snippets/graphs/package.json b/examples/typescript/snippets/graphs/package.json
new file mode 100644
index 0000000000..dcfa9be933
--- /dev/null
+++ b/examples/typescript/snippets/graphs/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "adk-docs-examples-graphs",
+ "version": "1.0.0",
+ "description": "TS graph workflow examples for the ADK Documentation",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "build": "tsc",
+ "clean": "rm -rf dist"
+ },
+ "keywords": [
+ "adk",
+ "google",
+ "agent",
+ "typescript",
+ "gemini",
+ "workflow"
+ ],
+ "author": "",
+ "license": "Apache-2.0",
+ "type": "module",
+ "devDependencies": {
+ "@types/node": "^20.14.2",
+ "typescript": "^5.9.2"
+ },
+ "dependencies": {
+ "@google/adk": "^2.0.0",
+ "zod": "^4.2.1"
+ }
+}
diff --git a/examples/typescript/snippets/graphs/routes/branches.ts b/examples/typescript/snippets/graphs/routes/branches.ts
new file mode 100644
index 0000000000..5233c695c2
--- /dev/null
+++ b/examples/typescript/snippets/graphs/routes/branches.ts
@@ -0,0 +1,66 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Branching is a node that emits a `route`, plus an edge row mapping each route
+// value to the node that handles it. A branch target can be anything node-like:
+// `taskBNode` here is an `LlmAgent`, `taskCNode` a plain function.
+
+// --8<-- [start:branches]
+import {
+ createEvent,
+ LlmAgent,
+ node,
+ NodeContext,
+ Workflow,
+} from '@google/adk';
+
+const taskANode = node(
+ (_ctx: NodeContext, nodeInput: string) => nodeInput.trim(),
+ { name: 'task_A_node' },
+);
+
+/** Stands in for an application-specific branch condition. */
+const condition = (nodeInput: string) => /\d/.test(nodeInput);
+
+/** Routes to task B or C based on nodeInput. */
+const router = node(
+ (_ctx: NodeContext, nodeInput: string) =>
+ condition(nodeInput)
+ ? createEvent({ route: 'RUN_TASK_C', output: nodeInput })
+ : createEvent({ route: 'RUN_TASK_B', output: nodeInput }),
+ { name: 'router' },
+);
+
+const taskBNode = new LlmAgent({
+ name: 'task_B_agent',
+ model: 'gemini-flash-latest',
+ instruction: 'Answer the user in a single short sentence.',
+});
+
+const taskCNode = node(() => 'Task C completed', { name: 'task_C_node' });
+
+export const rootAgent = new Workflow({
+ name: 'routing_workflow',
+ edges: [
+ ['START', taskANode, router],
+ [
+ router,
+ {
+ RUN_TASK_B: taskBNode,
+ RUN_TASK_C: taskCNode,
+ },
+ ],
+ ],
+});
+// --8<-- [end:branches]
diff --git a/examples/typescript/snippets/graphs/routes/fan_out_join.ts b/examples/typescript/snippets/graphs/routes/fan_out_join.ts
new file mode 100644
index 0000000000..d528fe60d0
--- /dev/null
+++ b/examples/typescript/snippets/graphs/routes/fan_out_join.ts
@@ -0,0 +1,56 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// A `JoinNode` is a fan-in barrier: it waits for EVERY predecessor to finish
+// and then hands the next node an object keyed by predecessor node name.
+
+// --8<-- [start:fan-out-join]
+import { JoinNode, node, NodeContext, Workflow } from '@google/adk';
+
+const parallelTaskA = node(
+ (_ctx: NodeContext, text: string) => text.toUpperCase(),
+ { name: 'parallel_task_A' },
+);
+
+const parallelTaskB = node((_ctx: NodeContext, text: string) => text.length, {
+ name: 'parallel_task_B',
+});
+
+const parallelTaskC = node(
+ (_ctx: NodeContext, text: string) => text.split('').reverse().join(''),
+ { name: 'parallel_task_C' },
+);
+
+const myJoinNode = new JoinNode({ name: 'my_join_node' });
+
+const finalTaskD = node(
+ (_ctx: NodeContext, results: Record) =>
+ [
+ `Uppercase: ${results['parallel_task_A']}`,
+ `Length: ${results['parallel_task_B']}`,
+ `Reversed: ${results['parallel_task_C']}`,
+ ].join('\n'),
+ { name: 'final_task_D' },
+);
+
+export const rootAgent = new Workflow({
+ name: 'fan_out_workflow',
+ edges: [
+ ['START', parallelTaskA, myJoinNode],
+ ['START', parallelTaskB, myJoinNode],
+ ['START', parallelTaskC, myJoinNode],
+ [myJoinNode, finalTaskD],
+ ],
+});
+// --8<-- [end:fan-out-join]
diff --git a/examples/typescript/snippets/graphs/routes/function_node.ts b/examples/typescript/snippets/graphs/routes/function_node.ts
new file mode 100644
index 0000000000..489be548f2
--- /dev/null
+++ b/examples/typescript/snippets/graphs/routes/function_node.ts
@@ -0,0 +1,51 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// The simplest node type: a plain function wrapped as a FunctionNode. It takes
+// text in, returns text out, and the framework hands that value to the next
+// node as its input — no session-state writes needed.
+
+// --8<-- [start:function-node]
+import {
+ createEvent,
+ node,
+ NodeContext,
+ Workflow,
+ type FunctionNodeHandler,
+} from '@google/adk';
+
+/** A bare return value: boxed into an event's `output` for you. */
+const myFunctionNode: FunctionNodeHandler = (
+ _ctx: NodeContext,
+ nodeInput: string,
+) => {
+ const inputTextModified = nodeInput.toUpperCase();
+ return inputTextModified;
+};
+
+/** The explicit form — identical behaviour, useful when you also set `route`. */
+const myExplicitEventNode = (_ctx: NodeContext, nodeInput: string) =>
+ createEvent({ output: `${nodeInput} IS AWESOME!` });
+
+export const rootAgent = new Workflow({
+ name: 'function_node_pipeline',
+ edges: [
+ [
+ 'START',
+ node(myFunctionNode, { name: 'my_function_node' }),
+ node(myExplicitEventNode, { name: 'add_suffix' }),
+ ],
+ ],
+});
+// --8<-- [end:function-node]
diff --git a/examples/typescript/snippets/graphs/routes/loop_escalation.ts b/examples/typescript/snippets/graphs/routes/loop_escalation.ts
new file mode 100644
index 0000000000..d8605d54bf
--- /dev/null
+++ b/examples/typescript/snippets/graphs/routes/loop_escalation.ts
@@ -0,0 +1,78 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// A loop is a BACK-EDGE in the graph: a downstream node routes back to an
+// earlier node, and the engine re-activates that node with a fresh lifecycle on
+// each iteration. The loop exits when the router picks the terminal branch.
+//
+// START -> seed_draft -> critic -> router --REVISE--> refine --+
+// ^ |
+// +----------------------------------+
+// router --DONE--> finalize
+
+// --8<-- [start:loop-escalation]
+import { createEvent, node, NodeContext, Workflow } from '@google/adk';
+
+interface Draft {
+ topic: string;
+ bullets: string[];
+}
+
+/** The critic is satisfied once the draft has at least this many bullets. */
+const REQUIRED_BULLETS = 3;
+
+const seedDraft = node(
+ (_ctx: NodeContext, topic: string): Draft => ({
+ topic: topic.trim(),
+ bullets: [`${topic.trim()} — point 1`],
+ }),
+ { name: 'seed_draft' },
+);
+
+const critic = node(
+ (_ctx: NodeContext, draft: Draft) =>
+ createEvent({
+ route: draft.bullets.length >= REQUIRED_BULLETS ? 'DONE' : 'REVISE',
+ output: draft,
+ }),
+ { name: 'critic' },
+);
+
+const refine = node(
+ (_ctx: NodeContext, draft: Draft): Draft => ({
+ ...draft,
+ bullets: [
+ ...draft.bullets,
+ `${draft.topic} — point ${draft.bullets.length + 1}`,
+ ],
+ }),
+ { name: 'refine' },
+);
+
+const finalize = node(
+ (_ctx: NodeContext, draft: Draft) =>
+ `Approved after ${draft.bullets.length} bullets:\n` +
+ draft.bullets.map((b) => ` • ${b}`).join('\n'),
+ { name: 'finalize' },
+);
+
+export const rootAgent = new Workflow({
+ name: 'loop_workflow',
+ edges: [
+ ['START', seedDraft, critic],
+ [critic, { REVISE: refine, DONE: finalize }],
+ [refine, critic],
+ ],
+});
+// --8<-- [end:loop-escalation]
diff --git a/examples/typescript/snippets/graphs/routes/nested_workflow.ts b/examples/typescript/snippets/graphs/routes/nested_workflow.ts
new file mode 100644
index 0000000000..2dcb144b09
--- /dev/null
+++ b/examples/typescript/snippets/graphs/routes/nested_workflow.ts
@@ -0,0 +1,94 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// A `Workflow` is itself a node, so you can use one inside another workflow's
+// edges to encapsulate a reusable sub-process.
+
+// --8<-- [start:nested-workflow]
+import { createEvent, node, NodeContext, Workflow } from '@google/adk';
+
+const taskA1 = node(
+ (_ctx: NodeContext, nodeInput: string) => nodeInput.trim(),
+ {
+ name: 'task_A1',
+ },
+);
+
+const router = node(
+ (_ctx: NodeContext, text: string) =>
+ createEvent({
+ route: text === text.toUpperCase() ? 'RUN_WORKFLOW_C' : 'RUN_WORKFLOW_B',
+ output: text,
+ }),
+ { name: 'router' },
+);
+
+/**
+ * Upper-cases the first letter of each word.
+ *
+ * Unicode-aware on purpose: `\b\w` is ASCII-only, so `ü` never matches — and
+ * the word boundary it creates before the *next* ASCII letter upper-cases that
+ * one instead ("strässe" -> "SträSse"). A letter whose uppercase form is more
+ * than one code point (German `ß` -> "SS") is left alone rather than mangled.
+ */
+const titleCase = (text: string) =>
+ text.replace(/(^|\P{L})(\p{L})/gu, (_match, sep: string, ch: string) => {
+ const upper = ch.toUpperCase();
+ return sep + ([...upper].length === 1 ? upper : ch);
+ });
+
+const workflowB = new Workflow({
+ name: 'workflow_B',
+ edges: [
+ [
+ 'START',
+ node((_ctx: NodeContext, text: string) => titleCase(text), {
+ name: 'b_title_case',
+ }),
+ node((_ctx: NodeContext, text: string) => `[B] ${text}`, {
+ name: 'b_frame',
+ }),
+ ],
+ ],
+});
+
+const workflowC = new Workflow({
+ name: 'workflow_C',
+ edges: [
+ [
+ 'START',
+ node((_ctx: NodeContext, text: string) => text.toLowerCase(), {
+ name: 'c_lower_case',
+ }),
+ node((_ctx: NodeContext, text: string) => `[C] ${text}`, {
+ name: 'c_frame',
+ }),
+ ],
+ ],
+});
+
+export const rootAgent = new Workflow({
+ name: 'parent_workflow',
+ edges: [
+ ['START', taskA1, router],
+ [
+ router,
+ {
+ RUN_WORKFLOW_B: workflowB,
+ RUN_WORKFLOW_C: workflowC,
+ },
+ ],
+ ],
+});
+// --8<-- [end:nested-workflow]
diff --git a/examples/typescript/snippets/graphs/routes/sequence.ts b/examples/typescript/snippets/graphs/routes/sequence.ts
new file mode 100644
index 0000000000..4a785d15e8
--- /dev/null
+++ b/examples/typescript/snippets/graphs/routes/sequence.ts
@@ -0,0 +1,40 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// A sequential route runs each node once, in the listed order. Each node's
+// return value is delivered to the next node as its input.
+
+// --8<-- [start:sequence]
+import { node, NodeContext, Workflow } from '@google/adk';
+
+const taskANode = node(
+ (_ctx: NodeContext, nodeInput: string) => `Summary: ${nodeInput.trim()}`,
+ { name: 'task_A_node' },
+);
+
+const taskBNode = node(
+ (_ctx: NodeContext, summary: string) => summary.toUpperCase(),
+ { name: 'task_B_node' },
+);
+
+const taskCNode = node(
+ (_ctx: NodeContext, shouted: string) => `${shouted} (done)`,
+ { name: 'task_C_node' },
+);
+
+export const rootAgent = new Workflow({
+ name: 'sequential_workflow',
+ edges: [['START', taskANode, taskBNode, taskCNode]],
+});
+// --8<-- [end:sequence]
diff --git a/examples/typescript/snippets/graphs/tsconfig.json b/examples/typescript/snippets/graphs/tsconfig.json
new file mode 100644
index 0000000000..55dd7277b2
--- /dev/null
+++ b/examples/typescript/snippets/graphs/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ /* Build Options */
+ "target": "es2022",
+ "module": "nodenext",
+ "moduleResolution": "nodenext",
+ "outDir": "./dist",
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+
+ /* Strictness */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+
+ /* Module Interop */
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "exclude": ["node_modules", "dist"]
+}