ec2b666284
Continuous Integration / Pre-commit Linter (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.10) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.11) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.12) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.10) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.11) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.12) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.14) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Waiting to run
Copybara PR Handler / close-imported-pr (push) Waiting to run
166 lines
7.3 KiB
Markdown
166 lines
7.3 KiB
Markdown
# Function Nodes
|
|
|
|
In ADK, any standard Python function, coroutine, or generator can be used as a workflow node. The framework automatically wraps these callables under the hood, allowing you to build complex graphs with minimal boilerplate.
|
|
|
|
## Introduction
|
|
|
|
Function nodes are the most common and lightweight way to implement logic in ADK workflows. Instead of subclassing `BaseNode` for every step, you can write standard Python functions.
|
|
|
|
Developer problems solved:
|
|
- **Zero Boilerplate**: Write standard Python code without framework-specific class definitions.
|
|
- **Implicit Wrapping**: Pass functions directly to workflow edges; the framework handles integration automatically.
|
|
- **Declarative Signatures**: Access workflow state, input from predecessor nodes, or the execution context simply by declaring them in the function parameters.
|
|
|
|
## Get started
|
|
|
|
The following example demonstrates how to define standard Python functions and use them directly in a workflow chain.
|
|
|
|
```python
|
|
from google.adk import START, Workflow
|
|
|
|
# 1. Simple sequential steps
|
|
# The output of step_one is automatically passed as input to step_two
|
|
def step_one(node_input: str) -> str:
|
|
return f"{node_input} -> step_one"
|
|
|
|
def step_two(node_input: str) -> str:
|
|
return f"{node_input} -> step_two"
|
|
|
|
# 2. Step that accesses workflow state
|
|
# user_name is automatically resolved from ctx.state["user_name"]
|
|
def step_three(node_input: str, user_name: str) -> str:
|
|
return f"Hello {user_name}! {node_input}"
|
|
|
|
# Use the functions directly in the workflow edges
|
|
workflow = Workflow(
|
|
name="my_workflow",
|
|
edges=[
|
|
(START, step_one, step_two, step_three),
|
|
],
|
|
)
|
|
```
|
|
|
|
## How it works
|
|
|
|
When a workflow executes a function node, it performs several operations automatically:
|
|
|
|
### Parameter Resolution
|
|
The framework inspects the function signature to determine how to populate its arguments:
|
|
* **`ctx`** (or any parameter type-hinted as `Context`): Injects the workflow `Context` object.
|
|
* **`node_input`**: Injects the output value from the predecessor node.
|
|
* **Any other parameter**: Resolved by looking up the parameter name in `ctx.state` (or `node_input` if parameter binding is customized).
|
|
|
|
### Type Coercion
|
|
Input values are automatically validated and coerced to match the function's type hints using Pydantic:
|
|
* **Pydantic Models**: If a parameter is type-hinted as a Pydantic `BaseModel` (e.g., `node_input: MyModel`) and the input is a dictionary, it is auto-converted to the model instance.
|
|
* **Content to String**: If a parameter expects a `str` but receives a `types.Content` object (e.g. the raw user message from `START`), it automatically extracts and concatenates the text parts.
|
|
|
|
### Event Normalization
|
|
Return and yield values are normalized to `Event` objects:
|
|
* Returning or yielding `None` does not emit an output event, but execution continues downstream with `None` passed as the input to successor nodes.
|
|
* Raw values (strings, dicts, etc.) are wrapped in `Event(output=value)`.
|
|
* Pydantic models are serialized to dictionaries.
|
|
* State changes made via `ctx.state` during execution are automatically captured and attached to the event to be persisted.
|
|
|
|
## Configuration & Explicit Wrapping
|
|
|
|
While implicit wrapping works for most cases, you can wrap functions explicitly using the `FunctionNode` class or the `@node` decorator when you need to configure execution behavior.
|
|
|
|
Use explicit configuration when you need to define:
|
|
* `rerun_on_resume`: Control if the node should rerun when the workflow resumes (default is `False` for function nodes, meaning they complete with the resuming input).
|
|
* `retry_config`: Enable retries on failures.
|
|
* `timeout`: Set a maximum execution time.
|
|
* `auth_config`: Gate execution with user authentication.
|
|
|
|
### Using `@node` Decorator
|
|
|
|
```python
|
|
from google.adk.workflow import node
|
|
|
|
@node(rerun_on_resume=True)
|
|
def process_payment(node_input: dict) -> str:
|
|
# This node will rerun if the workflow is resumed after a pause
|
|
...
|
|
```
|
|
|
|
### Using `FunctionNode` Class
|
|
|
|
```python
|
|
from google.adk.workflow import FunctionNode, RetryConfig
|
|
|
|
def my_func(node_input: str) -> str:
|
|
...
|
|
|
|
# Wrap explicitly to configure retries
|
|
custom_node = FunctionNode(
|
|
my_func,
|
|
name="payment_step",
|
|
retry_config=RetryConfig(max_attempts=3),
|
|
)
|
|
```
|
|
|
|
## Advanced applications
|
|
|
|
### Emitting Message Events for Web UI
|
|
Only the `Event.message` (user-facing content) is rendered in the Web UI, while `Event.output` is internal and passed downstream. For terminal nodes or nodes producing user-visible intermediate results, yield both a message event and an output event:
|
|
|
|
```python
|
|
from google.adk.events.event import Event
|
|
|
|
async def summarize(ctx: Context, node_input: str):
|
|
result = f"Summary: {node_input}"
|
|
# Rendered in UI (message accepts a raw string and auto-wraps it)
|
|
yield Event(message=result)
|
|
# Passed to downstream nodes
|
|
yield Event(output=result)
|
|
```
|
|
|
|
### State Integration
|
|
|
|
You can update the shared workflow state in two ways: by mutating `ctx.state` directly, or by yielding/returning an `Event(state=...)`.
|
|
|
|
#### 1. Mutating `ctx.state` directly (Imperative)
|
|
This is the most common way when your function already accesses the context. Mutations are tracked and automatically persisted by the framework when the node finishes execution.
|
|
|
|
```python
|
|
def update_via_context(ctx: Context, node_input: str) -> str:
|
|
# State is updated immediately in memory
|
|
ctx.state["counter"] = ctx.state.get("counter", 0) + 1
|
|
return node_input
|
|
```
|
|
|
|
#### 2. Yielding/Returning `Event(state=...)` (Declarative)
|
|
This is useful if you want to declare state changes as events, or if your function does not need the `ctx` object otherwise.
|
|
|
|
```python
|
|
from google.adk.events.event import Event
|
|
|
|
def update_via_event(node_input: str):
|
|
# Returns the state change without needing 'ctx' in the signature
|
|
return Event(
|
|
output=node_input,
|
|
state={"last_processed": node_input}
|
|
)
|
|
```
|
|
|
|
#### Key Differences
|
|
|
|
| Feature | Mutating `ctx.state` | Yielding `Event(state=...)` |
|
|
| :--- | :--- | :--- |
|
|
| **Visibility** | Changes are visible **immediately** to subsequent lines in the same function. | Changes are only visible **after** the event is yielded and processed by the framework. |
|
|
| **Signature** | Requires `ctx: Context` in the function parameters. | Can be used in any function (no `ctx` required). |
|
|
| **Style** | Imperative state modification. | Declarative event-driven state update. |
|
|
|
|
## Limitations
|
|
|
|
- **Union Type Hints**: If `node_input` is hinted with a `Union` type (e.g. `str | dict`), the framework skips automatic type validation to avoid false positives. You must perform manual `isinstance` checks in the function body if you need to validate the input type.
|
|
|
|
## Related samples
|
|
|
|
The following samples demonstrate function node usage:
|
|
- [Node Output](../../../../contributing/samples/workflows/node_output/agent.py) - Auto type conversion to Pydantic models.
|
|
- [Route](../../../../contributing/samples/workflows/route/agent.py) - Yielding events with routes.
|
|
- [State](../../../../contributing/samples/workflows/state/agent.py) - Interacting with workflow state.
|
|
- [Auth API Key](../../../../contributing/samples/workflows/auth_api_key/agent.py) - Using authentication.
|
|
- [Request Input Advanced](../../../../contributing/samples/workflows/request_input_advanced/agent.py) - Human-in-the-loop with schemas.
|