1. Why A Refund Assistant Cannot Be Only An Agent

Consider a refund assistant. User language is open-ended: “the size is wrong,” “the parcel never arrived,” or “I want another color” all require intent recognition, follow-up questions, and order lookup. The refund procedure cannot be equally open-ended: check the order, evaluate policy, request approval when risk is high, and only then write the refund. That split is why ADK puts Agent and Workflow together.

A prompt that says “check the order, then policy, then refund” may work once and skip a step later, or repeat a write during retry. Hard-coded control alone has the opposite problem: it cannot understand a new phrasing, request missing information, or choose a useful tool. The assistant needs autonomy and deterministic control to hand work to each other.

2. Five Roles To Learn First

  • Agent: understands language, asks follow-up questions, chooses tools, and makes open judgments.
  • Workflow: owns the graph: parallel steps, joins, branches, and required ordering.
  • Runner: starts a run and connects an Agent or Workflow to users, sessions, and services.
  • Event: records facts such as node start, output, approval wait, and completion.
  • Task API: delegates structured work to another Agent and requires an explicit completion result.

The minimal refund route needs the first four. Task API enters only when policy review becomes a specialist-agent job. Introducing it at that moment explains its purpose better than memorizing the full API up front.

3. How One Refund Reaches Completion

Runner chooses between a root LlmAgent and a root BaseNode. It does not automatically run a chat Agent and then enter a Workflow. This example therefore chooses a root Workflow so deterministic steps can be resumed. Its first node is an LlmAgent(single_turn) that interprets the user's language; order lookup, policy, approval, and the final write are later nodes. A root chat Agent could instead call a Workflow through an explicit application integration, but the two designs are alternative entry routes, not an undocumented sequence.

  1. Runner finds the user's session and gives “the shoe size is wrong” to the root Workflow.
  2. The intake Agent node emits a structured order id and reason, requests missing data when needed, then starts order and policy checks.
  3. Each check emits Events. Workflow waits for both instead of asking the model to remember progress.
  4. Eligible low-risk work takes the automatic route; high-risk work pauses in a human-approval branch.
  5. If a specialist Agent checks policy, Task API requires a structured result that explicitly completes the task.
  6. Runner stores process events and the final result in the session while streaming them to the caller.

Keep one division in mind: Agent owns understanding and judgment, Workflow owns required execution order, and Runner owns how the run becomes observable and recoverable.

ADK Python's README puts two items up front in 2.0: Workflow Runtime and Task API. Workflow Runtime is described as a graph-based engine for routing, fan-out/fan-in, loops, retry, state management, dynamic nodes, human-in-the-loop, and nested workflows. Task API is structured agent-to-agent delegation. That is the framework stance: the 2.0 highlights place autonomous agents and deterministic flow side by side.

The Quick Start says the same thing: ADK applications are built with Agent, which defines instructions, tools, and behavior, and Workflow, which orchestrates agents and tasks in a graph-based flow. See the README Quick Start. The top-level package export reinforces that boundary: google.adk exposes Agent, Workflow, Runner, Event, and Context. Source: __init__.py.

Reading contract. Do not choose between “all agent autonomy” and “all hard-coded workflow.” Track how Agent owns open language and tool use, Workflow owns topology and recovery, and Runner joins both to session events.

Evidence boundary. Source links are pinned to Google ADK Python public snapshot 62b97007378c4da749a1615d0881aba443a32325. This chapter uses only the public README, source code, and samples. It does not infer Google internal services, hosted platform behavior, or private control-plane features.

A refund assistant makes the handoff easier to replay. The shape below is not ADK's full schema; it is the ownership movement to track while reading the source:

Runner.run_async(root=refund_workflow, message="the shoe size does not fit")
  -> intake_agent(single_turn): emit {order_id, reason} or RequestInput
  -> Workflow state:
       order = pending(check_order)
       policy = pending(check_policy)

check_order node -> Event(output_for="check_order", output={order_id, status})
check_policy task agent -> finish_task(output={eligible: true, reason})
JoinNode waits for: check_order + check_policy

route:
  eligible and low_risk -> issue_refund node
  high_risk -> human_approval node

