The smallest sample that uses an actual LLM as the routing brain inside a workflow graph. An LlmAgent classifies the user's message into one of three categories; a trivial Go function then emits the corresponding Event.Routes value, dispatching to one of three handlers.
- Concept: LLM classifies, a plain function emits the route, the engine dispatches (
NewAgentNode+StringRoute). - Needs LLM? Yes (Gemini)
Same shape as adk-python's contributing/workflow_samples/route/ sample (LLM classifier + plain function emitting the routing event). For the non-LLM version, see ../string.
Show the canonical "LLM as the brain, engine does the routing" pattern: keep the LLM stateless about routing and keep the routing logic in plain code. The classifier returns one word; the router turns that into an Event.Routes value; three StringRoute edges dispatch to the matching handler.
export GOOGLE_API_KEY=<your-key>graph LR
User[User]
subgraph "ADK Application Workflow"
Start((Start)) --> C[Agent Node: classify LLM]
C --> R{Node: route_by_classification}
R -- "question" --> Q[Node: answer_question]
R -- "statement" --> S[Node: comment_statement]
R -- "exclamation" --> E[Node: react_exclamation]
Q --> End((End))
S --> End
E --> End
end
User -- "1. What time is it?" --> Start
End -- "2. answering question: What time is it?" --> User
go run ./examples/workflow/routing/llm/ consoleUser -> What time is it?
Agent -> question
answering question: What time is it?
User -> Hello world!
Agent -> exclamation
reacting to exclamation: Hello world!
User -> The sky is blue.
Agent -> statement
commenting on statement: The sky is blue.
The first line of agent output is the LLM's classification (it prints because the console launcher streams every event with text content). The second line is the handler's reply.
| Concept | Where |
|---|---|
workflow.NewAgentNode wrapping an LLMAgent |
classifyNode := workflow.NewAgentNode(classifier, ...) |
| LLM reply flowing to the next node | AgentNode synthesizes the classifier's final reply into Event.Output, which the scheduler feeds to the router as input; LLMAgent.OutputKey = "output" is optional here and only persists the reply to session state |
Custom BaseNode translating the LLM's free-form text into a route value |
route_by_classification reads the classifier's reply, normalises it, and emits Event.Routes |
StringRoute matching one of three categories |
three downstream edges, one per category |
Handler reading the original user message from ctx.UserContent |
each handler calls userMessage(ctx) rather than receiving it as graph input — the routing node only forwards the classification, not the original text |
Mirrors the canonical adk-python pattern: keep the LLM stateless about routing, keep the routing logic in plain code. The alternative — one custom node that calls the LLM and emits Routes from the same Run body — is shorter but mixes "LLM-driven decision" with "graph wiring", and reuses none of the engine's normal LLMAgent machinery (output_key, telemetry, etc.).
workflow.NewAgentNode wraps an agent.Agent for graph execution but does not make that agent visible in the runner's agent tree (the structure runner.findAgentToRun walks to resolve event.Author to an agent.Agent). Without the explicit SubAgents: []agent.Agent{classifier} registration, the runner logs
Event from an unknown agent: classify, event id: ...
on every turn. The warning is harmless — the runner falls back to rootAgent (the workflow itself), and isTransferableAcrossAgentTree blocks any actual re-routing to the LLM agent because the chain to root contains a non-LLMAgent (the workflow wrapper). But registering the classifier as a sub-agent silences the warning and keeps the agent tree consistent with the workflow graph. Treat it as required boilerplate for any workflow that wraps a sub-agent.
Edit gemini-flash-latest in main.go to whatever model your key has access to. The classifier prompt is very short and any modern Gemini model handles it.