Skip to content

docs(workflow): workflow samples (Part 8/8) - #595

Open
kalenkevich wants to merge 1 commit into
feat/workflows_part7from
feat/workflows_part8
Open

docs(workflow): workflow samples (Part 8/8)#595
kalenkevich wants to merge 1 commit into
feat/workflows_part7from
feat/workflows_part8

Conversation

@kalenkevich

Copy link
Copy Markdown
Collaborator

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

Problem: The workflow API needs runnable, discoverable examples.

Solution — Part 8 of 8 (final). Stacked on Part 7. Adds samples/workflows/ covering the API surface:

  • basics: sequence, loop, loop_self, route, multi_triggers, state, node_output, use_as_output, message
  • parallelism & dynamic: fan_out_fan_in, parallel_worker, dynamic_fan_out_fan_in, dynamic_nodes, nested_workflow
  • HITL & auth: request_input, request_input_advanced, request_input_rerun, auth_api_key, auth_oauth
  • agents & tools: agent_in_workflow, node_as_tool, retry
  • samples/workflows/README.md and a root sample script to run them.

Testing Plan

  • N/A (examples) — but the samples import only the public @google/adk surface and typecheck cleanly against source (verified with a temporary tsconfig aliasing @google/adkcore/src).

Manual E2E: Each sample is runnable via npm run sample -- samples/workflows/<name>.

Checklist

  • I have read CONTRIBUTING.md.
  • I have performed a self-review.
  • Commented hard-to-understand areas.
  • Added tests.
  • Samples typecheck against the public API.
  • Manually tested end-to-end.
  • Dependent changes merged.

Additional context

Final part of the stacked split (…Part 7 → Part 8). Diff: 24 files, +1,973.

Part 8/9 (final) of the feature/workflows split. Runnable examples covering the
workflow API surface:

- basics: sequence, loop, loop_self, route, multi_triggers, state, node_output,
  use_as_output, message
- parallelism & dynamic: fan_out_fan_in, parallel_worker,
  dynamic_fan_out_fan_in, dynamic_nodes, nested_workflow
- HITL & auth: request_input, request_input_advanced, request_input_rerun,
  auth_api_key, auth_oauth
- agents & tools: agent_in_workflow, node_as_tool, retry
- samples/workflows/README.md and a root `sample` script to run them

Samples import only the public `@google/adk` surface and typecheck cleanly
against source.

@AmaadMartin AmaadMartin left a comment

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.

Read the full diff and checked every symbol the 22 samples import against the barrel at c6a678e: all of them (node, NodeContext, Workflow, WorkflowAgent, JoinNode, RequestInput, DEFAULT_ROUTE, createEvent, FunctionTool, LlmAgent, AuthConfig/AuthScheme/AuthCredential/AuthCredentialTypes) are exported from @google/adk, and there is not a single deep import into core/src/... — the public-surface claim in the PR body holds. I also spot-checked the option shapes against source and they match: BuildNodeOptions (name/description/inputSchema/authConfig/rerunOnResume/parallelWorker/retryConfig), RunNodeOptions (useSubBranch/runId/useAsOutput), RequestInputParams, RetryConfig.maxAttempts/initialDelay, LlmAgentSchema (zod object or genai Schema), ToolUnion = BaseTool | BaseToolset | BaseNode (so node_as_tool passing a Workflow and a node in tools is valid), and the temp:<credentialKey> state key the two auth samples read — that is exactly what AuthHandler writes (core/src/auth/auth_handler.ts:23). No hardcoded secrets. Comments below are mostly doc/consistency; the unbounded-loop one is the only substantive one.

Comment on lines +60 to +61
| `parallel_worker` | Map a node across a list with bounded concurrency | ✅ |
| `dynamic_nodes` | Imperative `dynamicEntry` driving `ctx.runNode()` | ✅ |

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.

Not a nit. Two rows in this table advertise API surface that no sample in the set actually exercises.

| `parallel_worker`        | Map a node across a list with bounded concurrency        ||
| `dynamic_nodes`          | Imperative `dynamicEntry` driving `ctx.runNode()`        ||

