docs/guides/workflow/function_node/index.md
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.
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:
The following example demonstrates how to define standard Python functions and use them directly in a workflow chain.
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),
],
)
When a workflow executes a function node, it performs several operations automatically:
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.ctx.state (or node_input if parameter binding is customized).Input values are automatically validated and coerced to match the function's type hints using Pydantic:
BaseModel (e.g., node_input: MyModel) and the input is a dictionary, it is auto-converted to the model instance.str but receives a types.Content object (e.g. the raw user message from START), it automatically extracts and concatenates the text parts.Return and yield values are normalized to Event objects:
None does not emit an output event, but execution continues downstream with None passed as the input to successor nodes.Event(output=value).ctx.state during execution are automatically captured and attached to the event to be persisted.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.@node Decoratorfrom 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
...
FunctionNode Classfrom 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),
)
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:
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)
You can update the shared workflow state in two ways: by mutating ctx.state directly, or by yielding/returning an Event(state=...).
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.
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
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.
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}
)
| 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. |
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.The following samples demonstrate function node usage: