Back to articles
Tutorial

Build Your First AI Agent with Next.js and Vercel AI SDK: Complete Tutorial

Step-by-step guide to building a functional AI Agent that can search the web, manage tasks, and send notifications. Includes full code examples and deployment to Vercel.

15 min read

What We Are Building

In this tutorial, we will build a personal productivity agent that can:

  • Search the web for information
  • Create and manage todo items
  • Send email notifications
  • Remember context across conversations
  • By the end, you will have a working agent deployed on Vercel that you can extend with your own tools.

    Prerequisites

  • Node.js 18+
  • A Vercel account
  • OpenAI API key (or use Vercel AI Gateway)
  • Basic Next.js knowledge
  • Step 1: Project Setup

    Create a new Next.js project:

    npx create-next-app@latest ai-agent-demo --typescript --tailwind --app
    cd ai-agent-demo

    Install the AI SDK:

    pnpm add ai @ai-sdk/openai zod

    Step 2: Define Your Tools

    Create `lib/tools.ts`:

    import { tool } from 'ai';
    import { z } from 'zod';
    
    // Web search tool
    export const searchWeb = tool({
      description: 'Search the web for current information. Use this when you need up-to-date data or facts you are not certain about.',
      parameters: z.object({
        query: z.string().describe('The search query'),
      }),
      execute: async ({ query }) => {
        // Using a simple search API (replace with your preferred provider)
        const response = await fetch(
          `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}`,
          {
            headers: {
              'X-Subscription-Token': process.env.BRAVE_SEARCH_API_KEY!,
            },
          }
        );
        const data = await response.json();
        
        // Return top 3 results
        return data.web.results.slice(0, 3).map((r: any) => ({
          title: r.title,
          description: r.description,
          url: r.url,
        }));
      },
    });
    
    // Todo management tool
    export const manageTodo = tool({
      description: 'Create, list, or complete todo items. Use this to help the user manage their tasks.',
      parameters: z.object({
        action: z.enum(['create', 'list', 'complete']).describe('The action to perform'),
        title: z.string().optional().describe('Todo title (for create action)'),
        todoId: z.string().optional().describe('Todo ID (for complete action)'),
      }),
      execute: async ({ action, title, todoId }) => {
        // In production, use a database. For demo, use in-memory store.
        const todos = globalThis.todos || [];
        
        switch (action) {
          case 'create':
            const newTodo = { id: Date.now().toString(), title, completed: false };
            todos.push(newTodo);
            globalThis.todos = todos;
            return { success: true, todo: newTodo };
            
          case 'list':
            return { todos: todos };
            
          case 'complete':
            const todo = todos.find((t: any) => t.id === todoId);
            if (todo) {
              todo.completed = true;
              return { success: true, todo };
            }
            return { success: false, error: 'Todo not found' };
        }
      },
    });
    
    // Email notification tool
    export const sendNotification = tool({
      description: 'Send an email notification to the user. Use sparingly and only when explicitly requested.',
      parameters: z.object({
        subject: z.string().describe('Email subject'),
        body: z.string().describe('Email body content'),
      }),
      execute: async ({ subject, body }) => {
        // Using Resend (replace with your email provider)
        const response = await fetch('https://api.resend.com/emails', {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${process.env.RESEND_API_KEY}`,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            from: 'agent@yourdomain.com',
            to: process.env.USER_EMAIL,
            subject,
            text: body,
          }),
        });
        
        if (response.ok) {
          return { success: true, message: 'Email sent successfully' };
        }
        return { success: false, error: 'Failed to send email' };
      },
    });
    
    // Calculator tool
    export const calculate = tool({
      description: 'Perform mathematical calculations. Use for any math operations.',
      parameters: z.object({
        expression: z.string().describe('Mathematical expression (e.g., "2 + 2", "sqrt(16)", "15% of 200")'),
      }),
      execute: async ({ expression }) => {
        try {
          // Parse and evaluate safely (use mathjs in production)
          const sanitized = expression
            .replace(/sqrt/g, 'Math.sqrt')
            .replace(/pow/g, 'Math.pow')
            .replace(/(\d+)% of (\d+)/g, '($1/100) * $2');
          const result = Function('return ' + sanitized)();
          return { result, expression };
        } catch (error) {
          return { error: 'Could not evaluate expression' };
        }
      },
    });
    
    // Get current date/time tool
    export const getCurrentTime = tool({
      description: 'Get the current date and time. Use when the user asks about today, now, or current time.',
      parameters: z.object({
        timezone: z.string().optional().describe('Timezone (e.g., "America/New_York")'),
      }),
      execute: async ({ timezone }) => {
        const now = new Date();
        const options: Intl.DateTimeFormatOptions = {
          weekday: 'long',
          year: 'numeric',
          month: 'long',
          day: 'numeric',
          hour: '2-digit',
          minute: '2-digit',
          timeZone: timezone || 'UTC',
        };
        return {
          formatted: now.toLocaleString('en-US', options),
          iso: now.toISOString(),
          timezone: timezone || 'UTC',
        };
      },
    });

    Step 3: Create the Agent API Route

    Create `app/api/agent/route.ts`:

    import { streamText } from 'ai';
    import { openai } from '@ai-sdk/openai';
    import {
      searchWeb,
      manageTodo,
      sendNotification,
      calculate,
      getCurrentTime,
    } from '@/lib/tools';
    
    export const maxDuration = 30; // Allow longer execution for multi-step tasks
    
    const systemPrompt = `You are a helpful personal productivity assistant.
    
    Your capabilities:
    - Search the web for current information
    - Manage todo lists (create, list, complete tasks)
    - Send email notifications when requested
    - Perform calculations
    - Tell the current date and time
    
    Guidelines:
    1. Always use tools when you need real-time information - never make up data
    2. For multi-step tasks, think through the steps and execute them one by one
    3. Be concise but helpful in your responses
    4. Only send notifications when explicitly asked
    5. When listing todos, format them nicely
    6. If a tool fails, explain what happened and suggest alternatives
    
    Remember: You are an agent that ACTS, not just answers. When a user asks you to do something, do it using your tools.`;
    
    export async function POST(req: Request) {
      const { messages } = await req.json();
    
      const result = streamText({
        model: openai('gpt-4o'),
        system: systemPrompt,
        messages,
        tools: {
          searchWeb,
          manageTodo,
          sendNotification,
          calculate,
          getCurrentTime,
        },
        maxSteps: 5, // Allow up to 5 sequential tool calls
      });
    
      return result.toDataStreamResponse();
    }

    Step 4: Build the Chat Interface

    Create `app/page.tsx`:

    'use client';
    
    import { useChat } from '@ai-sdk/react';
    import { useState } from 'react';
    
    export default function AgentChat() {
      const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
        api: '/api/agent',
      });
    
      return (
        <div className="flex flex-col h-screen max-w-3xl mx-auto p-4">
          <header className="mb-4">
            <h1 className="text-2xl font-bold">AI Agent Demo</h1>
            <p className="text-gray-600">
              I can search the web, manage todos, send notifications, and more.
            </p>
          </header>
    
          <div className="flex-1 overflow-y-auto space-y-4 mb-4">
            {messages.map((message) => (
              <div
                key={message.id}
                className={`p-4 rounded-lg ${
                  message.role === 'user'
                    ? 'bg-blue-100 ml-12'
                    : 'bg-gray-100 mr-12'
                }`}
              >
                <div className="font-semibold mb-1">
                  {message.role === 'user' ? 'You' : 'Agent'}
                </div>
                <div className="whitespace-pre-wrap">{message.content}</div>
                
                {/* Show tool calls */}
                {message.toolInvocations?.map((tool, i) => (
                  <div key={i} className="mt-2 p-2 bg-yellow-50 rounded text-sm">
                    <div className="font-mono text-yellow-800">
                      Tool: {tool.toolName}
                    </div>
                    {tool.state === 'result' && (
                      <pre className="mt-1 text-xs overflow-auto">
                        {JSON.stringify(tool.result, null, 2)}
                      </pre>
                    )}
                  </div>
                ))}
              </div>
            ))}
            
            {isLoading && (
              <div className="p-4 bg-gray-100 rounded-lg mr-12 animate-pulse">
                Agent is thinking...
              </div>
            )}
          </div>
    
          <form onSubmit={handleSubmit} className="flex gap-2">
            <input
              value={input}
              onChange={handleInputChange}
              placeholder="Ask me anything or give me a task..."
              className="flex-1 p-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
              disabled={isLoading}
            />
            <button
              type="submit"
              disabled={isLoading}
              className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
            >
              Send
            </button>
          </form>
        </div>
      );
    }

    Step 5: Add Environment Variables

    Create `.env.local`:

    OPENAI_API_KEY=your_openai_key
    BRAVE_SEARCH_API_KEY=your_brave_search_key  # Optional
    RESEND_API_KEY=your_resend_key  # Optional
    USER_EMAIL=your@email.com  # For notifications

    Step 6: Test Your Agent

    Start the development server:

    pnpm dev

    Try these prompts:

    1. Simple tool use: "What time is it in Tokyo?"

    2. Multi-step task: "Search for the latest Next.js features and create a todo to learn each one"

    3. Calculation: "What is 15% of 2500?"

    4. Todo management: "Add a todo to review the quarterly report"

    5. Complex request: "Find the current Bitcoin price and if it is above $60,000, send me an email notification"

    Step 7: Add Conversation Memory

    For a production agent, you want to persist conversation history. Update your route:

    import { kv } from '@vercel/kv'; // Add Vercel KV for persistence
    
    export async function POST(req: Request) {
      const { messages, sessionId } = await req.json();
      
      // Load previous context if exists
      const previousContext = await kv.get(`agent:${sessionId}:context`);
      
      const result = streamText({
        model: openai('gpt-4o'),
        system: systemPrompt + (previousContext ? `\n\nPrevious context: ${previousContext}` : ''),
        messages,
        tools: { /* ... */ },
        maxSteps: 5,
        onFinish: async ({ text }) => {
          // Save important context for future conversations
          await kv.set(`agent:${sessionId}:context`, text.slice(0, 500), { ex: 86400 });
        },
      });
    
      return result.toDataStreamResponse();
    }

    Step 8: Deploy to Vercel

    vercel deploy

    Set your environment variables in the Vercel dashboard.

    Advanced: Adding Human-in-the-Loop

    For sensitive actions, require user confirmation:

    export const sendNotification = tool({
      description: 'Send an email notification. ALWAYS ask for confirmation before sending.',
      parameters: z.object({
        subject: z.string(),
        body: z.string(),
        confirmed: z.boolean().describe('Whether the user has confirmed sending'),
      }),
      execute: async ({ subject, body, confirmed }) => {
        if (!confirmed) {
          return {
            requiresConfirmation: true,
            preview: { subject, body },
            message: 'Please confirm you want to send this email.',
          };
        }
        // Actually send the email
        // ...
      },
    });

    Advanced: Error Handling and Retries

    export const searchWeb = tool({
      description: 'Search the web',
      parameters: z.object({ query: z.string() }),
      execute: async ({ query }) => {
        const maxRetries = 3;
        let lastError;
        
        for (let i = 0; i < maxRetries; i++) {
          try {
            const response = await fetch(/* ... */);
            if (!response.ok) throw new Error(`HTTP ${response.status}`);
            return await response.json();
          } catch (error) {
            lastError = error;
            await new Promise(r => setTimeout(r, 1000 * (i + 1))); // Exponential backoff
          }
        }
        
        return {
          error: true,
          message: `Search failed after ${maxRetries} attempts: ${lastError.message}`,
          suggestion: 'Try rephrasing your search query or try again later.',
        };
      },
    });

    What You Built

    You now have a functional AI Agent that:

  • Uses multiple tools to accomplish tasks
  • Handles multi-step reasoning
  • Streams responses for better UX
  • Can be extended with new capabilities
  • Deploys to Vercel's edge network
  • Next Steps

    1. Add more tools: Database queries, file operations, API integrations

    2. Implement RAG: Add vector search for document retrieval

    3. Build specialized agents: Create focused agents for specific domains

    4. Add authentication: Protect your agent with user authentication

    5. Monitor and log: Track agent performance and errors

    The agent paradigm is powerful because it is composable. Each tool you add multiplies the agent's capabilities. Start simple, ship fast, and iterate based on real usage.

    Found this helpful?Share this article with your network to help others discover useful AI insights.