Versión en Español: Este codelab también está disponible en Español.
In the era of generative AI, building chatbots that only talk is no longer enough. The current market demands AI agents: systems capable of reasoning, using external tools, and following business rules.
In this hands-on workshop, we will build a Smart Retail assistant. We will use Genkit, Google's framework designed to bring AI to production environments with the security and scalability that characterizes the Firebase ecosystem.
Throughout this codelab, you will learn to master using different LLMs so they act as an expert and reliable shopping assistant.
A smart assistant that:
Source Code: You can find the final solution at workshop-genkit-agents
First, let's prepare the ground.
Create a folder:
mkdir workshop-genkit-agents-101 && cd workshop-genkit-agents-101
Initialize the Node.js project, install TypeScript, and create the base structure:
# Initialize a new Node.js project
npm init -y
npm pkg set type=module
# Install and configure TypeScript
npm install -D typescript tsx
npx tsc --init
# Set up your source directory
mkdir src
touch src/index.ts
First, install the Genkit CLI globally. This will give you access to local development tools, including the developer user interface:
npm install -g genkit-cli
Next, add the following packages to your project:
npm install genkit @genkit-ai/google-genai
genkit — Provides core Genkit capabilities.@genkit-ai/google-genai — Provides access to Google AI Gemini models.Genkit can work with various model providers. This workshop uses the Gemini API, which offers a generous free tier and does not require a credit card to get started.
To use it, you will need a Google AI Studio API key:
Once you have your key, configure the GOOGLE_GENAI_API_KEY environment variable:
export GOOGLE_GENAI_API_KEY=<your API key>
A flow is a special Genkit function with built-in observability, type safety, and tool integration.
Update src/index.ts with the following:
import { googleAI } from '@genkit-ai/google-genai';
import { genkit, z } from 'genkit';
const ai = genkit({
plugins: [googleAI()],
model: googleAI.model('gemini-2.5-flash'),
});
export const basicScoutFlow = ai.defineFlow(
{
name: 'basicScoutFlow',
},
async (productType) => {
const { text } = await ai.generate(
`You are a retail expert. Give me the details of this product: ${productType}`
);
return text;
}
);
This code example:
gemini-2.5-flash model.The Developer UI is a local tool to test and inspect Genkit components, such as flows, through a visual interface.
Start the Developer UI
The Genkit CLI is required to run the Developer UI. If you followed the installation steps above, you already have it installed.
Run the following command from the root of your project:
genkit start -- npx tsx --watch src/index.ts
Open http://localhost:4000 in your browser.
In the sidebar, click on Flows and select basicScoutFlow.
Type a product (e.g. "Iphone 15") and hit Run.
Add this new flow to your src/index.ts file:
const ProductDetailsSchema = z.object({
name: z.string(),
brand: z.string(),
features: z.array(z.string()),
recommendation_text: z.string()
});
export const basicScoutFlow = ai.defineFlow(
{
name: 'basicScoutFlow',
inputSchema: z.string(),
outputSchema: ProductDetailsSchema,
},
async (productName) => {
const { output } = await ai.generate({
prompt: `You are a retail expert. Give me the details of this product: ${productName}`,
output: { schema: ProductDetailsSchema },
});
if (!output) throw new Error("The AI failed to generate the correct format.");
return output;
}
);
output.brand with auto-complete)."recommendation_text" will suggest whether the product suits you or not.Go back to the Developer UI (localhost:4000). You'll see that structuredScoutFlow now appears. Upon execution, you will notice that the response is no longer a paragraph, but a clean JSON object ready to be used in a production application.
Language models (LLMs) have a limit: their knowledge only goes up to their training date. They do not know what is in your warehouse today or what price a product has this very second.
Tool Calling allows the AI to use TypeScript functions to interact with external APIs. The AI does not just "talk", it now "acts".
Let's create a tool that performs an actual search in the DummyJSON catalog. Genkit uses the description you write so the AI understands when to use this function.
Add this code to src/index.ts:
export const searchProductTool = ai.defineTool(
{
name: 'searchProduct',
description: 'Searches for products in the official GenkitStore catalog to get real-time pricing and stock.',
inputSchema: z.object({ query: z.string() }),
outputSchema: z.any(),
},
async (input) => {
const response = await fetch(`https://dummyjson.com/products/search?q=${input.query}`);
const data = await response.json();
return data.products;
}
);
Once the tool is defined, you can open it and test it directly in the Genkit Developer UI (localhost:4000), where it is already listed alongside your flows.
The magic of Genkit is that you do not call the function manually. You hand it over to the model and it, based on the user's question, decides whether it needs to use it.
First, update the ProductDetailsSchema you defined earlier so it accepts a list of products:
const ProductDetailsSchema = z.array(z.object({
name: z.string(),
brand: z.string(),
features: z.array(z.string()),
recommendation_text: z.string()
}));
Add this new flow to src/index.ts:
export const agentFlow = ai.defineFlow(
{
name: 'agentFlow',
inputSchema: z.string(),
outputSchema: ProductDetailsSchema,
},
async (userInput) => {
const { output } = await ai.generate({
prompt: userInput,
tools: [searchProductTool],
output: { schema: ProductDetailsSchema },
});
if (!output) throw new Error("The AI failed to generate the correct format.");
return output;
}
);
A business rule is a constraint imposed by the developer that the AI cannot ignore. In this step, you will learn to intercept API data and apply business logic so the assistant is responsible and reliable.
First, update ProductDetailsSchema to include the new fields: price, rating, warning, and availability:
const ProductDetailsSchema = z.array(z.object({
name: z.string(),
brand: z.string(),
price: z.number(),
rating: z.number(),
features: z.array(z.string()),
recommendation_text: z.string(),
warning: z.string().optional(),
availability: z.enum(['In Stock', 'Low Stock', 'Out of Stock']).optional()
}));
Let's modify our flow so it analyzes the rating field. If the rating is lower than 4.0, we will force the AI to include a warning message.
Add this flow to src/index.ts:
export const guardedScoutFlow = ai.defineFlow(
{
name: 'guardedScoutFlow',
inputSchema: z.string(),
outputSchema: ProductDetailsSchema,
},
async (userInput) => {
const { output } = await ai.generate({
prompt: userInput,
tools: [searchProductTool],
output: { schema: ProductDetailsSchema },
system: `
You are an honest shopping assistant.
GOLDEN RULE: If a product has a rating lower than 3.0,
you must fill out the 'warning' field stating that customer
satisfaction is low.
`,
});
if (!output) throw new Error("Error generating response");
return output;
}
);
Sometimes, the rule isn't just a message but filtering out information before the AI sees it. If a product exceeds the user's budget, it's better to exclude it so the AI doesn't recommend it.
Modify the body of your searchProductTool to add this filter:
async (input) => {
const response = await fetch(`https://dummyjson.com/products/search?q=${input.query}`);
const data = await response.json();
// Pricing rule: Filter out products that exceed the maximum budget
const affordableProducts = data.products.filter((p: any) => p.price <= 1900);
return affordableProducts;
}
guardedScoutFlow asking for: ""I am looking for a laptop""."recommendation_text" field generates a product recommendation for you.system prompt to lay down ethical rules for the AI.Have you ever wondered exactly what happens between the user asking a question and the AI responding? In traditional development, we console.log or use a debugger. In Genkit, we have Tracing.
It is a detailed view (like Chrome's "Network Tab") that records every step of a Flow:
To see an interesting Trace, we need a flow that does multiple things. Make sure your Developer UI is running:
genkit start -- npx tsx --watch src/index.ts
guardedScoutFlow flow."I am looking for a laptop under 3000 USD that has good reviews".Once the flow completes:
So far, we have run our agent through the Developer UI. In a professional environment, we need our flow to be accessible via a secure HTTPS URL, so it can be consumed by an Angular, React, or Flutter app.
If you do not yet have a Firebase project with Cloud Functions in TypeScript, follow these steps:
firebase login
firebase login --reauth # alternative, if needed
firebase login --no-localhost # if running in a remote shell
firebase init
The setup wizard will ask you a series of questions. These are the recommended answers:
Question | Answer |
Which Firebase features do you want to set up? | Functions: Configure a Cloud Functions directory and its files |
Please select an option | Use an existing project |
Select a default Firebase project | Select your project (e.g. |
What language would you like to use? | TypeScript |
Do you want to use ESLint? | No |
Do you want to install dependencies with npm now? | Yes |
Would you like to install agent skills for Firebase? | No |
Once finished, you will see the Firebase initialization complete! message, and the functions/ folder will have been created with all the required structure.
Update the end of your functions/src/index.ts file:
import { googleAI } from '@genkit-ai/google-genai';
import { genkit, z } from 'genkit';
import { defineSecret } from "firebase-functions/params";
import { onCallGenkit } from "firebase-functions/https";
// enableFirebaseTelemetry();
const googleAIapiKey = defineSecret("GOOGLE_GENAI_API_KEY");
const ai = genkit({
plugins: [googleAI()],
model: googleAI.model('gemini-2.5-flash'),
});
const ProductDetailsSchema = z.array(z.object({
name: z.string(),
brand: z.string(),
price: z.number(),
rating: z.number(),
features: z.array(z.string()),
recommendation_text: z.string(),
warning: z.string().optional(),
availability: z.enum(['In Stock', 'Low Stock', 'Out of Stock']).optional()
}));
export const basicScoutFlow = ai.defineFlow(
{
name: 'basicScoutFlow',
inputSchema: z.string(),
outputSchema: ProductDetailsSchema,
},
async (productName) => {
const { output } = await ai.generate({
prompt: `You are a retail expert. Give me the details of this product: ${productName}`,
output: { schema: ProductDetailsSchema },
});
if (!output) throw new Error("The AI failed to generate the correct format.");
return output;
}
);
export const generateBasicScoutFlow = onCallGenkit(
{
cors: '*',
authPolicy: () => true,
secrets: [googleAIapiKey],
},
basicScoutFlow
);
In production, we must never leave API Keys embedded in the code or in local .env files. We will use the Firebase secret manager so Gemini can authenticate securely.
Run this command in your terminal to upload your Google AI Studio key:
firebase functions:secrets:set GOOGLE_GENAI_API_KEY
Paste your key when the terminal prompts for it.
Edit src/index.ts and append this right after the imports.
import { defineSecret } from 'firebase-functions/params';
const googleAIapiKey = defineSecret('GOOGLE_GENAI_API_KEY');
Make sure you are logged in via firebase login and have selected your project. Then run:
firebase deploy --only functions
Once the deployment finishes, the terminal will give you a URL similar to:
https://us-central1-your-project.cloudfunctions.net/generateBasicScoutFlow
You have progressed from having a local script to a generative AI API with professional orchestration, real database connections (Tools), guaranteed data contracts (Zod), and active business rules.
You have built a robust, typed, and production-ready AI system.
If you have questions or want to share what you've built, feel free to reach out!