ワンクリックで
convex-agents
// Building AI agents with the Convex Agent component including thread management, tool integration, streaming responses, RAG patterns, and workflow orchestration
// Building AI agents with the Convex Agent component including thread management, tool integration, streaming responses, RAG patterns, and workflow orchestration
Prevent feature creep when building software, apps, and AI-powered products. Use this skill when planning features, reviewing scope, building MVPs, managing backlogs, or when a user says "just one more feature." Helps developers and AI agents stay focused, ship faster, and avoid bloated products.
Guidelines for building production-ready Convex apps covering function organization, query patterns, validation, TypeScript usage, error handling, and the Zen of Convex design philosophy
How to create, structure, and publish self-contained Convex components with proper isolation, exports, and dependency management
Scheduled function patterns for background tasks including interval scheduling, cron expressions, job monitoring, retry strategies, and best practices for long-running tasks
Complete file handling including upload flows, serving files via URL, storing generated files from actions, deletion, and accessing file metadata from system tables
Writing queries, mutations, actions, and HTTP actions with proper argument validation, error handling, internal functions, and runtime considerations
| name | Convex Agents |
| description | Building AI agents with the Convex Agent component including thread management, tool integration, streaming responses, RAG patterns, and workflow orchestration |
| version | 1.0.0 |
| author | Convex |
| tags | ["convex","agents","ai","llm","tools","rag","workflows"] |
Build persistent, stateful AI agents with Convex including thread management, tool integration, streaming responses, RAG patterns, and workflow orchestration.
Before implementing, do not assume; fetch the latest documentation:
npm install @convex-dev/agent ai openai
// convex/agent.ts
import { Agent } from "@convex-dev/agent";
import { components } from "./_generated/api";
import { OpenAI } from "openai";
const openai = new OpenAI();
export const agent = new Agent(components.agent, {
chat: openai.chat,
textEmbedding: openai.embeddings,
});
// convex/threads.ts
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { agent } from "./agent";
// Create a new conversation thread
export const createThread = mutation({
args: {
userId: v.id("users"),
title: v.optional(v.string()),
},
returns: v.id("threads"),
handler: async (ctx, args) => {
const threadId = await agent.createThread(ctx, {
userId: args.userId,
metadata: {
title: args.title ?? "New Conversation",
createdAt: Date.now(),
},
});
return threadId;
},
});
// List user's threads
export const listThreads = query({
args: { userId: v.id("users") },
returns: v.array(v.object({
_id: v.id("threads"),
title: v.string(),
lastMessageAt: v.optional(v.number()),
})),
handler: async (ctx, args) => {
return await agent.listThreads(ctx, {
userId: args.userId,
});
},
});
// Get thread messages
export const getMessages = query({
args: { threadId: v.id("threads") },
returns: v.array(v.object({
role: v.string(),
content: v.string(),
createdAt: v.number(),
})),
handler: async (ctx, args) => {
return await agent.getMessages(ctx, {
threadId: args.threadId,
});
},
});
// convex/chat.ts
import { action } from "./_generated/server";
import { v } from "convex/values";
import { agent } from "./agent";
import { internal } from "./_generated/api";
export const sendMessage = action({
args: {
threadId: v.id("threads"),
message: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
// Add user message to thread
await ctx.runMutation(internal.chat.addUserMessage, {
threadId: args.threadId,
content: args.message,
});
// Generate AI response with streaming
const response = await agent.chat(ctx, {
threadId: args.threadId,
messages: [{ role: "user", content: args.message }],
stream: true,
onToken: async (token) => {
// Stream tokens to client via mutation
await ctx.runMutation(internal.chat.appendToken, {
threadId: args.threadId,
token,
});
},
});
// Save complete response
await ctx.runMutation(internal.chat.saveResponse, {
threadId: args.threadId,
content: response.content,
});
return null;
},
});
Define tools that agents can use:
// convex/tools.ts
import { tool } from "@convex-dev/agent";
import { v } from "convex/values";
import { api } from "./_generated/api";
// Tool to search knowledge base
export const searchKnowledge = tool({
name: "search_knowledge",
description: "Search the knowledge base for relevant information",
parameters: v.object({
query: v.string(),
limit: v.optional(v.number()),
}),
handler: async (ctx, args) => {
const results = await ctx.runQuery(api.knowledge.search, {
query: args.query,
limit: args.limit ?? 5,
});
return results;
},
});
// Tool to create a task
export const createTask = tool({
name: "create_task",
description: "Create a new task for the user",
parameters: v.object({
title: v.string(),
description: v.optional(v.string()),
dueDate: v.optional(v.string()),
}),
handler: async (ctx, args) => {
const taskId = await ctx.runMutation(api.tasks.create, {
title: args.title,
description: args.description,
dueDate: args.dueDate ? new Date(args.dueDate).getTime() : undefined,
});
return { success: true, taskId };
},
});
// Tool to get weather
export const getWeather = tool({
name: "get_weather",
description: "Get current weather for a location",
parameters: v.object({
location: v.string(),
}),
handler: async (ctx, args) => {
const response = await fetch(
`https://api.weather.com/current?location=${encodeURIComponent(args.location)}`
);
return await response.json();
},
});
// convex/assistant.ts
import { action } from "./_generated/server";
import { v } from "convex/values";
import { agent } from "./agent";
import { searchKnowledge, createTask, getWeather } from "./tools";
export const chat = action({
args: {
threadId: v.id("threads"),
message: v.string(),
},
returns: v.string(),
handler: async (ctx, args) => {
const response = await agent.chat(ctx, {
threadId: args.threadId,
messages: [{ role: "user", content: args.message }],
tools: [searchKnowledge, createTask, getWeather],
systemPrompt: `You are a helpful assistant. You have access to tools to:
- Search the knowledge base for information
- Create tasks for the user
- Get weather information
Use these tools when appropriate to help the user.`,
});
return response.content;
},
});
// convex/knowledge.ts
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { agent } from "./agent";
// Add document to knowledge base
export const addDocument = mutation({
args: {
title: v.string(),
content: v.string(),
metadata: v.optional(v.object({
source: v.optional(v.string()),
category: v.optional(v.string()),
})),
},
returns: v.id("documents"),
handler: async (ctx, args) => {
// Generate embedding
const embedding = await agent.embed(ctx, args.content);
return await ctx.db.insert("documents", {
title: args.title,
content: args.content,
embedding,
metadata: args.metadata ?? {},
createdAt: Date.now(),
});
},
});
// Search knowledge base
export const search = query({
args: {
query: v.string(),
limit: v.optional(v.number()),
},
returns: v.array(v.object({
_id: v.id("documents"),
title: v.string(),
content: v.string(),
score: v.number(),
})),
handler: async (ctx, args) => {
const results = await agent.search(ctx, {
query: args.query,
table: "documents",
limit: args.limit ?? 5,
});
return results.map((r) => ({
_id: r._id,
title: r.title,
content: r.content,
score: r._score,
}));
},
});
// convex/workflows.ts
import { action, internalMutation } from "./_generated/server";
import { v } from "convex/values";
import { agent } from "./agent";
import { internal } from "./_generated/api";
// Multi-step research workflow
export const researchTopic = action({
args: {
topic: v.string(),
userId: v.id("users"),
},
returns: v.id("research"),
handler: async (ctx, args) => {
// Create research record
const researchId = await ctx.runMutation(internal.workflows.createResearch, {
topic: args.topic,
userId: args.userId,
status: "searching",
});
// Step 1: Search for relevant documents
const searchResults = await agent.search(ctx, {
query: args.topic,
table: "documents",
limit: 10,
});
await ctx.runMutation(internal.workflows.updateStatus, {
researchId,
status: "analyzing",
});
// Step 2: Analyze and synthesize
const analysis = await agent.chat(ctx, {
messages: [{
role: "user",
content: `Analyze these sources about "${args.topic}" and provide a comprehensive summary:\n\n${
searchResults.map((r) => r.content).join("\n\n---\n\n")
}`,
}],
systemPrompt: "You are a research assistant. Provide thorough, well-cited analysis.",
});
// Step 3: Generate key insights
await ctx.runMutation(internal.workflows.updateStatus, {
researchId,
status: "summarizing",
});
const insights = await agent.chat(ctx, {
messages: [{
role: "user",
content: `Based on this analysis, list 5 key insights:\n\n${analysis.content}`,
}],
});
// Save final results
await ctx.runMutation(internal.workflows.completeResearch, {
researchId,
analysis: analysis.content,
insights: insights.content,
sources: searchResults.map((r) => r._id),
});
return researchId;
},
});
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
threads: defineTable({
userId: v.id("users"),
title: v.string(),
lastMessageAt: v.optional(v.number()),
metadata: v.optional(v.any()),
}).index("by_user", ["userId"]),
messages: defineTable({
threadId: v.id("threads"),
role: v.union(v.literal("user"), v.literal("assistant"), v.literal("system")),
content: v.string(),
toolCalls: v.optional(v.array(v.object({
name: v.string(),
arguments: v.any(),
result: v.optional(v.any()),
}))),
createdAt: v.number(),
}).index("by_thread", ["threadId"]),
documents: defineTable({
title: v.string(),
content: v.string(),
embedding: v.array(v.float64()),
metadata: v.object({
source: v.optional(v.string()),
category: v.optional(v.string()),
}),
createdAt: v.number(),
}).vectorIndex("by_embedding", {
vectorField: "embedding",
dimensions: 1536,
}),
});
import { useQuery, useMutation, useAction } from "convex/react";
import { api } from "../convex/_generated/api";
import { useState, useRef, useEffect } from "react";
function ChatInterface({ threadId }: { threadId: Id<"threads"> }) {
const messages = useQuery(api.threads.getMessages, { threadId });
const sendMessage = useAction(api.chat.sendMessage);
const [input, setInput] = useState("");
const [sending, setSending] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
const handleSend = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim() || sending) return;
const message = input.trim();
setInput("");
setSending(true);
try {
await sendMessage({ threadId, message });
} finally {
setSending(false);
}
};
return (
<div className="chat-container">
<div className="messages">
{messages?.map((msg, i) => (
<div key={i} className={`message ${msg.role}`}>
<strong>{msg.role === "user" ? "You" : "Assistant"}:</strong>
<p>{msg.content}</p>
</div>
))}
<div ref={messagesEndRef} />
</div>
<form onSubmit={handleSend} className="input-form">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type your message..."
disabled={sending}
/>
<button type="submit" disabled={sending || !input.trim()}>
{sending ? "Sending..." : "Send"}
</button>
</form>
</div>
);
}
npx convex deploy unless explicitly instructed