| name | convex-agents |
| displayName | 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"] |
Convex Agents
Build persistent, stateful AI agents with Convex including thread management, tool integration, streaming responses, RAG patterns, and workflow orchestration.
Documentation Sources
Before implementing, do not assume; fetch the latest documentation:
Instructions
Why Convex for AI Agents
- Persistent State - Conversation history survives restarts
- Real-time Updates - Stream responses to clients automatically
- Tool Execution - Run Convex functions as agent tools
- Durable Workflows - Long-running agent tasks with reliability
- Built-in RAG - Vector search for knowledge retrieval
Setting Up Convex Agent
bun install @convex-dev/agent ai openai
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,
});
Thread Management
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { agent } from "./agent";
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;
},
});
export const listThreads = query({
args: { userId: v.id("users") },
returns: v.array(v.object({
: v.(),
: v.(),
: v.(v.()),
})),
: (ctx, args) => {
agent.(ctx, {
: args.,
});
},
});
getMessages = ({
: { : v.() },
: v.(v.({
: v.(),
: v.(),
: v.(),
})),
: (ctx, args) => {
agent.(ctx, {
: args.,
});
},
});
Sending Messages and Streaming Responses
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) => {
await ctx.runMutation(internal.chat.addUserMessage, {
threadId: args.threadId,
content: args.message,
});
const response = await agent.chat(ctx, {
threadId: args.threadId,
messages: [{ role: "user", content: args.message }],
stream: true,
onToken: async (token) => {
ctx.(internal.., {
: args.,
token,
});
},
});
ctx.(internal.., {
: args.,
: response.,
});
;
},
});
Tool Integration
Define tools that agents can use:
import { tool } from "@convex-dev/agent";
import { v } from "convex/values";
import { api } from "./_generated/api";
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;
},
});
export const createTask = tool({
name: "create_task",
description: "Create a new task for the user",
parameters: v.object({
title: v.string(),
: v.(v.()),
: v.(v.()),
}),
: (ctx, args) => {
taskId = ctx.(api.., {
: args.,
: args.,
: args. ? (args.).() : ,
});
{ : , taskId };
},
});
getWeather = ({
: ,
: ,
: v.({
: v.(),
}),
: (ctx, args) => {
response = (
);
response.();
},
});
Agent with Tools
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;
},
});
RAG (Retrieval Augmented Generation)
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { agent } from "./agent";
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) => {
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: .(),
});
},
});
search = ({
: {
: v.(),
: v.(v.()),
},
: v.(v.({
: v.(),
: v.(),
: v.(),
: v.(),
})),
: (ctx, args) => {
results = agent.(ctx, {
: args.,
: ,
: args. ?? ,
});
results.( ({
: r.,
: r.,
: r.,
: r.,
}));
},
});
Workflow Orchestration
import { action, internalMutation } from "./_generated/server";
import { v } from "convex/values";
import { agent } from "./agent";
import { internal } from "./_generated/api";
export const researchTopic = action({
args: {
topic: v.string(),
userId: v.id("users"),
},
returns: v.id("research"),
handler: async (ctx, args) => {
const researchId = await ctx.runMutation(internal.workflows.createResearch, {
topic: args.topic,
userId: args.userId,
status: "searching",
});
const searchResults = await agent.search(ctx, {
query: args.topic,
table: "documents",
limit: 10,
});
ctx.(internal.., {
researchId,
: ,
});
analysis = agent.(ctx, {
: [{
: ,
: ,
}],
: ,
});
ctx.(internal.., {
researchId,
: ,
});
insights = agent.(ctx, {
: [{
: ,
: ,
}],
});
ctx.(internal.., {
researchId,
: analysis.,
: insights.,
: searchResults.( r.),
});
researchId;
},
});
Examples
Complete Chat Application Schema
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.()),
}))),
: v.(),
}).(, []),
: ({
: v.(),
: v.(),
: v.(v.()),
: v.({
: v.(v.()),
: v.(v.()),
}),
: v.(),
}).(, {
: ,
: ,
}),
});
React Chat Component
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();
();
();
{
({ threadId, message });
} {
();
}
};
(
);
}
Best Practices
- Never run
bunx convex deploy unless explicitly instructed
- Never run any git commands unless explicitly instructed
- Store conversation history in Convex for persistence
- Use streaming for better user experience with long responses
- Implement proper error handling for tool failures
- Use vector indexes for efficient RAG retrieval
- Rate limit agent interactions to control costs
- Log tool usage for debugging and analytics
Common Pitfalls
- Not persisting threads - Conversations lost on refresh
- Blocking on long responses - Use streaming instead
- Tool errors crashing agents - Add proper error handling
- Large context windows - Summarize old messages
- Missing embeddings for RAG - Generate embeddings on insert
References