Google ADK in TypeScript: LlmAgent, tools and workflows
How an ADK project is shaped — rootAgent in agent.ts, FunctionTool with Zod, sequential, parallel and loop workflows sharing state through outputKey — and the four constraints the docs spell out.
One export, two ways to run it
Google's Agent Development Kit has a TypeScript SDK, @google/adk, and a convention the CLI depends on: a project's agent.ts exports rootAgent, and the ADK tooling loads it.
import { LlmAgent } from "@google/adk"
export const rootAgent = new LlmAgent({
name: "support_agent",
model: "gemini-flash-latest",
description: "Answers questions about the product and acts on the customer's behalf.",
instruction: "You answer questions about the product…",
tools: [lookupOrder],
})
npx adk run agent.ts gives you that agent in the terminal; npx adk web gives you a local dev UI at localhost:8000 with a session view, tool-call traces and the confirmation dialogs the SDK can raise. Both come from @google/adk-devtools, which is why the free Google ADK agent generator emits package.json with main: "agent.ts" and run and web scripts, plus an index.ts that drives the same agent through InMemoryRunner for the programmatic case.
The name is an identifier, and ADK checks
ADK validates every agent name on construction: it must start with a letter or underscore and contain only letters, digits, underscores and hyphens — and it cannot be user, which is reserved for the end user's turns. A bad name throws before the first request.
This matters beyond style. The name is what the model passes to transfer_to_agent when it hands off, what AgentTool exposes when an agent is wrapped as a tool, and what shows up in every trace. So in the generator, agent names are checked and never repaired — Booking Agent is reported and left out rather than quietly becoming booking_agent — and the agent's file is its name: agents/booking_agent.ts.
Tools are FunctionTool with Zod
import { FunctionTool } from "@google/adk"
import { z } from "zod"
const lookupOrder = new FunctionTool({
name: "lookup_order",
description: "Look up one order by its id and return its status, items and delivery estimate.",
parameters: z.object({
orderId: z.string().describe("The order id, as printed on the confirmation email."),
}),
async execute(input) {
return { status: "success", order: await orders.get(input.orderId) }
},
})
Return an object. A non-object return is wrapped as { result: value }, and the docs recommend a status key because it is what the model reads most reliably.
Asking the user before acting is code in the tool, not a flag — the TypeScript SDK has no requireConfirmation option. The pattern is to check toolContext.toolConfirmation?.confirmed, and when it is not set, call toolContext.requestConfirmation({ hint, payload }) and return a waiting status:
async execute(input, toolContext) {
if (!toolContext?.toolConfirmation?.confirmed) {
toolContext?.requestConfirmation({
hint: `Refund ${input.amount}?`,
payload: input,
})
return { status: "AWAITING_CONFIRMATION" }
}
return { status: "success", refund: await refunds.issue(input) }
}
adk web shows the dialog and re-runs the tool with the decision. A plain script only sees the waiting status, which is worth knowing before you wonder why your refund tool "did nothing".
Google Search must be alone
GOOGLE_SEARCH is a built-in tool — grounding through Gemini — and the docs state plainly that it can only be used by itself within an agent. Put it beside a FunctionTool on the same LlmAgent and the request fails.
The documented workaround is a dedicated search agent whose only tool is the search, reached from the root as a tool:
const researcher = new LlmAgent({
name: "researcher",
model: "gemini-2.5-flash",
description: "Searches the web for one question.",
instruction: "Search and summarise in two sentences.",
tools: [GOOGLE_SEARCH],
})
export const rootAgent = new LlmAgent({
// …
tools: [lookupOrder, new AgentTool({ agent: researcher })],
})
Two other constraints ride along: it needs a Gemini 2 or later model, and grounding policy requires you to display the search suggestions it returns.
Sub-agents: transfer, tool, or step
An LlmAgent root can involve another agent in two ways. Listing it in subAgents lets the model transfer the conversation to it — transfer_to_agent — and the sub-agent's description is what the root reads before deciding. Wrapping it in new AgentTool({ agent }) makes it a function the root calls and gets an answer from, keeping the conversation itself.
Neither inherits the root's instruction. A sub-agent with an empty instruction is an agent with no role, and it only falls back to the root's model when it has none of its own.
The third way is to make the root a workflow agent. SequentialAgent, ParallelAgent and LoopAgent have no model and no instruction; they run their subAgents as steps, in order, concurrently, or repeatedly:
export const rootAgent = new SequentialAgent({
name: "writing_pipeline",
description: "Draft, then refine.",
subAgents: [writer, refinementLoop],
})
Steps share data through session state. A step's outputKey writes its final text to a key, and any later step reads it back as {key} inside its instruction:
const critic = new LlmAgent({
name: "critic",
model: "gemini-2.5-flash",
instruction: "Review {draft}. Say 'No major issues found.' when it is done.",
outputKey: "criticism",
})
A LoopAgent needs a way to stop. maxIterations is the cap; the early exit is a tool that sets toolContext.actions.escalate = true, conventionally named exit_loop, which a step's instruction tells the model to call when the work is finished. A loop with neither never ends — the generator reports that combination rather than emitting it.
MCP toolsets and structured output
MCPToolset attaches an external MCP server to an LlmAgent: { type: "StdioConnectionParams", serverParams: { command, args } } for a child process, { type: "SseConnectionParams", url } for a remote one, with an optional array of tool names as a second argument to expose only some. ADK connects it the first time the agent needs a tool. It has no name of its own, so it is the one thing in the generated tree whose file is a slug rather than an identifier.
outputSchema makes the root answer in JSON of a fixed shape, written as a @google/genai Schema (Type.OBJECT with properties and required). It also, per ADK, disables tool calls and transfers on that agent — the model is no longer free to call anything. So it belongs on a final step or a leaf agent, not on a root that also needs tools. The generator emits the schema and reports the conflict.
Where to start
mkdir my-agent && cd my-agent
npm init --yes && npm pkg set type="module" main="agent.ts"
npm install @google/adk && npm install -D @google/adk-devtools
Node 24.13 or newer, GEMINI_API_KEY in .env (or the Vertex variables), then npx adk web. If you would rather see the whole project before writing it — the root and its steps, the search sub-agent done correctly, the confirmation pattern, the loop with its exit — the Google ADK agent generator produces it as a downloadable .zip, free and without an account.
The same form exists for the Claude Agent SDK, the OpenAI Agents SDK and LangChain Deep Agents, and the MCP server generator builds the toolsets any of them can attach.
Target-specific scaffolding
Continue this learning path
Permissions, manifests, capabilities, agent projects and MCP servers for browser extensions, desktop, mobile, and backend agents.
Design your application
Design Web, Desktop, Mobile, Terminal, or Extension projects with a target-aware canvas and exporter.
Open projects