Skip to content
Back to skills

Adk Workflow Graphs

ASecurity

Build graph-based agents with ADK 2.0 `Workflow` — declare nodes and edges, route conditionally, fan-out/fan-in in parallel, run dynamic nodes at runtime, and add human-in-the-loop revision cycles. Load this skill whenever the agent needs anything beyond a strictly linear or trivially parallel pipeline.

  • 2 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added October 2, 2026
ai-agentspythongosqlnodeapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 6 files and shows the line behind each finding

Scanned October 2, 2026

npx -y skills add Folken2/nuvel --skill adk-workflow-graphs --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Adk Workflow Graphs?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Adk Workflow Graphs
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/folken2-adk-workflow-graphs/badge)](https://www.skillsdirectory.com/skills/folken2-adk-workflow-graphs)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: adk-workflow-graphs
description: >
  Build graph-based agents with ADK 2.0 `Workflow` — declare nodes and edges,
  route conditionally, fan-out/fan-in in parallel, run dynamic nodes at runtime,
  and add human-in-the-loop revision cycles. Load this skill whenever the agent
  needs anything beyond a strictly linear or trivially parallel pipeline.
---

# ADK 2.0 Workflow Graphs

**Version 1.0** | Requires `google-adk>=2.0.0`

ADK 2.0 introduced **`google.adk.workflow.Workflow`** — a graph-based
orchestration primitive that subsumes `SequentialAgent`, `LoopAgent`, and
`ParallelAgent` into one model. Nodes are units of work (functions, `LlmAgent`s,
or other `BaseNode`s); edges declare how output flows and how control routes.

This skill is the API reference. For *when to choose* a Workflow vs. a single
`LlmAgent` vs. one of the shortcut classes, see `adk-agent-patterns`.

## Minimal example

```python
from google.adk.workflow import Workflow

def greet(node_input: str) -> str:
    return f"Hello! You said: {node_input}"

root_agent = Workflow(
    name="my_workflow",
    edges=[
        ('START', greet),
    ],
)
```

Run with `adk run my_agent/` (CLI) or `adk web my_agent/` (web UI).

**The `'START'` sentinel** is the workflow's entry point. `('START', greet)`
means "when the workflow begins, pass the user input to `greet`."

## References

| Resource | Description | Load when |
|----------|-------------|-----------|
| function-nodes | `@node` decorator and `FunctionNode`; param resolution from `ctx.state`; async/generator nodes | A graph step is plain Python — transform, validate, branch logic |
| llm-agent-nodes | `_LlmAgentWrapper` behavior; `output_schema` for parsed dicts; tools-in-workflow; emitting routes from LLM output | A graph step is an `LlmAgent` and you need to compose its output cleanly |
| routing-and-conditions | `Event(route=...)`, dict-syntax edges, `__DEFAULT__` fallback, cyclical edges with validation rules | Branching on a node's result, revision loops, or a state-machine-shaped flow |
| parallel-and-fanout | Tuple syntax for fan-out/fan-in, `JoinNode`, diamond patterns, parallel-state-write rules | Running nodes concurrently — especially when you also need to aggregate results |
| dynamic-nodes | `ctx.run_node()` API, `rerun_on_resume=True`, deterministic naming for replay, mixing static + dynamic | The set of next steps depends on runtime data (data-driven fan-out, recursion) |

Load a reference with `load_skill_resource("adk-workflow-graphs", "<resource>.md")`.

## Core mental model

A `Workflow` is itself a `BaseNode`. Its body is a list of `edges`, where each
edge is a tuple `(source, target)` and either side can be:

- a single node (function, `LlmAgent`, `FunctionNode`, or nested `Workflow`)
- a tuple of nodes — fan-out (in `target`) or fan-in (in `source`)
- a dict mapping route strings to nodes — conditional routing (in `target`)
- the sentinel `'START'` — the entry point (in `source` only)

```python
edges = [
    ('START', classifier),                       # entry
    (classifier, {                               # routed branches
        "sql":   sql_writer,
        "chart": chart_maker,
        "__DEFAULT__": fallback,
    }),
    ((sql_writer, chart_maker, fallback), join), # fan-in
    (join, format_answer),                       # linear step
]
```

Output flows along edges automatically: a node's return value becomes the next
node's `node_input`. State that should be visible across the whole graph goes
through `ctx.state` (read in nodes, written by setting `ctx.state[key] = val`).

## Cheat sheet

```python
from google.adk.workflow import Workflow, node, FunctionNode, JoinNode
from google.adk import Event

# 1. Function node (decorator)
@node
def transform(node_input: str) -> str:
    return node_input.upper()

# 2. Function node (explicit, for naming or reuse)
clean = FunctionNode(lambda x: x.strip(), name="clean")

# 3. Routing — return Event(output=..., route=...)
@node
def classify(node_input: str):
    return Event(output=node_input, route="error" if "fail" in node_input else "ok")

# 4. Dict-syntax routing in edges
edges = [
    ('START', classify),
    (classify, {"ok": transform, "error": handle_error}),
]

# 5. Fan-out (tuple on target side)
edges = [('START', (analyze, translate, summarize))]

# 6. Fan-in (tuple on source side, often with JoinNode)
edges = [
    ((analyze, translate, summarize), JoinNode(name="join")),
    (JoinNode, final_step),
]

# 7. Cycle (revision loop) — must include at least one routed exit
edges = [
    ('START', drafter),
    (drafter, reviewer),
    (reviewer, {"revise": drafter, "approve": send}),
]

# 8. Dynamic child node from inside a parent
@node(rerun_on_resume=True)
async def parent(ctx, node_input: str):
    result = await ctx.run_node(child_agent, node_input=node_input)
    return result
```

## Migration from 1.x shortcut classes

`SequentialAgent`, `LoopAgent`, and `ParallelAgent` still work in 2.0 — they are
now thin wrappers that compile to the same graph machinery. Migrate to
`Workflow` when:

- You need to branch or route based on a step's result.
- You need to fan out *and* aggregate (`ParallelAgent` alone has no join).
- You're nesting two or more of the shortcut classes.
- You want a plain Python step (`FunctionNode`) inline with LLM steps.

Stay on the shortcut classes for trivially linear (3 steps, no routing) or
trivially parallel (no aggregation) cases — they're one line and equivalent.

## Critical rules

1. **`rerun_on_resume=True`** on any node that calls `ctx.run_node()`. Without
   it, an interruption (HITL pause, error retry) won't replay the parent and
   the dynamic child's result won't be wired back in.
2. **Cycles must have a routed edge.** `(reviewer, drafter)` alone is rejected
   at graph validation — there'd be no way out. Use a dict so at least one
   route exits the cycle.
3. **Use deterministic names for dynamic children** — pass `name=` to
   `ctx.run_node()` so replays match the original execution.
4. **Don't wrap with `asyncio.create_task()`** inside a node — always `await`
   directly. The scheduler handles concurrency; manual tasks break replay.
5. **Don't set `ctx.event_author` manually** — the runner manages authorship.

## Common mistakes

| Mistake | Fix |
|---------|-----|
| Forgetting the `'START'` sentinel | Every workflow needs `('START', first_node)` |
| Returning raw `types.Content` from an LLM node | In 2.0 the wrapper auto-extracts `str`; just `return` text or set `output_schema=` for dict |
| Unconditional cycle | Add a routed exit: `(reviewer, {"revise": drafter, "done": next})` |
| Manual `asyncio.create_task()` inside a node | Use `await ctx.run_node(...)` — the scheduler handles parallelism |
| Two parallel branches writing to the same `ctx.state` key | Give each branch a unique key, or fan into a `JoinNode` that merges them |
| Using `ctx.state` for per-step output passing | Output flows along edges automatically as `node_input`; reserve `ctx.state` for cross-cutting values |

Files in this skill

  • SKILL.md6.9 KB
  • references/dynamic-nodes.md4.3 KB
  • references/function-nodes.md2.8 KB
  • references/llm-agent-nodes.md3.8 KB
  • references/parallel-and-fanout.md3 KB
  • references/routing-and-conditions.md3.2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.