Runner event queue -> plugin callbacks -> session service -> caller stream

The important point is that the agent does not have to remember "which refund step am I on" inside prompt text. Open language handling lives in Agent, topology and joins live in Workflow, task completion lives in Task API, and Runner turns the result into observable, resumable session events.

4. Agent Owns Autonomy, But Not The Whole App

LlmAgent contains the classic agent owners: model, instruction, tools, planner, callbacks, input/output schema, output key, and code execution. The important field is mode: chat is a standard chat agent, task is a multi-turn task agent, and single_turn completes work without chatting with the user. Source: LlmAgent fields.

Mode Behavior Refund-assistant role
chat A primary assistant that can continue a user conversation. Understand the refund request and ask for missing details.
task A delegated worker that explicitly completes through finish_task. Check refund policy as a bounded subtask with schema-validated output.
single_turn A one-input/one-output worker rather than an ongoing chat. Summarize order risk inside one workflow node.

ADK does not force every step to be an autonomous chat agent. A root LLM agent must be chat mode. An LLM agent used as a workflow node defaults to single_turn. A task-mode agent receives a finish_task tool so the model can explicitly signal completion. The post-init hook also wraps single_turn and task sub-agents as tools.

FinishTaskTool shows the Task API semantics. It validates task output against the task agent's output schema and treats the finish_task call as the task completion signal. That is much stronger than asking a sub-agent to end with a magic word. Sources: tool declaration and execution and validation.

5. Workflow Owns Deterministic Graph Control

The Workflow source file says it combines the user-facing graph definition with the execution engine, and that Workflow(BaseNode) uses _run_impl as its orchestration loop. The class comment splits that loop into setup, loop, and finalize: build graph, seed triggers, schedule ready nodes, handle completions, and collect terminal output. Sources: file header and Workflow definition.

This graph is not just a visual DSL. Graph.from_edge_items compiles explicit edges, tuple chains, routing maps, and fan-out tuples into internal edges. Validation checks duplicate node names, START, reachability, duplicate edges, default routes, and unconditional cycles. Sources: edge item types, compilation and routing, and graph validation.

The fan-out/fan-in sample makes the style concrete: three functions run from START, converge into JoinNode, then pass to aggregate. The model is not asked through prompt text to do uppercase, count, and reverse in the right order. The graph runtime owns the topology. Sample: fan_out_fan_in. JoinNode advertises _requires_all_predecessors, which tells the orchestrator to wait for all incoming predecessors. Source: JoinNode.

Agent Owner

  • Instruction and static instruction
  • Tools, toolsets, planner, callbacks
  • chat / task / single_turn mode
  • Output schema and output key

Workflow Owner

  • Edges, routes, fan-out/fan-in, join
  • Node input/output schema
  • Retry, timeout, wait_for_output
  • Resume, interrupt, terminal output

6. BaseNode And NodeRunner Are The Execution Grain

ADK abstracts workflow steps as BaseNode. It owns name, description, rerun_on_resume, wait_for_output, retry, timeout, input_schema, output_schema, and state_schema. BaseNode.run is the public entry point: it calls _run_impl, skips None, passes through Event, converts RequestInput into an interrupt event, and wraps any other yielded value into Event(output=value). Sources: BaseNode fields and BaseNode.run.

BaseNode responsibility Business-flow meaning
input_schema / output_schema Nodes exchange validated values instead of unstructured prompt strings.
retry_config / timeout External slowness and failure policy remain visible at workflow level.
rerun_on_resume Recovery can choose whether to execute the interrupted node again.
wait_for_output Fan-in and downstream scheduling can state exactly which output they await.

NodeRunner executes each node. It creates a child context, drives BaseNode.run, enqueues events into the invocation queue, writes output, route, and interrupt IDs back into the child context, and returns that context to the workflow loop. It enriches events with author, invocation ID, node_info.path, branch, and isolation scope. Sources: NodeRunner comment and event enrichment.

6.1 Who Owns Each Piece Of Refund State?

“Workflow has state” does not make Workflow the owner of every fact. Refund RF-204 leaves four records with different purposes:

Record Owner What it retains What it cannot prove
Child context NodeRunner Node input, output, route, interrupt id That the external refund happened
Invocation queue Runner Events awaiting consumption in this invocation That an in-memory queue survives process restart
Session events Session service Non-partial Events, attribution, recovery material That the next model sees the full event history
Refund fact and receipt External refund system Idempotency key, refund state, business receipt That Workflow completion alone proves the write

7. Runner Connects Agent And Workflow To Session Events

Runner's class comment says it runs agents while handling session, event generation, artifact storage, session management, and memory. Source: Runner attributes. Its sync run is a local convenience wrapper; the production path is run_async. run_async dispatches by root type: root LlmAgent uses chat mode and is wrapped as a node; root BaseNode uses the node runtime. Source: run / run_async.

Event consumption matters. _consume_event_queue pulls events from the invocation queue, runs plugin callbacks, builds the output event, persists non-partial events through the session service, and yields them to the caller. ADK events are not temporary logs. They are session material. Source: _consume_event_queue.

Event carries workflow metadata: NodeInfo.path, output_for, message_as_output, branch, and isolation_scope. Those fields give graph nodes, task delegation, branch history, and resumable sessions a shared language. Source: Event and NodeInfo.

Pause, retry, and resume must remain separate. Each means “the node did not finish in one pass,” but each has a different state owner and persistence contract:

Runtime case What runtime does What recovery depends on Common misread
Pause / interrupt RequestInput becomes an Event with an interrupt ID; non-partial events enter the session service. The caller returns a function response for the matching interrupt ID. This is not node failure and does not restart the whole graph.
Retry NodeRunner retries inside the current execution according to retry_config and timeout. The current child context and local attempt count; attempt count is not persisted across resume. Retry handles failure in one invocation; it is not interrupt recovery.
Resume Runner parses resume input, rehydration scans session events, and workflow restores completed, waiting, and interrupted nodes. Invocation ID, prior events, interrupt response, and rerun_on_resume. With rerun_on_resume=false, resume input may become node output; true reruns node logic.

Resume-input extraction and context setup live in the Runner resume path; prior node state is recovered by the rehydration scan; and prior output plus interrupt IDs are restored in the NodeRunner child context.

Workflow recovery still does not roll back an external system. issue_refund should carry a stable idempotency key such as refund:RF-204. Only a business receipt from the refund system proves adoption. If the external write succeeds but the connection fails before its Event is persisted, resume must query or retry by idempotency key. “No completion Event in the session” does not prove “no refund happened.” A node producing output, policy validation passing, and the refund system accepting the write are three separate states.

8. Task API Bridges Agent And Workflow

_llm_agent_wrapper runs LLM agents as workflow nodes. It prepares input for single_turn agents, handles delegated task function calls for task agents, and turns schema-validated output into node output. run_llm_agent_as_node also states the default: an LLM agent used as a workflow node becomes single_turn unless configured otherwise. Task and single_turn agents use isolation scope to filter context, while chat agents see the full conversation. Sources: input/output handling and run_llm_agent_as_node.

This is the difference between ADK and a pure workflow library or a pure agent loop. A workflow can run an agent as a node. An agent can delegate to task or single_turn sub-agents through tools. The bridge is made from Event, node output, function calls, and isolation scope.

9. Invariants To Carry Forward

Invariant Source Fact Engineering Meaning
Agent and Workflow are peer entry points. The top-level package exports Agent, Workflow, Runner, and Event. ADK does not stuff all business flow into prompts or demote agents to plain functions.
Workflow owns graph execution. _run_impl is setup, loop, and finalize. Routes, concurrency, joins, resume, and terminal output have a runtime owner.
NodeRunner is the event attribution grain. Node events are stamped with path, run ID, branch, and isolation scope. Sessions can reconstruct who ran, who produced output, and where execution paused.
Task API is not a prompt convention. Task mode gets finish_task, and the wrapper handles output. Delegation can have schema, completion signal, and context isolation.
Runner is the service boundary. Events enter a queue, run plugin callbacks, persist to session service, then yield. Application code receives an observable and recoverable execution stream.

The next chapter moves to Agno. The question shifts from graph runtime to agent platform: when agents go into products, who owns API, storage, tracing, scheduling, RBAC, and the control plane?

Sources