Build a working AI-powered chat app in 15 minutes. By the end, you will have a Next.js app where users can ask about the weather and the AI calls your custom tool to answer.
Before you start, make sure you have:
npx create-next-app@latestWhat you should know: This guide assumes familiarity with React components, hooks (useState), and basic TypeScript. If you know how to build a form in React, you have everything you need. We will explain Zod and Glove-specific concepts as they come up.
Three ideas power every Glove app. Refer back to these if anything later in the guide feels unfamiliar:
Tools are capabilities your app can perform — things like “get weather” or “search products.” Each tool has a name, a description (so the AI knows what it does), an input schema (defined with Zod, a validation library), and a do function that runs when the AI calls it.
The display stack lets tools show UI to the user. A tool can push a React component onto a stack that your app renders. There are two modes: pushAndWait pauses the tool until the user responds (like a confirmation dialog), while pushAndForget shows UI and lets the tool keep running (like showing a data card). We won't use the display stack in this guide, but you can learn about it in Concepts.
The agent loop is the engine that drives everything. When a user sends a message, the AI reads the available tools, calls whichever tools it needs, reads the results, and either responds or calls more tools. This loop repeats until the AI has enough information to give a final answer.
Install Glove and Zod (the validation library Glove uses for tool inputs):
pnpm add glove-core glove-react glove-next zodOr with npm:
npm install glove-core glove-react glove-next zodHere is what each package does:
glove-react — React hooks and components for your UI (API reference)glove-next — server handler that connects to AI providers (API reference)glove-core — the runtime engine (included as a dependency of glove-react)zod — validates tool inputs at runtimeCreate an API route that handles chat requests. The createChatHandler function from glove-next does this in one line — it connects to your AI provider and streams responses back:
import { createChatHandler } from "glove-next";
// This creates a POST endpoint that streams AI responses
export const POST = createChatHandler({
provider: "openai", // or "anthropic"
model: "gpt-4o-mini", // or "claude-sonnet-4-20250514"
});Set your API key in .env.local at the root of your project:
OPENAI_API_KEY=sk-...Using Anthropic? Change to provider: "anthropic" and model: "claude-sonnet-4-20250514", then set ANTHROPIC_API_KEY instead. See all supported providers.
Create a GloveClient with a system prompt and tools. This is where you tell the AI what your app can do:
import { GloveClient } from "glove-react";
import { z } from "zod";
export const gloveClient = new GloveClient({
// Where to send chat requests (the route you created above)
endpoint: "/api/chat",
// Instructions for the AI — what role should it play?
systemPrompt: "You are a helpful weather assistant.",
// Tools — capabilities the AI can use
tools: [
{
name: "get_weather",
description: "Get the current weather for a city.",
// Zod schema: defines what input the AI must provide
// z.object() creates an object schema, z.string() validates a string
inputSchema: z.object({
city: z.string().describe("The city to get weather for"),
}),
// This runs when the AI decides to use this tool
async do(input) {
// In a real app, you'd call a weather API here
return {
city: input.city,
temperature: "72°F",
condition: "Sunny",
};
},
},
],
});The inputSchema tells the AI what arguments the tool expects, and Zod validates them at runtime. The do function runs when the AI calls this tool — its return value is sent back to the AI as the tool result.
Wrap your app with GloveProvider so any component can access the agent. Create a client component for the provider:
"use client";
import { GloveProvider } from "glove-react";
import { gloveClient } from "@/lib/glove";
export function Providers({ children }: { children: React.ReactNode }) {
return <GloveProvider client={gloveClient}>{children}</GloveProvider>;
}Then wrap your root layout with it:
import { Providers } from "./providers";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Use the useGlove hook to get the conversation state and a function to send messages:
"use client";
import { useState } from "react";
import { useGlove } from "glove-react";
export default function Chat() {
// useGlove gives you everything you need:
// - timeline: array of messages and tool calls
// - streamingText: text being streamed right now
// - busy: true while the AI is thinking
// - sendMessage: send a user message
const { timeline, streamingText, busy, sendMessage } = useGlove();
const [input, setInput] = useState("");
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!input.trim() || busy) return;
sendMessage(input.trim());
setInput("");
}
return (
<div style={{ maxWidth: 600, margin: "2rem auto", fontFamily: "sans-serif" }}>
<h1>Weather Chat</h1>
{/* Render the conversation */}
<div>
{timeline.map((entry, i) => {
// Each entry has a "kind" that tells you what type it is
if (entry.kind === "user") {
return (
<div key={i} style={{ margin: "1rem 0" }}>
<strong>You:</strong> {entry.text}
</div>
);
}
if (entry.kind === "agent_text") {
return (
<div key={i} style={{ margin: "1rem 0" }}>
<strong>Assistant:</strong> {entry.text}
</div>
);
}
if (entry.kind === "tool") {
return (
<div
key={i}
style={{
margin: "0.5rem 0",
padding: "0.5rem",
background: "#f0f0f0",
borderRadius: 4,
fontSize: "0.875rem",
}}
>
Tool: <strong>{entry.name}</strong> — {entry.status}
</div>
);
}
return null;
})}
</div>
{/* Show text as it streams in */}
{streamingText && (
<div style={{ margin: "1rem 0", opacity: 0.7 }}>
<strong>Assistant:</strong> {streamingText}
</div>
)}
{/* Message input */}
<form onSubmit={handleSubmit} style={{ display: "flex", gap: "0.5rem", marginTop: "1rem" }}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask about the weather..."
disabled={busy}
style={{ flex: 1, padding: "0.5rem" }}
/>
<button type="submit" disabled={busy}>
Send
</button>
</form>
</div>
);
}pnpm devOpen http://localhost:3000 and try asking “What's the weather in Tokyo?”. The AI will call your get_weather tool and respond with the result.
You have a working agent. Here is where to go next: