Complete guide to building your first conversational AI agent in under 30 minutes.
By the end of this guide, you'll have a working AI agent that can:
- ✅ Understand natural language queries
- ✅ Extract structured data from conversations
- ✅ Maintain context across multiple turns
- ✅ Use tools to perform actions
- ✅ Handle complex conversation flows
- ✅ Execute multiple steps in a single LLM call (multi-step execution)
Time estimate: 15-30 minutes
- Node.js 18+ or Bun 1.0+
- API key for an AI provider (Gemini, OpenAI, or Anthropic)
- Redis (for session persistence)
- Database (PostgreSQL, MySQL, etc. for advanced persistence)
# Create a new directory
mkdir my-first-agent && cd my-first-agent
# Initialize with your package manager
bun init -y # or npm init -y
# Install @falai/agent
bun add @falai/agentCreate a .env file:
# Choose one AI provider
GEMINI_API_KEY=your_gemini_api_key_here
# OPENAI_API_KEY=your_openai_api_key_here
# ANTHROPIC_API_KEY=your_anthropic_api_key_hereCreate index.ts:
import { Agent, GeminiProvider } from "@falai/agent";
// Create AI provider
const provider = new GeminiProvider({
apiKey: process.env.GEMINI_API_KEY!,
model: "models/gemini-2.5-flash",
});
// Create your agent
const agent = new Agent({
name: "MyFirstAgent",
description: "A helpful AI assistant",
provider,
});
// Create a simple route with basic string condition
const generalRoute = agent.createRoute({
title: "General Help",
description: "Answers general questions",
when: ["User needs help or asks a question"], // Simple string condition
initialStep: {
prompt: "How can I help you today?",
},
});
// Test your agent
async function main() {
const response = await agent.respond("Hello! Can you tell me about TypeScript?");
console.log("🤖 Agent:", response.message);
}
main();# Run with Bun
bun run index.ts
# Or with Node.js + TypeScript
npx tsx index.tsCongratulations! 🎉 You now have a working AI agent.
Now let's build an agent that intelligently collects structured data:
// Define the data you want to collect
interface BookingData {
destination: string;
travelDate: string;
travelers: number;
budget: number;
}import { Agent, GeminiProvider } from "@falai/agent";
// Define data interface first
interface BookingData {
destination?: string;
travelDate?: string;
travelers?: number;
budget?: number;
estimatedPrice?: number;
availabilityChecked?: boolean;
}
// Create agent with centralized data schema first
const agent = new Agent<{}, BookingData>({
name: "TravelAgent",
provider: new GeminiProvider({
apiKey: process.env.GEMINI_API_KEY!,
}),
// Agent-level schema defines all possible data fields
schema: {
type: "object",
properties: {
destination: { type: "string", description: "Travel destination" },
travelDate: { type: "string", description: "Travel date" },
travelers: { type: "number", minimum: 1, maximum: 10 },
budget: { type: "number", description: "Budget in USD" },
},
required: ["destination", "travelers"],
},
// Agent-level data validation and enrichment
hooks: {
onDataUpdate: async (data, previousData) => {
// Auto-set budget range based on travelers
if (data.travelers && !data.budget) {
data.budget = data.travelers * 500; // Default $500 per person
}
return data;
}
}
});
// Create booking tool using unified Tool interface - simple return value
agent.addTool({
id: "check_availability",
name: "Availability Checker",
description: "Check travel availability and pricing",
parameters: {
type: "object",
properties: {},
},
handler: async ({ context, data, updateData }) => {
// Simulate availability check using collected data
const available = Math.random() > 0.2;
const price = Math.floor(Math.random() * 1000) + 500;
// Update data with pricing information using helper method
await updateData({
estimatedPrice: price,
availabilityChecked: true
});
// Return simple string value - unified interface supports both simple and complex returns
return available
? `✅ Available! Estimated cost: $${price} for ${data.travelers} travelers to ${data.destination}`
: "❌ Not available for those dates. Please try different dates.";
},
};
// Routes specify required fields instead of schemas
const bookingRoute = agent.createRoute({
title: "Travel Booking",
description: "Help users book travel",
when: ["User wants to book travel"],
requiredFields: ["destination", "travelDate", "travelers"], // Required for completion
optionalFields: ["budget"], // Nice to have but not required
initialStep: {
prompt: "I'd love to help you book a trip! Where would you like to go?",
collect: ["destination"],
},
});
// Build conversation flow with agent-level data awareness
const askDate = bookingRoute.initialStep.nextStep({
prompt: "When would you like to travel?",
collect: ["travelDate"],
requires: ["destination"], // Must have destination from agent data
skipIf: (data) => !!data.travelDate, // Skip if already collected
});
const askTravelers = askDate.nextStep({
prompt: "How many people are traveling?",
collect: ["travelers"],
requires: ["destination"], // Prerequisites from agent data
skipIf: (data) => data.travelers !== undefined,
});
const askBudget = askTravelers.nextStep({
prompt: "What's your budget for this trip?",
collect: ["budget"],
requires: ["destination", "travelers"],
skipIf: (data) => data.budget !== undefined,
});
const checkAndBook = askBudget.nextStep({
prompt: "Let me check availability for your trip.",
tools: ["check_availability"], // Reference tool by ID
requires: ["destination", "travelers"], // Minimum data needed
});async function testBookingAgent() {
// Create agent with automatic session management and agent-level schema
const sessionAgent = new Agent<{}, BookingData>({
name: "TravelAgent",
provider: new GeminiProvider({
apiKey: process.env.GEMINI_API_KEY!,
}),
sessionId: "user-alice", // Automatically manages this session
// Same agent-level schema and configuration
schema: agent.schema,
hooks: agent.hooks
});
// Add the same tool to the session agent
sessionAgent.addTool({
id: "check_availability",
name: "Availability Checker",
description: "Check travel availability and pricing",
handler: async ({ context, data, updateData }) => {
const available = Math.random() > 0.2;
const price = Math.floor(Math.random() * 1000) + 500;
await updateData({ estimatedPrice: price, availabilityChecked: true });
return {
data: available
? `✅ Available! Estimated cost: $${price} for ${data.travelers} travelers to ${data.destination}`
: "❌ Not available for those dates. Please try different dates.",
};
},
});
// Copy the route to the session agent
sessionAgent.createRoute(bookingRoute.options);
// User provides partial information - simple message API
const response1 = await sessionAgent.respond("I want to go to Paris");
console.log("Bot:", response1.message);
console.log("Agent data:", sessionAgent.getCollectedData()); // Agent-level data access
// User provides more details - session automatically maintained
const response2 = await sessionAgent.respond("Next Friday, 2 people, $2000 budget");
console.log("Bot:", response2.message);
console.log("Final agent data:", sessionAgent.getCollectedData());
// Check route completion
console.log("Route complete:", bookingRoute.isComplete(sessionAgent.getCollectedData()));
console.log("Progress:", Math.round(bookingRoute.getCompletionProgress(sessionAgent.getCollectedData()) * 100) + "%");
}
testBookingAgent();
// Advanced: Show different tool creation approaches
function demonstrateToolCreationMethods() {
// Method 1: Direct addition (simplest)
agent.addTool({
id: "simple_search",
description: "Basic search functionality",
handler: async ({ context, data }, args) => `Found results for: ${args.query}`
});
// Method 2: Registry for reuse
agent.tool.register({
id: "reusable_validator",
description: "Reusable validation tool",
handler: async ({ context, data }, args) => ({
data: "Validation complete",
success: true,
contextUpdate: { lastValidated: new Date() }
})
});
// Method 3: Pattern helpers
const enrichmentTool = agent.tool.createDataEnrichment({
id: "enrich_booking",
fields: ['destination', 'travelers'],
enricher: async (context, data) => ({
bookingCode: `${data.destination?.slice(0,3).toUpperCase()}-${data.travelers}`,
estimatedDuration: data.destination === 'Paris' ? '8 hours' : '12 hours'
})
});
agent.tool.register(enrichmentTool);
}
demonstrateToolCreationMethods();
// Advanced: Tools as Step Lifecycle Hooks
function demonstrateLifecycleHooks() {
// Create tools for step lifecycle
agent.addTool({
id: "prepare_booking",
description: "Prepare booking context",
handler: async ({ context, data, updateContext }) => {
// Enrich context before AI response
await updateContext({
bookingStartTime: new Date().toISOString(),
userTier: 'premium'
});
return "Booking preparation complete"; // Simple return
}
});
agent.addTool({
id: "finalize_booking",
description: "Finalize booking process",
handler: async ({ context, data }) => {
// Process after AI response
const confirmationId = await bookingService.reserve(data);
return {
data: `Booking reserved with ID: ${confirmationId}`,
success: true,
contextUpdate: { lastBookingId: confirmationId }
}; // Complex ToolResult
}
});
// Use tools in step lifecycle
const bookingStep = agent.createRoute({
title: "Hotel Booking with Lifecycle",
steps: [{
id: "process_booking",
prompt: "Let me process your booking...",
prepare: "prepare_booking", // Tool executes before AI response
finalize: "finalize_booking", // Tool executes after AI response
collect: ["hotelName", "checkInDate"]
}]
});
console.log("Lifecycle hooks configured with tools");
}
demonstrateLifecycleHooks();Notice how the agent:
- ✅ Automatically extracted destination from "Paris"
- ✅ Understood "Next Friday, 2 people, $2000 budget" as structured data
- ✅ Skipped asking for already-known information
- ✅ Used the ToolManager API to create and execute tools with simplified context
- ✅ Batched multiple steps together when data was available
@falai/agent automatically batches multiple steps together when their data requirements are satisfied, reducing LLM calls and improving conversation flow.
When a user provides information that satisfies multiple steps, they execute together:
// Traditional approach: 3 separate turns
// Turn 1: "Which hotel?" → "Grand Hotel"
// Turn 2: "What date?" → "Friday"
// Turn 3: "How many guests?" → "2"
// With multi-step execution: 1 turn
const response = await agent.respond("Book Grand Hotel for 2 on Friday");
// All 3 steps execute in a single LLM call!
console.log(response.executedSteps);
// [
// { id: "ask-hotel", routeId: "booking" },
// { id: "ask-date", routeId: "booking" },
// { id: "ask-guests", routeId: "booking" }
// ]
console.log(response.stoppedReason);
// "route_complete"- Fewer LLM Calls - Multiple steps in one call reduces costs
- Better UX - Users don't repeat information they've already provided
- Faster Responses - Less back-and-forth means quicker completion
- Automatic - No code changes needed, works with existing routes
// Create a booking route with multiple steps
const bookingRoute = agent.createRoute({
title: "Hotel Booking",
requiredFields: ["hotel", "date", "guests"],
initialStep: {
prompt: "Which hotel?",
collect: ["hotel"],
skipIf: (data) => !!data.hotel,
},
});
const askDate = bookingRoute.initialStep.nextStep({
prompt: "What date?",
collect: ["date"],
skipIf: (data) => !!data.date,
});
const askGuests = askDate.nextStep({
prompt: "How many guests?",
collect: ["guests"],
skipIf: (data) => data.guests !== undefined,
});
// User provides all info at once
const response = await agent.respond(
"I want to book the Grand Hotel for 2 people next Friday"
);
// Pre-extraction captures all data
// All steps execute in one batch
// Route completes immediately!
console.log(response.message);
// "Perfect! I've booked the Grand Hotel for 2 guests on Friday."
console.log(response.isRouteComplete);
// trueThe response includes information about what executed:
const response = await agent.respond("Book for 2 guests");
// Which steps ran
console.log(response.executedSteps);
// [{ id: "ask-guests", routeId: "booking" }]
// Why execution stopped
console.log(response.stoppedReason);
// "needs_input" - waiting for hotel and date
// Or if complete:
// "route_complete" - all steps finished
// "end_route" - reached END_ROUTE markerLearn how to create sophisticated routing logic with the new ConditionTemplate system:
Perfect for AI-driven routing decisions:
// String conditions provide context to AI for routing
const supportRoute = agent.createRoute({
title: "Customer Support",
when: "User needs help or has a problem", // AI understands intent
initialStep: {
prompt: "I'm here to help! What can I assist you with?",
},
});
const feedbackRoute = agent.createRoute({
title: "Feedback Collection",
when: "User wants to leave feedback or a review", // AI context
initialStep: {
prompt: "I'd love to hear your feedback!",
},
});For programmatic logic and precise control:
interface UserContext {
userType: 'free' | 'premium' | 'enterprise';
loginCount: number;
lastActivity: Date;
}
const agent = new Agent<UserContext>({
name: "SmartAgent",
provider: new GeminiProvider({ apiKey: process.env.GEMINI_API_KEY! }),
context: {
userType: 'free',
loginCount: 1,
lastActivity: new Date(),
},
});
// Function-only conditions for precise control
const premiumRoute = agent.createRoute({
title: "Premium Features",
when: (ctx) => ctx.context?.userType === 'premium', // Programmatic check
initialStep: {
prompt: "Welcome to premium features! What would you like to explore?",
},
});
const onboardingRoute = agent.createRoute({
title: "User Onboarding",
when: (ctx) => ctx.context?.loginCount <= 3, // New user logic
initialStep: {
prompt: "Welcome! Let me show you around.",
},
});Combine AI understanding with programmatic precision:
interface SupportContext {
userTier: 'basic' | 'premium' | 'enterprise';
supportTickets: number;
accountAge: number; // days
}
interface SupportData {
issueType?: 'technical' | 'billing' | 'general';
priority?: 'low' | 'medium' | 'high';
previousAttempts?: number;
}
const agent = new Agent<SupportContext, SupportData>({
name: "SupportAgent",
provider: new GeminiProvider({ apiKey: process.env.GEMINI_API_KEY! }),
});
// Mixed conditions: AI context + programmatic logic
const escalationRoute = agent.createRoute({
title: "Escalated Support",
when: [
"User is frustrated or needs urgent help", // AI context
(ctx) => ctx.data?.previousAttempts > 2, // Programmatic check
(ctx) => ctx.context?.userTier === 'enterprise' // Context check
],
skipIf: [
"Support system is under maintenance", // AI context
(ctx) => new Date().getHours() < 9 || new Date().getHours() > 17 // Outside hours
],
initialStep: {
prompt: "I understand this is urgent. Let me connect you with our senior support team.",
},
});
// Advanced step conditions
const technicalStep = escalationRoute.initialStep.nextStep({
when: [
"User has a technical issue that needs expert help", // AI context
(ctx) => ctx.data?.issueType === 'technical' // Data check
],
skipIf: (ctx) => ctx.data?.priority === 'low', // Skip low priority technical issues
prompt: "Let me get our technical expert to help you.",
collect: ["issueDescription"]
});Exclude routes from consideration based on conditions:
const paymentRoute = agent.createRoute({
title: "Payment Processing",
when: ["User wants to make a payment or purchase"],
skipIf: [
"Payment system is temporarily unavailable", // AI context
(ctx) => ctx.context?.paymentSystemDown === true, // System check
(ctx) => ctx.data?.paymentBlocked === true // User-specific block
],
initialStep: {
prompt: "I'll help you with your payment.",
},
});
const maintenanceRoute = agent.createRoute({
title: "Maintenance Notice",
when: "User asks about system issues or downtime",
skipIf: (ctx) => ctx.context?.maintenanceMode !== true, // Only show during maintenance
initialStep: {
prompt: "We're currently performing scheduled maintenance. Service will resume shortly.",
},
});async function testConditions() {
// Test with different contexts
const basicUser = { userTier: 'basic', supportTickets: 1, accountAge: 30 };
const premiumUser = { userTier: 'premium', supportTickets: 5, accountAge: 365 };
// Basic user - should get standard support
const response1 = await agent.respond("I need help", {
contextOverride: basicUser
});
console.log("Basic user route:", response1.session?.currentRoute?.title);
// Premium user with multiple tickets - should get escalated support
const response2 = await agent.respond("I'm having issues again", {
contextOverride: premiumUser
});
console.log("Premium user route:", response2.session?.currentRoute?.title);
}
testConditions();Key Benefits:
- ✅ Simple strings for AI-driven routing decisions
- ✅ Functions for precise programmatic control
- ✅ Arrays to combine both approaches
- ✅ Route skipIf for dynamic exclusion
- ✅ Context access in all condition types
Make your agent remember conversations across sessions:
import { MemoryAdapter } from "@falai/agent";
const agent = new Agent({
name: "PersistentAgent",
provider: new GeminiProvider({
apiKey: process.env.GEMINI_API_KEY!,
}),
// Add persistence
persistence: {
adapter: new MemoryAdapter(), // Or RedisAdapter, PrismaAdapter, etc.
},
});
// Create agent with automatic session management and persistence
const persistentAgent = new Agent({
name: "PersistentAgent",
provider: new GeminiProvider({
apiKey: process.env.GEMINI_API_KEY!,
}),
persistence: {
adapter: new MemoryAdapter(), // Or RedisAdapter, PrismaAdapter, etc.
},
sessionId: "user-123", // Automatically loads or creates this session
});
// Sessions are automatically saved and restored
const response1 = await persistentAgent.respond("Hello, my name is Alice");
// Session data automatically persisted
const response2 = await persistentAgent.respond("What's my name?");
console.log(response2.message); // Agent remembers: "Your name is Alice"- Schema-Driven Extraction - Advanced data collection patterns
- Session Management - Multi-turn conversations
- Modern Streaming API - Streaming with the modern API
- Completion Transitions - Route completion and transitions
- Knowledge-Based Agent - Domain-specific knowledge bases
- Route Lifecycle Hooks - Custom route behavior
- Server Session Management - Server-side sessions
- Database Persistence - Custom storage adapters
- Streaming Responses - Real-time UX
"API key not found"
# Make sure your .env file exists and has the correct variable
echo "GEMINI_API_KEY=your_key_here" > .env"Module not found"
# Reinstall dependencies
rm -rf node_modules package-lock.json
bun install # or npm install"Type errors"
// Make sure you're using TypeScript 5.3+
npx tsc --version
// Or use tsx for running TypeScript directly
npx tsx your-file.ts"Agent not responding"
// Check your API key is valid
curl "https://generativelanguage.googleapis.com/v1beta/models?key=YOUR_KEY"
// Verify your provider configuration
console.log("Provider:", agent.options.provider.name);- 📖 Full Documentation - Complete API reference
- 💬 Examples Directory - Working code samples
- 🐛 GitHub Issues - Report bugs
- 💡 Discussions - Ask questions
You now have the foundation to build sophisticated AI agents. The framework is designed to scale with your needs - from simple chatbots to complex, data-driven conversational applications.
What's next? Explore the examples directory to see more advanced patterns, or dive into the API documentation for detailed method references.
Happy building! 🚀