dynamicEntry appears exactly once in the whole diff — in this line. dynamic_nodes/agent.ts uses edges: [['START', orchestrate]] with an ordinary function node, not the WorkflowConfig.dynamicEntry field (core/src/workflow/workflow.ts:48, which is mutually exclusive with edges). Likewise maxParallelWorkers appears zero times in the diff, so parallel_worker/agent.ts leaves it undefined — unbounded — and does not demonstrate the bounding this row claims.

Either wire the samples to those fields, or reword the rows (e.g. "imperative orchestrator node driving ctx.runNode()" and "map a node across a list"). Since the PR positions this directory as covering the full workflow surface, dynamicEntry having no sample at all is worth calling out in the Feature coverage section too.

async function* (ctx: NodeContext, nodeInput: string) {
ctx.state.set('topic', nodeInput);

for (;;) {

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.

Not a nit. This is an unbounded loop around two live model calls, with no cap and no warning to the reader.

    for (;;) {
      const headline = (await ctx.runNode(generateHeadline)).output as string;
      const feedback = (await ctx.runNode(evaluateHeadline, headline))
        .output as {grade: string};
      if (feedback.grade === 'tech-related') {

The only exit is the grader returning 'tech-related'. If the model keeps grading 'unrelated' (easy with an off-topic input — the sample header suggests no topic constraint), this spins forever at two gemini-2.5-flash calls per iteration and silently burns the user's quota. A sample is the worst place for that, because it is the first thing a new user runs.

const MAX_ATTEMPTS = 5;
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
  ...
  if (feedback.grade === 'tech-related') {
    yield headline;
    return;
  }
}
yield `Gave up after ${MAX_ATTEMPTS} attempts.`;

Same shape exists in loop/agent.ts — the [routeHeadline, {unrelated: generateHeadline}] edge is a graph-level cycle with no iteration limit either. That one is arguably the point of the loop sample, so at minimum the README "needs API key" column should say these two can loop indefinitely.

* `contributing/samples/workflows/sequence`.
*
* REQUIRES an API key (both nodes call a live model). Set GEMINI_API_KEY, then:
* node dev/dist/esm/cli_entrypoint.js run samples/workflows/sequence/agent.ts

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. The samples are split down the middle on how to run them, and this half contradicts the README.

 *   node dev/dist/esm/cli_entrypoint.js run samples/workflows/sequence/agent.ts
 *   npm run sample -- samples/workflows/sequence/agent.ts

13 sample headers use npm run sample -- ... (which is also what the README Running section documents, and what this PR adds the root script for); 9 use the raw node dev/dist/esm/cli_entrypoint.js run ... form: agent_in_workflow, auth_api_key, auth_oauth, node_as_tool, request_input, request_input_advanced, request_input_rerun, route, sequence. Worth normalizing all nine to npm run sample -- so the docs and the samples teach one invocation.

Comment on lines +73 to +79
if (nodeInput === 'reject') {
return createEvent({route: 'rejected'});
}
if (nodeInput === 'approve') {
return createEvent({route: 'approved'});
}
ctx.state.set('feedback', nodeInput);

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. The four HITL samples parse the human's reply four different ways, and this is the strictest one.

    if (nodeInput === 'reject') {
      return createEvent({route: 'rejected'});
    }
    if (nodeInput === 'approve') {
      return createEvent({route: 'approved'});
    }
    ctx.state.set('feedback', nodeInput);

Exact match here and in request_input_rerun/agent.ts; request_input_advanced/agent.ts normalizes with ['yes','y','true','approve','approved'].includes(input.trim().toLowerCase()); node_as_tool/agent.ts uses ['yes','y','true'].includes(String(answer).toLowerCase()). In the interactive flow the README describes ("type your reply on the next turn"), typing Approve or approve here falls through to the revise branch, stores the word as manager feedback, and re-runs draft_email — a silent extra model call that looks like a framework bug to a first-time reader.

const reply = nodeInput.trim().toLowerCase();
if (reply === 'reject') ...
if (reply === 'approve') ...

Picking one normalization and using it in all four keeps the samples from teaching three different idioms for the same thing.

name: 'explain_topic',
model: 'gemini-2.5-flash',
instruction:
'Explain how the following topic relates the the original topic: "{topic}".',

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. Typo in the prompt — "relates the the".

      'Explain how the following topic relates the the original topic: "{topic}".',
      'Explain how the following topic relates to the original topic: "{topic}".',

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants