Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/validation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ jobs:
- name: Run type check
run: npm run ts:check

- name: Type check samples
run: npm run ts:check:samples

- name: Run tests and check code coverage
run: npm run test:coverage

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"clean:all": "rm package-lock.json && rm -rf ./node_modules && npm run clean:all --workspaces",
"rebuild": "npm run clean:all && npm install && npm run build",
"ts:check": "tsc --noEmit",
"ts:check:samples": "tsc --noEmit -p samples",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit. I retract my earlier framing here, and I am sorry for the round trip.

I wrote that ts:check:samples duplicates the root ts:check. You already answered that: the repo-wide check fails today with 288 errors in test files, the scoped config was my own fallback suggestion, and your latest note says #648 should absorb this script. Nothing to change.

One point stands. #648 edits the same validation.yaml lines this PR edits (permissions, persist-credentials, the SHA pins), so whichever lands second needs a manual rebase.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No apology needed — and thanks for the #648 heads-up, which was the part that
mattered.

Nothing changed here. On the collision: your 80ee6fc landed on this branch
while I was working, so the rebase kept it and my four commits sit on top of
it. That commit already handles the paths half of the #648 interaction —
samples/tsconfig.json now resets "paths": {} so the samples keep resolving
@google/adk through node_modules once #648 aliases it to core/src at the
root. The validation.yaml half (permissions, persist-credentials, the SHA
pins) still needs a manual rebase for whichever lands second, as you said.

Also rebased onto current main (fcc6c1e) while here, which is what surfaced
#636 and the third stale claim — see the session-state thread.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up here since this is the thread about the #648 collision — it landed
while I was working, so this branch is rebased onto it (82c7b60) and the
conflict is resolved.

Three things fell out of it:

The validation.yaml conflict resolved as both steps rather than one:
ts:check for the repo, ts:check:samples for the samples. Reasoning below.

Your zizmor commit dropped out — permissions: contents: read,
persist-credentials: false and the three SHA pins all came in with #648, so
git dropped that patch as already upstream. Nothing lost.

#648 also created a quieter problem, now fixed. The root config names no
include and excludes only node_modules and **/dist, so the repo-wide
tsc --noEmit picked up all 26 sample files — and resolved their
@google/adk imports through the root paths aliases, against core/src.
That is exactly the resolution 80ee6fc added "paths": {} to prevent, so the
two checks were running over the same files with opposite resolutions, and the
wrong one would have won any disagreement. samples is excluded from the root
config now, leaving one owner.

Verified on both sides rather than assumed: tsc --noEmit --listFiles reports
0 files under samples/ and still passes, and tsc -p samples --listFiles
reports all 26 and resolves @google/adk to core/dist/types/index.d.ts.

So ts:check:samples is not redundant after #648 — it is the only check that
sees these files the way a user's project would. Happy to fold it into
ts:check instead if you would rather have one step, but that means the
samples get checked against workspace sources, which 80ee6fc argues against.

"lint": "eslint \"**/*.ts\"",
"lint:fix": "eslint --fix \"**/*.ts\"",
"format": "prettier \"**/*.ts\" --write",
Expand Down
14 changes: 14 additions & 0 deletions samples/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
// The root config aliases `@google/adk` to `core/src`, so that a test
// type-checks against the sources vitest runs it against. A sample is a
// consumer, not part of the build, so it has to resolve the package the
// way a user's project does: through `node_modules`, against the
// published types. `core`, `dev` and `integrations` reset this for the
// same reason.
Comment on lines +4 to +9

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit. This comment describes #648's tree, not this one.

At 0a0238e the root tsconfig.json declares no paths, and core, dev and integrations carry no reset either. All of that arrives with #648.

So "paths": {} is correct and harmless, and it is the right thing to have here — but it is inert today, and the reason given for it is not true yet. Either land this after #648, or write the comment in the future tense.

Comment on lines +4 to +9

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, withdrawn. Keep "paths": {} and its comment as they are.

