-
Notifications
You must be signed in to change notification settings - Fork 218
docs(workflow): add runnable TypeScript ports of the graph-workflow doc snippets #634
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2ea98f1
69679f2
2322fdc
5050299
6389d10
53d5722
83dbd66
09c3146
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit. This comment describes #648's tree, not this one. At So
Comment on lines
+4
to
+9
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit, withdrawn. Keep #648 merged at 02:13Z, four minutes before I posted. The head I read, Your only conflict is |
||
| "paths": {} | ||
| }, | ||
| "include": ["**/*.ts"], | ||
| "exclude": ["node_modules"] | ||
| } | ||
| 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
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 changesThat builds the workspaces, which are The result is 2129 lines that back public documentation pages and that no job compiles. The workflow API is The script already exists — - name: Type check
run: npm run ts:checkI have not confirmed
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch — fixed in c8ff7b4. You were right to hedge on Scoped it instead, following the same extends-the-root pattern // 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:samplesPlaced after Verified it is not vacuously green: Also added a note to the README under Running, since the build command there was the thing that implied coverage that did not exist:
The repo-wide
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rebased onto current
Re-verified on the rebased branch, so the type check is doing real work against And behaviorally, not just at the type level: all 26 samples construct, and the One thing to flag for whoever reviews the CI step: |
||
|
|
||
| `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
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit. No check runs these 26 samples.
The sibling set named here solves that: each
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done — All 26 are constructed, which is the case you named: a One table-driven file rather than 18 pairs. The sibling set needs a file each The 8 model-backed samples are constructed only. Driving them means a Checked against all three failures it should catch, since a green new test
|
||
| 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], | ||
| ], | ||
| }); |
| 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, | ||
| }, | ||
| ], | ||
| ], | ||
| }); |
There was a problem hiding this comment.
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:samplesduplicates the rootts: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.yamllines this PR edits (permissions,persist-credentials, the SHA pins), so whichever lands second needs a manual rebase.There was a problem hiding this comment.
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
80ee6fclanded on this branchwhile I was working, so the rebase kept it and my four commits sit on top of
it. That commit already handles the
pathshalf of the #648 interaction —samples/tsconfig.jsonnow resets"paths": {}so the samples keep resolving@google/adkthroughnode_modulesonce #648 aliases it tocore/srcat theroot. The
validation.yamlhalf (permissions,persist-credentials, the SHApins) 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.
There was a problem hiding this comment.
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 theconflict is resolved.
Three things fell out of it:
The
validation.yamlconflict resolved as both steps rather than one:ts:checkfor the repo,ts:check:samplesfor the samples. Reasoning below.Your zizmor commit dropped out —
permissions: contents: read,persist-credentials: falseand the three SHA pins all came in with #648, sogit dropped that patch as already upstream. Nothing lost.
#648 also created a quieter problem, now fixed. The root config names no
includeand excludes onlynode_modulesand**/dist, so the repo-widetsc --noEmitpicked up all 26 sample files — and resolved their@google/adkimports through the rootpathsaliases, againstcore/src.That is exactly the resolution
80ee6fcadded"paths": {}to prevent, so thetwo checks were running over the same files with opposite resolutions, and the
wrong one would have won any disagreement.
samplesis excluded from the rootconfig now, leaving one owner.
Verified on both sides rather than assumed:
tsc --noEmit --listFilesreports0 files under
samples/and still passes, andtsc -p samples --listFilesreports all 26 and resolves
@google/adktocore/dist/types/index.d.ts.So
ts:check:samplesis not redundant after #648 — it is the only check thatsees these files the way a user's project would. Happy to fold it into
ts:checkinstead if you would rather have one step, but that means thesamples get checked against workspace sources, which
80ee6fcargues against.