You will build Traivel, an AI travel assistant.
npm -v && node -v && git -v
Clone the project starter:
git clone https://github.com/ekafyi/bwai26-adk-js-workshop-starter.git
# install dependencies
npm install
What's in package.json?
@google/adk – ADK JS@google/adk-devtools – ADK JS Development ToolsAdd GEMINI_API_KEY in .env.
cp .env.example .env
vi .env # or other editor
In ADK, agent is an instance of LlmAgent. Let's define our agent:
// agent.ts
import { LlmAgent } from "@google/adk";
export const rootAgent = new LlmAgent({
name: "traivel_agent",
model: "gemini-flash-latest",
description: "...",
instruction: "...",
});
An Agent has:
This workshop uses "gemini-flash-latest" for price and performance balance. You can also specify the exact model name, e.g. gemini-3-flash-preview, gemini-3-pro-preview, gemini-2.5-pro, etc. See valid model names.
Copy into description field (or write your own!):
"Travel wishlist and planner agent for Indonesian travelers. Helps search destinations, check visa requirements, discover new places, and get activity recommendations."
Instruction contains agent's persona, behaviour, guidelines.
Copy into instruction field:
`You are Traivel, a friendly travel assistant for Indonesian travelers.
Your capabilities:
- **Search destinations**: Use \`search_destinations\` when users ask about places to visit. Filter by region (domestic/international), category, or budget.
- **Check visa requirements**: Use \`check_visa_requirement\` when users ask about international travel documents. Only relevant for international destinations.
- **Destination scouting**: Use the \`destination_scout\` sub-agent when users want detailed info about attractions and dining for a specific destination.
- **Guided discovery**: If a user seems undecided, use the \`destination-picker\` skill to guide them through preference discovery.
Guidelines:
- Greet user and immediately ask about their desired travel destination.
- Respond in Bahasa Indonesia if the user writes in Bahasa Indonesia, otherwise English.
- For international destinations, proactively mention visa requirements.
- When users ask "what's there to do?" about a specific place, delegate to the destination scout.
- If user is unsure where to go, use the destination-picker skill instead of dumping all destinations.
- If user seems not convinced about a destination, suggest alternatives rather than just one.
`
ADK DevTools provides a CLI and Dev-only Web UI for quick testing and debugging.
Run the agent with the ADK CLI:
npx adk run agent.ts
💬 Type a message like Hello! — what happens?
Our agent can chat, but it can't do anything yet. Let's add tools.
LLMs have immense knowledge but also have various limitations. We use tools to enhance it.
Tools are functions the agent decides to call based on the conversation. The agent reads each tool's name, description, and parameter descriptions to decide whether and how to call it.
ADK uses FunctionTool with Zod schemas for type-safe parameter definitions.
import { FunctionTool } from "@google/adk";
const myTool = new FunctionTool({
name: "...",
description: "...",
execute: () => ({ }),
});
mock-data.ts contains travel destination and visa requirements for Indonesian nationals, wrapped in async functions that resemble code to interact with external service in real apps.
It's ready to use, you don't need to do anything.
We have two (mock) tools already defined in the starter code: searchDestinations and checkVisaRequirement. We will populate the parameters and execute fields.
Tool parameters are defined with zod (included as dev dependency) for type safety. Copy these into your code:
// tools.ts
export const searchDestinations = new FunctionTool({
// ...
parameters: z.object({
region: z.enum(["domestic", "international"]).optional()
.describe("Filter by domestic or international destinations."),
category: z.string().optional()
.describe("Filter by category: beach, culture, nature, culinary, adventure, diving, shopping, history, family."),
budget: z.enum(["budget", "mid-range", "luxury"]).optional()
.describe("Filter by budget level."),
}),
// ...
});
export const checkVisaRequirement = new FunctionTool({
name: "check_visa_requirement",
description: "Check visa requirements ...",
parameters: z.object({
country: z.string().describe("The destination country name."),
}),
// ...
});
The execute function receives the parsed parameters and returns a plain object. The agent reads the returned data and decides how to present it.
// tools.ts
export const searchDestinations = new FunctionTool({
// ...
execute: ({ region, category, budget }) => fetchDestinations(region, category, budget),
});
export const checkVisaRequirement = new FunctionTool({
// ...
execute: ({ country }) => fetchVisaInfo(country),
});
Now we will give our agent the tools we defined.
Import the tools into agent.ts and pass them to the LlmAgent config:
// agent.ts
import { searchDestinations, checkVisaRequirement } from "./tools.js";
export const rootAgent = new LlmAgent({
// ... name, model, description, instruction
tools: [searchDestinations, checkVisaRequirement],
});
Test again:
npx adk run agent.ts
Try these:
Input | Expected behavior |
"I want to go to Bali" | Calls |
"Plan my trip to the Moon" | Declines — not a real destination |
"I want to go to Japan" | Calls |
"Any nice beaches in Indonesia?" | Calls |
The agent has no hardcoded routing logic — it reads tool descriptions and decides what to call.
npx adk web
It opens a browser-based UI at http://localhost:8000. It has:
Try the prompts from the previous step in the Web UI. What details can you find?
Use a skill when you want to guide the agent through a multi-step process without bloating the main instruction.
Tools or Skills?
Create a file at skills/destination-picker/SKILL.md and populate with this:
---
name: destination-picker
description: Guided destination discovery skill for undecided travelers. Asks about preferences and recommends destinations step by step.
---
# Destination Picker
You are a travel advisor helping undecided users discover their ideal destination.
## Steps
1. **Ask about vibe**: "What kind of experience are you looking for? Beach relaxation, cultural immersion, nature adventure, or food exploration?"
2. **Ask about region preference**: "Prefer somewhere in Indonesia, or open to international destinations?" "Asia, Europe, elsewhere?"
3. **Ask about budget**: "What's your budget range? Budget-friendly, mid-range, or luxury?"
4. **Recommend**: Based on their answers, call `search_destinations` with the matching filters and present the top 2-3 results with a brief pitch for each.
5. **Offer next steps**: "Would you like to know more about any of these? I can check visa requirements for international options."
## Guidelines
- Keep the conversation friendly and casual, like a knowledgeable friend.
- Don't ask all questions at once — one at a time.
- If the user gives partial answers (e.g. only mentions beach), work with what you have and ask follow-ups.
- Always use the `search_destinations` tool to ground your recommendations in actual data rather than making up destinations.
When the user says "I'm not sure where to go", the agent loads this skill and follows the steps. This way, it does not bloat agent's instruction.
Now we import the skills and pass it to the agent. We load and put them in a SkillToolset object.
// agent.ts
import { LlmAgent, SkillToolset, loadAllSkillsInDir } from "@google/adk";
const skills = await loadAllSkillsInDir("./skills");
const skillToolset = new SkillToolset(skills);
export const rootAgent = new LlmAgent({
// ... name, model, description, instruction
tools: [searchDestinations, checkVisaRequirement, skillToolset],
});
loadAllSkillsInDir scans subdirectories for SKILL.md files.SkillToolset exposes them to the agent as a callable tool.Test with the ADK DevTools Web UI: npx adk web.
💬 Try: I'm not sure where to go for my next trip — how does the agent respond?
When building complex apps, agent instructions fill up quickly. Splitting into focused sub-agents gives better results — each sub-agent has a specific role and instruction. The root agent delegates to them based on their description.
Add destinationScoutAgent to the root agent's subAgents array:
// agent.ts
import { LlmAgent, ParallelAgent, SkillToolset, loadAllSkillsInDir } from "@google/adk";
// Root agent
export const rootAgent = new LlmAgent({
name: "traivel_agent",
// ...
subAgents: [destinationScoutAgent],
});
// Parallel sub-agents
const destinationScoutAgent = new ParallelAgent({
// ... TODO
});
As a parallel agent, destinationScoutAgent runs multiple sub-agents simultaneously as independent queries – the attraction and dining scouts, in this case – and collects their outputs.
import { LlmAgent, ParallelAgent } from "@google/adk";
const destinationScoutAgent = new ParallelAgent({
name: "destination_scout",
description: "Runs attraction and dining scouts for a given destination in parallel.",
subAgents: [attractionScoutAgent, diningScoutAgent],
});
const attractionScoutAgent = new LlmAgent({
name: "attraction_scout",
model: "gemini-flash-latest",
description: "Recommends top attractions for a given destination.",
instruction: `You are a travel attraction expert for Indonesian travelers.
When given a destination, suggest 3-4 top attractions with brief descriptions.
Keep each description to 1-2 sentences. Format as a numbered list.`,
});
const diningScoutAgent = new LlmAgent({
name: "dining_scout",
model: "gemini-flash-latest",
description: "Recommends local food and dining options for a given destination.",
instruction: `You are a food expert for Indonesian travelers.
When given a destination, suggest 3-4 must-try local dishes or restaurants.
Keep each suggestion to 1-2 sentences. Format as a numbered list.`,
});
ADK provides several multi-agent patterns:
Pattern | Use case |
| Independent tasks that run simultaneously |
| Pipeline — output of one feeds into the next |
| Iterative refinement until a condition is met |
| Use one agent as a tool within another |
Test:
npx adk run agent.ts
Test with the ADK DevTools Web UI: npx adk web.
💬 Ask: What to do in Bali? — how does the agent respond?
You built Traivel, an AI travel assistant with custom tools, skills, and parallel sub-agents.
Feature | API |
Agent |
|
Custom tool |
|
Parameter schema | Zod — |
Skill loading |
|
Parallel agents |
|
CLI test |
|
Web UI |
|
File | Purpose |
| Root agent + sub-agents + skill toolset |
|
|
| Mock destination and visa data |
| Guided discovery skill |
Next: Part II adds session state, persistent sessions (SQLite), and cross-session memory.