#648 merged at 02:13Z, four minutes before I posted. The head I read, 0a0238e, sits on fcc6c1e7 (#636) and predates it, so the root paths and the sibling resets really were absent — but only there. Current main carries both, so your comment is accurate and the reset goes live on the rebase. I should have checked whether #648 had landed before I filed this.

Your only conflict is .github/workflows/validation.yaml:44-45, where main now has #648's Run type check step. The hardening lines merge clean; both sides are identical.

"paths": {}
},
"include": ["**/*.ts"],
"exclude": ["node_modules"]
}
170 changes: 170 additions & 0 deletions samples/workflows/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Graph workflow samples

Runnable TypeScript ports of the **Python** code snippets in the ADK
[Graph Workflows docs](https://adk.dev/graphs/). One directory per snippet,
grouped by the docs page it comes from, so a sample directory maps 1:1 to a
section anchor on adk.dev.

Each directory exports a `rootAgent` that runs with the ADK CLI. The docs
snippets are fragments — they reference helpers they never define (`condition()`,
`task_A_node`, …) — so each port fills those in with the smallest plausible
implementation and says so in its header comment. Everything else follows the
Python source as closely as the TypeScript API allows; where the two genuinely
differ, the file comments say why.

## Running

Build once, then run any sample by its `agent.ts` path:

```bash
npm run build # builds @google/adk (and the CLI); needed once / after changes
npm run sample -- samples/workflows/routes/sequence/agent.ts
```
Comment on lines +19 to +22

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, and the one thing I would fix before merging. Nothing in CI type-checks these files.

npm run build            # builds @google/adk (and the CLI); needed once / after changes

That builds the workspaces, which are core, dev and integrations — samples/ is not one, and no tsconfig names it. validation.yaml runs secretlint, build, tests, lint, format and docs. Lint reaches these files, but eslint.config.js uses tseslint.configs.recommended, which is not type-aware, so a wrong signature, a renamed type or a removed export passes it.

The result is 2129 lines that back public documentation pages and that no job compiles. The workflow API is @experimental, and #635, #636 and #637 are all changing it right now.

The script already exists — "ts:check": "tsc --noEmit" — and the root tsconfig names no include, so it picks these up. Adding one step to validation.yaml covers it:

- name: Type check
  run: npm run ts:check

I have not confirmed ts:check passes repo-wide today, so it may need a scoped tsconfig instead.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in c8ff7b4.

You were right to hedge on ts:check: it does not pass repo-wide today. 288 errors in 44 files, all of them test files (core/test/**, tests/integration/**) — src/ and samples/ are clean. So a bare npm run ts:check step would have gone red on arrival, and fixing those 288 is not this PR's business.

Scoped it instead, following the same extends-the-root pattern core, dev and integrations already use:

// samples/tsconfig.json
{
  "extends": "../tsconfig.json",
  "include": ["**/*.ts"],
  "exclude": ["node_modules"]
}
"ts:check:samples": "tsc --noEmit -p samples"
- name: Type check samples
  run: npm run ts:check:samples

Placed after Build packages, since @google/adk resolves to core/dist/types.

Verified it is not vacuously green: tsc -p samples --listFiles picks up all 26 files, and a deliberately broken annotation in routes/sequence/agent.ts fails the step as expected.

Also added a note to the README under Running, since the build command there was the thing that implied coverage that did not exist:

samples/ is not an npm workspace, so npm run build does not compile it. It has its own samples/tsconfig.json and is type-checked separately, in CI and locally.

The repo-wide ts:check failure seems worth its own issue — happy to file it if it is not already tracked.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebased onto current main (cf6e7ed) — no content change, but it matters for
the red check, which was not this branch.

run-tests (macos-latest) failed with seven Cannot read properties of undefined in tests/integration/workflows/**, none of them files this PR
touches. That was #616 invalidating every recorded fixture in the sample
harness; #647 fixed it on main at 18:07, and this PR's CI ran at 15:11. The
merge commit predated the fix, and re-running the job would have reused the same
merge SHA, so the rebase is what picks it up.

Re-verified on the rebased branch, so the type check is doing real work against
the API as it stands after #635 and #637:

npm run ts:check:samples   -> 0 errors
npx vitest run --project integration tests/integration/workflows
                           -> 37 files, 84 tests, all passing
npm run lint / format:check -> clean

And behaviorally, not just at the type level: all 26 samples construct, and the
18 offline ones still run end-to-end through a real InMemoryRunner with the
same event counts as before the rebase — worth checking specifically, since #637
changed how a finished run rehydrates and #635 changed dynamic HITL resume, and
the HITL samples (human_input/*, dynamic/human_input) lean on both. They
still pause on turn 1 as intended.

One thing to flag for whoever reviews the CI step: ts:check:samples is scoped
because the repo-wide ts:check does not pass today. I have a separate branch
that fixes those 288 errors and wires the repo-wide check in; if that lands,
ts:check:samples becomes redundant and should be folded into it rather than
kept alongside.


`npm run sample -- <path>` is shorthand for
`node dev/dist/esm/cli_entrypoint.js run <path>`.

`samples/` is not an npm workspace, so `npm run build` does not compile it. It
has its own `samples/tsconfig.json` and is type-checked separately, in CI and
locally:

```bash
npm run ts:check:samples
```

CI also executes them, in `tests/integration/docs_samples/`: every sample is
constructed (a `WorkflowAgent` validates its graph in its constructor), and the
offline ones are run end-to-end with the model stubbed out, so a stray model
call in one of them fails too. A new sample directory has to be added to that
test's offline or model-backed list, or it fails for being uncovered.

```bash
npx vitest run --project integration tests/integration/docs_samples
```

The CLI is interactive: type a message and press Enter to send it to the
workflow; type `exit` to quit. Node events print as `[<node_name>]: <output>`
and the last line is the workflow's output. A node that emits only `output` (no
display content) prints nothing — that is expected.

Pipe a single message, or script a multi-turn run with `--replay` (a JSON file
of queries, resolved relative to the working directory):

```bash
echo "hello world" | npm run sample -- samples/workflows/routes/sequence/agent.ts

echo '{"state":{},"queries":["start","21"]}' > replay.json
npm run sample -- samples/workflows/human_input/get_started/agent.ts --replay replay.json
```

## API keys

Samples marked **key** below call a live model. Set `GEMINI_API_KEY` (a `.env`
file in the working directory is loaded automatically) before running them. The
rest are function-only and run offline.

## Human input

The HITL samples **pause** mid-run (you will see an `adk_request_input`
request). Just type your reply on the next turn — a plain-text reply is routed
to the pending interrupt, so you can approve, reject, or supply a value
interactively.

## Samples

### [`/graphs/`](https://adk.dev/graphs/) — `graphs/`

| Sample | Docs section | Shows | Key |
| ------------------ | ---------------------------------------------------------------------------------- | ---------------------------------------------------------- | --- |
| `get_started` | [Get started](https://adk.dev/graphs/#get-started) | Agent → function → agent → function, in sequence | ✅ |
| `process_pipeline` | [Build processes with graphs](https://adk.dev/graphs/#build-processes-with-graphs) | Classify, then dispatch on a route **array** (multi-route) | ✅ |

### [`/graphs/routes/`](https://adk.dev/graphs/routes/) — `routes/`

| Sample | Docs section | Shows | Key |
| ----------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------- | --- |
| `function_node` | [Nodes](https://adk.dev/graphs/routes/#nodes) | The primary node type; bare return vs. explicit `Event` | — |
| `sequence` | [Route sequences](https://adk.dev/graphs/routes/#route-sequences) | `['START', a, b, c]` — each node once, in order | — |
| `branches` | [Route branches](https://adk.dev/graphs/routes/#route-branches-and-conditional-execution) | A router node plus a route→node dispatch map | ✅ |
| `fan_out_join` | [Fan out and join](https://adk.dev/graphs/routes/#parallel-tasks-fan-out-and-join-paths) | Parallel paths merged by a `JoinNode` barrier | — |
| `nested_workflow` | [Nested workflows](https://adk.dev/graphs/routes/#nested-workflows) | A `Workflow` used as a node inside another workflow | — |
| `loop_escalation` | [Loop and escalation exit](https://adk.dev/graphs/routes/#loop-and-escalation-exit) | A back-edge cycle with a routed exit | — |

### [`/graphs/data-handling/`](https://adk.dev/graphs/data-handling/) — `data_handling/`

| Sample | Docs section | Shows | Key |
| ------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | --- |
| `node_output` | [Node output](https://adk.dev/graphs/data-handling/#node-output) | Return a value / an `Event` / yield; last `output` event wins | — |
| `structured_output` | [Passing structured data](https://adk.dev/graphs/data-handling/#node-output-passing-structured-data) | A typed object across an edge, validated by schemas | — |
| `routing_output` | [Routing output](https://adk.dev/graphs/data-handling/#routing-output) | `route` and `output` on one event; `DEFAULT_ROUTE` | — |
| `user_message` | [User-facing messages](https://adk.dev/graphs/data-handling/#user-facing-messages) | A display message vs. data for the next node | — |
| `session_state` | [Session state and scopes](https://adk.dev/graphs/data-handling/#session-state-and-state-scopes) | `ctx.state`, the `app:`/`user:`/`temp:` prefixes | — |
| `schemas` | [Constrain node data with schemas](https://adk.dev/graphs/data-handling/#constrain-node-data-with-schemas) | `inputSchema` / `outputSchema` on an agent node, plus a tool | ✅ |
| `structured_access` | [Access structured data in agents](https://adk.dev/graphs/data-handling/#access-structured-data-in-agents) | `{Class.field}` and `<Class.field from source_node>` | ✅ |

### [`/graphs/human-input/`](https://adk.dev/graphs/human-input/) — `human_input/`

| Sample | Docs section | Shows | Key |
| -------------------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | --- |
| `get_started` | [Get started](https://adk.dev/graphs/human-input/#get-started) | The two-node pause: `RequestInput`, reply feeds next | — |
| `payload_and_schema` | [Message and payload](https://adk.dev/graphs/human-input/#request-input-with-a-message-and-payload) | `message` + `payload` + `responseSchema` | — |
| `initial_prompt` | [Tool-confirmation section](https://adk.dev/graphs/human-input/#tool-confirmation-approval-prompts-in-llm-agents) | A HITL node as the FIRST step of a workflow | — |

### [`/graphs/dynamic/`](https://adk.dev/graphs/dynamic/) — `dynamic/`

| Sample | Docs section | Shows | Key |
| ---------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | --- |
| `get_started` | [Get started](https://adk.dev/graphs/dynamic/#get-started) | An orchestrator node driving a child via `ctx.runNode()` | — |
| `nodes` | [Nodes](https://adk.dev/graphs/dynamic/#node) / [Workflows](https://adk.dev/graphs/dynamic/#workflows) | `node()` vs. `new FunctionNode()` | — |
| `data_handling` | [Data handling](https://adk.dev/graphs/dynamic/#data-handling) | `editorial_workflow`: agent → function, no state keys | ✅ |
| `sequence_route` | [Sequence route](https://adk.dev/graphs/dynamic/#sequence-route) | `city_workflow`: sequential `runNode` calls + schemas | ✅ |
| `loop_route` | [Loop route](https://adk.dev/graphs/dynamic/#loop-route) | A real `while` loop (generate → lint → fix), bounded | ✅ |
| `parallel_route` | [Parallel execution routes](https://adk.dev/graphs/dynamic/#parallel-execution-routes) | `Promise.all` fan-out (the `asyncio.gather` equivalent) | — |
| `human_input` | [Human input](https://adk.dev/graphs/dynamic/#human-input) | HITL inside an orchestrator; the leaf keeps `rerunOnResume: false` | — |
| `custom_run_ids` | [Custom execution IDs](https://adk.dev/graphs/dynamic/#custom-execution-ids) | `ctx.runNode(..., {runId})` for a reorderable collection | — |

## Python → TypeScript differences

The ports are faithful in structure; these are the places where the API itself
differs, all called out again in the affected sample's header comment.

- **No `@node` decorator.** `node(fn, options)` is the factory form; the
explicit `new FunctionNode(name, fn, config)` constructor is also public.
- **No `Event.message`.** Python's `Event(message=...)` becomes an event with
`content` — rendered to the user, and NOT passed to the next node.
- **No `Event(state=...)`.** Write through `ctx.state`; the accumulated delta is
attached to the node's events.
- **No signature-based injection.** Python binds `node_input`/state values to
named parameters by introspection. TypeScript handlers always take
`(ctx, input)` and read state explicitly via `ctx.state`.
- **A workflow's input is a `string` only for a text-only turn** — for anything
else the entry node is handed the raw `Content`. Every entry node here
declares `nodeInput: string` and calls string methods on it directly, so a
non-text first turn fails loudly rather than stringifying to
`"[object Object]"`; take a `Content` (or `unknown`) if you need to accept
one. Values that genuinely are untyped — `ctx.runNode(...).output`, a
`ctx.resumeInputs[id]` reply — are coerced explicitly at the point of use.
- **`ctx.runNode()` resolves to a node _result_,** not the output directly — read
`.output`. It also does not throw when a child interrupts: check
`.interruptIds` and bail out (see `dynamic/human_input`).
- **A second `output` event overwrites the first, silently.** The Python page
gives two accounts of emitting `output` more than once from a node — each
`yield` "adds to a list of data objects on the Event", and two yields carrying
`Event.output` are "a runtime error". Neither is what happens here: there is
no list and no error, the last event to set `output` wins, and the successor
never sees the rest. Emit it once (see `data_handling/node_output`).
- **`LlmAgent.inputSchema` is not the node's input contract.** It is only used
when the agent is exposed as a tool. Inside a graph, put the validating schema
on the node: `node(agent, {inputSchema})`.
- **Schemas are Zod objects** (or a genai `Schema`) rather than pydantic models.
- **`{Class.field}` and `<Class.field from source_node>` work verbatim** — the
Python data-selection syntax is supported (see `data_handling/structured_access`).

## See also

The `tests/integration/workflows/*/agent.ts` files are a second, larger set of
workflow examples — TypeScript ports of Python's
[`contributing/samples/workflows`](https://github.com/google/adk-python/tree/main/contributing/samples/workflows),
each paired with a record/replay integration test. They cover surface these
docs snippets do not: retries, parallel workers, auth (API key and OAuth),
node-as-tool, `task` mode, and multi-trigger nodes.
Comment on lines +165 to +170

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit. No check runs these 26 samples.

eslint "**/*.ts", prettier "**/*.ts", scripts/check_license.sh and the new tsc step all read samples/, so a syntax, style, license or type error fails CI. Nothing executes a workflow, so a graph that stops validating in its constructor stays green.

The sibling set named here solves that: each tests/integration/workflows/*/ pairs an agent with a record/replay test, and npm run record:samples re-records them. Please consider the same for the 18 offline samples.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — tests/integration/docs_samples/docs_samples_test.ts.

All 26 are constructed, which is the case you named: a WorkflowAgent
validates its graph in its constructor, so that catches a sample that stops
loading. The 18 offline ones are also run end-to-end through a real
InMemoryRunner, reusing your _harness/sample_harness.ts in offline mode —
which has the useful side effect that an "offline" sample that starts calling a
model throws on the empty response set instead of reaching the network.

One table-driven file rather than 18 pairs. The sibling set needs a file each
because each one asserts something specific about its own graph and carries a
fixture; these need no fixtures, and the property being checked is the same for
every one of them, so 18 near-identical files would be copies rather than
tests. A guard case asserts the table matches the directories on disk, so a new
sample fails until it is classified — the "silently uncovered" hole is closed
by that, not by the file count.

The 8 model-backed samples are constructed only. Driving them means a
checked-in fixture each, and what they exercise beyond the sibling set is
prompt wording rather than graph shape. Say the word if you would rather have
them recorded too.

Checked against all three failures it should catch, since a green new test
proves nothing on its own:

  • duplicate node name in routes/sequence -> that sample's case fails in
    validateDuplicateNodeNames
  • an unregistered new sample directory -> the coverage guard fails
  • an LlmAgent spliced into an offline graph -> fails on the missing fixture

npm run lint, format:check, check_license.sh, ts:check:samples and the
full integration project (74 files, 206 tests) all pass with it.

69 changes: 69 additions & 0 deletions samples/workflows/data_handling/node_output/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

/**
* TypeScript port of the Python snippet in
* https://adk.dev/graphs/data-handling/#node-output
*
* def my_function_node(node_input: str):
* output_value = node_input.upper()
* return Event(output=output_value) # "THE RESULT"
*
* A node hands data to its successor through the event's `output` field. Three
* equivalent ways to produce it:
*
* 1. return a bare value — boxed into `Event(output=value)` for you
* 2. return `createEvent({output})` — the explicit form, when you also need
* `route`, `content`, or `actions`
* 3. yield from a generator — to stream progress alongside the result
*
* Caution: emit `output` from ONE event per execution — but nothing enforces
* that here, so getting it wrong is silent. A node may yield any number of
* events carrying `output`; each one overwrites the last, and the successor
* receives only the final value. The Python page describes two other
* behaviours, and neither holds in TypeScript: it says each `yield` "adds to a
* list of data objects on the Event", and then cautions that two yields
* carrying `Event.output` are "a runtime error". There is no list and no
* error — just last-write-wins. Carry progress on `content` instead.
*
* Run (offline, no API key):
* npm run sample -- samples/workflows/data_handling/node_output/agent.ts
*/

import {createEvent, node, NodeContext, WorkflowAgent} from '@google/adk';

// 1. A bare return value.
const returnRawValue = node(
(_ctx: NodeContext, nodeInput: string) => nodeInput.toUpperCase(),
{name: 'return_raw_value'},
);

// 2. An explicit Event.
const returnEventOutput = node(
(_ctx: NodeContext, nodeInput: string) =>
createEvent({output: `${nodeInput}!`}),
{name: 'return_event_output'},
);

// 3. A generator: stream progress, then emit the output event last.
const yieldProgressThenOutput = node(
async function* (_ctx: NodeContext, nodeInput: string) {
// Progress goes on `content`: displayed, and not passed to the successor.
yield createEvent({
content: {role: 'model', parts: [{text: 'Working on it...'}]},
});
// Exactly one event sets `output`, so there is nothing to overwrite it.
yield createEvent({output: `<<${nodeInput}>>`});
},
{name: 'yield_progress_then_output'},
);

export const rootAgent = new WorkflowAgent({
name: 'node_output_workflow',
edges: [
['START', returnRawValue, returnEventOutput, yieldProgressThenOutput],
],
});
65 changes: 65 additions & 0 deletions samples/workflows/data_handling/routing_output/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

/**
* TypeScript port of the Python snippet in
* https://adk.dev/graphs/data-handling/#routing-output
*
* def router(node_input: str):
* return Event(route="BUG")
*
* `route` is the event field that drives conditional edge dispatch — it is
* independent of `output`, so a router can select a branch AND forward a payload
* in the same event. Route values may be strings, numbers, or booleans, and
* `DEFAULT_ROUTE` catches everything no other branch matched.
*
* Run (offline, no API key):
* npm run sample -- samples/workflows/data_handling/routing_output/agent.ts
* Try "the app crashed" (BUG) or "where is my order?" (falls through).
*/

import {
createEvent,
DEFAULT_ROUTE,
node,
NodeContext,
WorkflowAgent,
} from '@google/adk';

const router = node(
(_ctx: NodeContext, nodeInput: string) =>
createEvent({
route: /bug|crash|error/i.test(nodeInput) ? 'BUG' : 'OTHER',
// Forwarded to whichever branch fires.
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 WorkflowAgent({
name: 'routing_output_workflow',
edges: [
['START', router],
[
router,
{
BUG: handleBug,
// Fires when no other route on this node matched.
[DEFAULT_ROUTE]: handleAnythingElse,
},
],
],
});
Loading
Loading