2.2 Recipe: The AI Travel Planner Suite
In this recipe, we’re going to build a production-ready feature: an AI Travel Planner Suite. This suite will consist of multiple specialized flows that work together to provide a seamless travel planning experience.
The idea is to demonstrate how to:
- The power of flow orchestration: building complex features by composing smaller flows. This promotes reusability and maintainability.
- Use structured data outputs with Zod schemas for reliable frontend
integration.
Structured outputs ensure that the data returned from LLMs is predictable and easy to work with. From processing results to rendering UI components, structured data is a must-have for building robust applications instead of simple chatbots that are synonymous with GenAI Apps.
- Implement streaming responses for long-form content generation.
For this example, we’ll create four distinct flows:
-
An explorer flow that suggests travel destinations based on user preferences. We will pass the user’s vibe and budget and get back structured JSON data.
-
An itinerary builder that generates detailed day-by-day plans. This flow will stream the response back to the client for a better user experience.
-
A packing assistant that creates customized packing lists. This flow will also return structured data.
-
A master “lucky planner” flow that orchestrates the other three to deliver a complete travel package.
For testing purposes, we are going to use the Genkit Developer UI to test our flows. We can also use CURL or Postman to make HTTP requests to our flow server. I will be adding supplementary recipes on how to build a frontend to consume these flows later on.
Prerequisites
Section titled “Prerequisites”You should have your environment set up for Genkit development. Follow the Getting Started guide if you haven’t done so already.
I have a Genkit Starter Template that you can use to quickly spin up a new project with everything configured. You can find it here.
Building the Travel Planner Suite
Section titled “Building the Travel Planner Suite”Package Installation
Section titled “Package Installation”First, let’s first ensure we have the necessary packages installed. We’ll need the
@genkit-ai/googleai plugin for LLM access and @genkit-ai/express to serve
our flows over HTTP.
npm install @genkit-ai/googleai @genkit-ai/expressIf you are curious about using other LLM providers, check out the Genkit AI Plugins documentation. There are many to choose from!
Step 1: Define Our Explorer Flow Schemas
Section titled “Step 1: Define Our Explorer Flow Schemas”We will start by defining our first flow: the Destination Explorer. This flow will suggest travel destinations based on user preferences. It will return structured JSON data, so we need to define our input and output schemas using Zod.
When prompting LLMs, you can request for structured outputs like JSON. However,
to ensure the output is always valid and parsable, it’s best to enforce the
schema at the LLM level as well as in your flow definition. the ai.generate
method in Genkit allows you to do just that, by passing an output parameter with
the desired schema.
For this flow, we will define the shape of input and output data as follows:
// Please note we are importing z from 'genkit' to ensure compatibility with Genkit's internal handling of schemas.import { z } from 'genkit';
// Define our shared suggestion schemaconst explorerFlowInputSchema = z.object({ vibe: z.enum(['adventure', 'relaxation', 'culture', 'foodie', 'nightlife']), budget: z.enum(['cheap', 'moderate', 'luxury']),});
// Define our explorer flow output schemaconst explorerFlowOutputSchema = z.object({ suggestions: z.array( z.object({ city: z.string().describe('Name of the city'), country: z.string().describe('Name of the country'), reason: z.string().describe('Reason for recommendation'), }), ),});Next, then we can now define our explorer flow using these schemas. As each flow is an instance of Genkit, first, we need to create an AI instance.
import { genkit, z } from 'genkit';
const ai = genkit({ plugins: [googleAI()], model: googleAI.model('gemini-2.5-flash'),});And now we can define our explorer flow.
const explorerFlow = ai.defineFlow( { name: 'destinationExplorer', inputSchema: explorerFlowInputSchema, outputSchema: explorerFlowOutputSchema, }, async ({ vibe, budget }) => { // ... code to call the LLM and return structured suggestions },);If you have followed along, your full code should look like this so far:
import { genkit, z } from 'genkit';import { googleAI } from '@genkit-ai/googleai';
const ai = genkit({ plugins: [googleAI()], model: googleAI.model('gemini-2.5-flash'),});
// Define our shared suggestion schemaconst explorerFlowInputSchema = z.object({ vibe: z.enum(['adventure', 'relaxation', 'culture', 'foodie', 'nightlife']), budget: z.enum(['cheap', 'moderate', 'luxury']),});
// Define our explorer flow output schemaconst explorerFlowOutputSchema = z.object({ suggestions: z.array( z.object({ city: z.string().describe('Name of the city'), country: z.string().describe('Name of the country'), reason: z.string().describe('Reason for recommendation'), }), ),});
const explorerFlow = ai.defineFlow( { name: 'destinationExplorer', inputSchema: explorerFlowInputSchema, outputSchema: explorerFlowOutputSchema, }, async (input) => { // ... LOGIC TO CALL LLM GOES HERE },);Next, let’s implement the logic to call the LLM and return structured
suggestions. This is done inside the flow handler function, and it’s where we
use the ai.generate method to interact with the LLM. As we will see later, we
can perform other operations here as well, such as calling other flows or
external APIs.
const explorerFlow = ai.defineFlow( { name: 'destinationExplorer', inputSchema: explorerFlowInputSchema, outputSchema: explorerFlowOutputSchema, }, // input type is inferred from inputSchema async (input) => { // 1. Call the LLM with structured output enforcement, using the input parameters const { output } = await ai.generate({ prompt: ` You are a travel expert. Suggest 5 travel destinations based on the following preferences: Vibe: ${input.vibe} Budget: ${input.budget} `, // Enforce the schema at the LLM level for perfect JSON output output: { schema: explorerFlowOutputSchema }, });
// 2. Throw an error if output is missing, sometimes LLM calls can fail silently if (!output) { throw new Error('Failed to generate suggestions'); }
// 3. Return the structured output return output; },);And that’s our explorer flow done! Next, let’s build the other flows.
Step 2: Define the Itinerary Builder Flow
Section titled “Step 2: Define the Itinerary Builder Flow”The Itinerary Builder flow will generate a detailed day-by-day itinerary based on the selected destination and trip duration. This flow will stream the response back to the client for a better user experience.
First, let’s define the input schema for this flow:
const itineraryFlowInputSchema = z.object({ destination: z.string(), durationDays: z.number().min(1).max(7),});We won’t define a complex output schema for this flow since it will return raw Markdown text.
Next, let’s define the itinerary flow. Just like before, we use the
ai.defineFlow method to create the flow. However, this time, we will use the
ai.generateStream method to enable streaming, instead of the regular
ai.generate.
const itineraryFlow = ai.defineFlow( { name: 'itineraryBuilder', inputSchema: itineraryFlowInputSchema, outputSchema: z.string(), // We return raw Markdown text }, // input type is inferred from inputSchema async (input, { sendChunk }) => { // 1. Use streaming generation for the itinerary const { response, stream } = ai.generateStream({ prompt: ` You are an expert travel planner. Your goal is to create detailed itineraries for travelers.
Create a detailed ${input.durationDays}-day itinerary for ${input.destination}.
Use Markdown formatting. Include timings and specific restaurants. `, });
// 2. Stream the response back to the client for await (const chunk of stream) { sendChunk(chunk.text); // Stream partial text to client }
// 3. Return the full text at the end return (await response).text; },);And that’s our itinerary flow done! Next, let’s build the packing assistant flow.
Step 3: Define the Packing Assistant Flow
Section titled “Step 3: Define the Packing Assistant Flow”The Packing Assistant flow will create a customized packing list based on the destination, duration, and activities planned. This flow will also return structured data.
First, let’s define the input and output schemas for this flow:
const packingFlowInputSchema = z.object({ destination: z.string(), season: z.enum(['spring', 'summer', 'fall', 'winter']),});
const packingFlowOutputSchema = z.object({ essentials: z.array(z.string()).describe('Essential items to pack'), clothing: z.array(z.string()).describe('Clothing items to pack'), gadgets: z.array(z.string()).describe('Gadgets to bring along'),});Next, let’s define the packing flow using these schemas:
const packingFlow = ai.defineFlow( { name: 'packingAssistant', inputSchema: packingFlowInputSchema, outputSchema: packingFlowOutputSchema, }, async (input) => { const { output } = await ai.generate({ prompt: ` You are a packing expert. Your goal is to help travelers pack efficiently for their trips.
For each trip, provide a categorized packing list with essentials, clothing, and gadgets. Consider the destination and season.
Create a packing list for ${input.destination} in ${input.season}. `, output: { schema: packingFlowOutputSchema }, });
if (!output) { throw new Error('Failed to generate packing list'); }
return output; },);And that’s our packing assistant flow done! Finally, let’s build the master flow that orchestrates all these flows.
Step 4: Define the Lucky Planner Master Flow
Section titled “Step 4: Define the Lucky Planner Master Flow”The idea behind the Lucky Planner flow is to provide a one-stop solution for users to get a complete travel package. This flow will orchestrate the other three flows to deliver destination suggestions, a detailed itinerary, and a packing list.
First, let’s define the input and output schemas for the Lucky Planner flow:
const iAmFeelingLuckyFlowInputSchema = z.object({ vibe: z.enum(['adventure', 'relaxation', 'culture', 'foodie', 'nightlife']), budget: z.enum(['cheap', 'moderate', 'luxury']), season: z.enum(['spring', 'summer', 'fall', 'winter']), durationDays: z.number().min(1).max(7),});
const iAmFeelingLuckyFlowOutputSchema = z.object({ destination: z.string(), reason: z.string(), itinerary: z.string(), packingList: packingFlowOutputSchema,});And then we can define the Lucky Planner flow itself. Please note how we call the other flows within this flow handler function and since flows are just async functions, we can call them like regular functions, that return promises.
const iAmFeelingLuckyFlow = ai.defineFlow( { name: 'iAmFeelingLuckyPlanner', inputSchema: iAmFeelingLuckyFlowInputSchema, outputSchema: iAmFeelingLuckyFlowOutputSchema, }, async (input) => { // 1. Orchestrate: Call the explorer flow to get options const { suggestions } = await explorerFlow({ vibe: input.vibe, budget: input.budget, });
// if no suggestions, throw an error if (!suggestions || suggestions.length === 0) { throw new Error('No suggestions found'); }
// 2. Pick the attractive option (here, we just take the first) const selection = suggestions[0]; const destination = selection.city;
// 3. Parallel Execution: Run itinerary and packing flows together const [itinerary, packingList] = await Promise.all([ itineraryFlow({ destination, durationDays: input.durationDays }), packingFlow({ destination, season: input.season }), ]);
// 4. Return the combined result return { destination: selection.city, reason: selection.reason, itinerary, packingList, }; },);And that’s our Lucky Planner flow done! We now have a complete suite of flows that work together to provide a seamless travel planning experience.
Step 5: Testing the Flows using Genkit Developer UI
Section titled “Step 5: Testing the Flows using Genkit Developer UI”First, we need to start the Genkit Developer UI. If you followed the Getting Started guide, you should have it installed globally and your project set up correctly. If not, please refer to the guide.
Please ensure you have your GENKIT_API_KEY environment variable set up with your Genkit API key or in your
.envfile, if you are using our starter kit.
Next, we need to run the Genkit Developer UI in our project directory, like so:
npx genkit start -o -- npx tsx ./src/index.tsThis command does two things:
- It starts the Genkit Developer UI and opens it in your default web browser.
- It runs your Genkit project using
tsx, which allows us to use TypeScript directly without compiling it first.Make sure to replace
./src/index.tswith the actual path to your project entry file if it’s different.
Once the Developer UI is running, you should see your defined flows listed in the sidebar. You can click on each flow to test them individually by providing the required inputs.
Step 6: Serving the Flows over HTTP
Section titled “Step 6: Serving the Flows over HTTP”In the current setup, our flows are defined but not yet accessible over HTTP, so
they can’t be called from a frontend or external application. To make our flows
accessible, we need to set up an Express server using the @genkit-ai/express
plugin. And then we can use the startFlowServer function to serve our flows, like so:
import { startFlowServer } from '@genkit-ai/express';
// Our existing flow definitions go here...
startFlowServer({ // Register all our flows here flows: [explorerFlow, itineraryFlow, packingFlow, iAmFeelingLuckyFlow], // Specify the port to run the server on port: 3000, // Enable CORS for cross-origin requests cors: true,});And that’s it! You now have a robust multi-flow backend built with Genkit, featuring structured outputs, streaming, and flow orchestration. You can now call these flows from your frontend application or any HTTP client.
# Test the Destination Explorer flow on localhost:3000, using CURLcurl --request POST \ --url http://localhost:3000/destinationExplorer \ --header 'content-type: application/json' \ --data '{ "data": { "vibe": "adventure", "budget": "cheap" }}'
# Test the Itinerary Builder flow on localhost:3000, using CURLcurl --request POST \ --url http://localhost:3000/itineraryBuilder \ --header 'content-type: application/json' \ --data '{ "data": { "destination": "Nairobi", "durationDays": 5 }}'
# Test the Packing Assistant flow on localhost:3000, using CURLcurl --request POST \ --url http://localhost:3000/packingAssistant \ --header 'content-type: application/json' \ --data '{ "data": { "destination": "Nairobi", "season": "summer" }}'
# Test the Lucky Planner flow on localhost:3000, using CURLcurl --request POST \ --url http://localhost:3000/iAmFeelingLuckyPlanner \ --header 'content-type: application/json' \ --data '{ "data": { "vibe": "adventure", "budget": "cheap", "season": "summer", "durationDays": 5 }}'Conclusion
Section titled “Conclusion”In this recipe, we built a comprehensive AI Travel Planner Suite using Genkit. We demonstrated how to define multiple specialized flows, enforce structured data outputs, implement streaming responses, and orchestrate flows to create complex features. This approach promotes modularity, reusability, and maintainability in your Genkit applications.
To further enhance this suite, consider adding more features such as:
- Integrating real-time data sources (e.g., weather, events) into the itinerary. TIP: You can call external APIs from within your flow handler functions to fetch real-time data.
- Adding user preferences for activities and accommodations.
🚀 Get the Complete Code
This recipe includes a full GitHub repository with working code, setup instructions, and examples. Unlock access to build faster and support this cookbook!