Prerequisites

What you will learn

You will build Traivel, an AI travel assistant.

What you need

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?

Workaround adk-devtools bug

Env variable

Add 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.

Description

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

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

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.

Our Tools

We have two (mock) tools already defined in the starter code: searchDestinations and checkVisaRequirement. We will populate the parameters and execute fields.

Parameters

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."),
  }),
  // ...
});

Execute

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 search_destinations with region: domestic

"Plan my trip to the Moon"

Declines — not a real destination

"I want to go to Japan"

Calls search_destinations AND check_visa_requirement (international → visa)

"Any nice beaches in Indonesia?"

Calls search_destinations only (domestic, no visa needed)

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],
});

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.

Register the sub-agent on the root agent

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
});

Define the parallel scout

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.`,
});

Multi-agent patterns

ADK provides several multi-agent patterns:

Pattern

Use case

ParallelAgent

Independent tasks that run simultaneously

SequentialAgent

Pipeline — output of one feeds into the next

LoopAgent

Iterative refinement until a condition is met

AgentTool

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.

API Recap

Feature

API

Agent

new LlmAgent({ name, model, instruction, tools, subAgents })

Custom tool

new FunctionTool({ name, description, parameters, execute })

Parameter schema

Zod — z.object({ ... }) with .describe()

Skill loading

loadAllSkillsInDir("./skills") + new SkillToolset(skills)

Parallel agents

new ParallelAgent({ subAgents: [...] })

CLI test

npx adk run agent.ts

Web UI

npx adk web

Files in this codelab

File

Purpose

agent.ts

Root agent + sub-agents + skill toolset

tools.ts

searchDestinations + checkVisaRequirement tools

mock-data.ts

Mock destination and visa data

skills/destination-picker/SKILL.md

Guided discovery skill

Next: Part II adds session state, persistent sessions (SQLite), and cross-session memory.