convex-agents
Builds persistent, stateful AI agents on the Convex platform.
Install
mkdir -p .claude/skills/convex-agents && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/9233" && unzip -o skill.zip -d .claude/skills/convex-agents && rm skill.zipInstalls to .claude/skills/convex-agents
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Building AI agents with the Convex Agent component including thread management, tool integration, streaming responses, RAG patterns, and workflow orchestrationKey capabilities
- →Manage conversation threads for AI agents
- →Integrate tools for agent actions
- →Stream agent responses to clients
- →Implement RAG patterns for knowledge retrieval
- →Orchestrate long-running agent tasks
- →Store conversation history persistently
How it works
The Convex Agent component builds stateful AI agents by managing threads, integrating tools, and streaming responses, ensuring conversation history persists across restarts.
Inputs & outputs
When to use convex-agents
- →Building AI agents with persistent state
- →Implementing RAG patterns
- →Setting up real-time streaming agent responses
About this skill
Convex agents
Produces a chat or tool calling agent backed by @convex-dev/agent, with thread history stored in Convex and a reactive message list for the UI. The one rule: every LLM call runs inside an action. Mutations save the prompt and schedule the action; they never call a model.
When to reach for this
- Adding a chat assistant with persistent conversation history
- Letting an LLM call your queries and mutations as tools
- Streaming a model reply to one or more clients
- Answering questions over your own documents (RAG)
- Chaining several LLM steps into a durable job that survives restarts
Install and register
npm install @convex-dev/agent ai @ai-sdk/openai zod
npx convex env set OPENAI_API_KEY sk-...
// convex/convex.config.ts
import { defineApp } from "convex/server";
import agent from "@convex-dev/agent/convex.config";
const app = defineApp();
app.use(agent);
export default app;
Run npx convex dev once so components.agent is generated before defining an agent.
Define an agent
// convex/agent.ts
import { Agent, stepCountIs } from "@convex-dev/agent";
import { openai } from "@ai-sdk/openai";
import { components } from "./_generated/api";
export const supportAgent = new Agent(components.agent, {
name: "Support Agent",
languageModel: openai.chat("gpt-4o-mini"),
instructions: "You are a support assistant. Answer briefly and cite docs when possible.",
// Lets the model call tools and then respond, up to 5 steps
stopWhen: stepCountIs(5),
});
name tags each saved message with the agent that wrote it. Everything except name can be overridden per call.
Create a thread and generate a reply
Save the user prompt in a mutation, then schedule an internal action that generates the reply. Clients subscribed to the thread see the new message without the action returning anything.
// convex/chat.ts
import { v } from "convex/values";
import { mutation, internalAction, QueryCtx, MutationCtx } from "./_generated/server";
import { components, internal } from "./_generated/api";
import { saveMessage } from "@convex-dev/agent";
import { supportAgent } from "./agent";
// Throws unless the signed in user owns the thread
async function authorizeThreadAccess(ctx: QueryCtx | MutationCtx, threadId: string) {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
const thread = await ctx.runQuery(components.agent.threads.getThread, { threadId });
if (!thread || thread.userId !== identity.subject) throw new Error("Unauthorized");
}
export const startThread = mutation({
args: {},
returns: v.string(),
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
const { threadId } = await supportAgent.createThread(ctx, { userId: identity.subject });
return threadId;
},
});
export const sendMessage = mutation({
args: { threadId: v.string(), prompt: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
await authorizeThreadAccess(ctx, args.threadId);
const { messageId } = await saveMessage(ctx, components.agent, {
threadId: args.threadId,
prompt: args.prompt,
});
await ctx.scheduler.runAfter(0, internal.chat.generateReply, {
threadId: args.threadId,
promptMessageId: messageId,
});
return null;
},
});
export const generateReply = internalAction({
args: { threadId: v.string(), promptMessageId: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
// promptMessageId makes retries safe: the same prompt is reused, never duplicated
await supportAgent.generateText(
ctx,
{ threadId: args.threadId },
{ promptMessageId: args.promptMessageId },
);
return null;
},
});
Thread ids are strings, not v.id(...), since the table lives inside the component.
List messages for the UI
// convex/chat.ts (continued)
import { paginationOptsValidator } from "convex/server";
import { listUIMessages } from "@convex-dev/agent";
import { query } from "./_generated/server";
export const listMessages = query({
args: { threadId: v.string(), paginationOpts: paginationOptsValidator },
handler: async (ctx, args) => {
await authorizeThreadAccess(ctx, args.threadId);
return await listUIMessages(ctx, components.agent, args);
},
});
// src/Chat.tsx
import { useUIMessages } from "@convex-dev/agent/react";
import { api } from "../convex/_generated/api";
function Chat({ threadId }: { threadId: string }) {
const { results, status, loadMore } = useUIMessages(
api.chat.listMessages,
{ threadId },
{ initialNumItems: 20 },
);
return (
<div>
{results.map((m) => (
<div key={m.key} data-role={m.role}>{m.text}</div>
))}
{status === "CanLoadMore" && <button onClick={() => loadMore(20)}>Older</button>}
</div>
);
}
listUIMessages merges tool calls and the assistant text that follows them into one UIMessage, which keeps rendering simple.
One tool
Tools are defined with createTool and get a ctx that includes runQuery, runMutation, userId, and threadId. Annotate the handler return type to avoid circular type errors.
// convex/tools.ts
import { createTool } from "@convex-dev/agent";
import { z } from "zod";
import { api } from "./_generated/api";
export const searchOrders = createTool({
description: "Find the current user's orders that match a search term",
args: z.object({
term: z.string().describe("Product name or order number to look for"),
}),
handler: async (ctx, args): Promise<Array<{ id: string; status: string }>> => {
return await ctx.runQuery(api.orders.search, { term: args.term });
},
});
Pass it to the agent with tools: { searchOrders } in the constructor or at the call site. For tool error handling, runtime tools with closures, and delta streaming to the client, open references/tools-and-streaming.md.
Retrieval and multi step jobs
For embedding documents, searching them with @convex-dev/rag or a hand rolled vector index, injecting results into the prompt, and running several LLM steps as a durable @convex-dev/workflow job, open references/rag-and-workflows.md.
Common mistakes
| Mistake | Why it breaks | Do instead |
|---|---|---|
Calling generateText in a mutation | Mutations cannot make network calls and must be deterministic | Save the prompt with saveMessage, schedule an internalAction |
v.id("threads") for thread ids | The threads table lives in the component, so ids are strings outside it | Use v.string() |
Skipping npx convex dev after app.use(agent) | components.agent is not generated, so types fail | Run dev once before writing agent code |
| Returning the reply text from the action to the client | Loses the reply if the client disconnects, no reactivity | Let clients read listUIMessages; the saved message shows up on its own |
Tools without .describe() on args | The model guesses what each field means and calls tools badly | Describe every zod field |
| Tool handler with no return type annotation | TypeScript circularity errors from ctx.runQuery | Add : Promise<...> to the handler |
| Exposing the message query with no auth check | Any client can read any thread | Call an authorizeThreadAccess helper first |
stopWhen left at the default with tools defined | The model calls a tool and stops without a text reply | Set stopWhen: stepCountIs(n) with n > 1 |
Checklist
-
app.use(agent)inconvex.config.tsandnpx convex devhas run - Provider key stored with
npx convex env set, never in client code -
Agenthas aname,languageModel, andinstructions - Prompts saved in a mutation, replies generated in an
internalActionwithpromptMessageId - Thread and message ids typed as
v.string() - Message list query checks thread ownership before calling
listUIMessages - Every tool arg has a zod
.describe()and the handler has a return type -
stopWhen: stepCountIs(n)set when tools are in play - Client uses
useUIMessagesrather than reading action return values
Docs
Prerequisites
Limitations
- →Conversations are lost if threads are not persisted
- →Blocking on long responses degrades user experience
How it compares
This skill provides a framework for building persistent, stateful AI agents with built-in RAG and tool execution within the Convex ecosystem, unlike a generic AI agent implementation.
Compared to similar skills
convex-agents side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| convex-agents (this skill) | 0 | 8mo | Review | Intermediate |
| reasoningbank-with-agentdb | 5 | 11mo | Review | Intermediate |
| ai-engineer | 7 | 5mo | No flags | Advanced |
| llm-application-dev | 3 | 6mo | Review | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by waynesutton
View all by waynesutton →You might also like
reasoningbank-with-agentdb
ruvnet
Implement ReasoningBank adaptive learning with AgentDB's 150x faster vector database. Includes trajectory tracking, verdict judgment, memory distillation, and pattern recognition. Use when building self-learning agents, optimizing decision-making, or implementing experience replay systems.
ai-engineer
sickn33
Build production-ready LLM applications, advanced RAG systems, and intelligent agents. Implements vector search, multimodal AI, agent orchestration, and enterprise AI integrations. Use PROACTIVELY for LLM features, chatbots, AI agents, or AI-powered applications.
llm-application-dev
skillcreatorai
Building applications with Large Language Models - prompt engineering, RAG patterns, and LLM integration. Use for AI-powered features, chatbots, or LLM-based automation.
mistral-core-workflow-b
jeremylongshore
Execute Mistral AI secondary workflows: Embeddings and Function Calling. Use when implementing semantic search, RAG applications, or tool-augmented LLM interactions. Trigger with phrases like "mistral embeddings", "mistral function calling", "mistral tools", "mistral RAG", "mistral semantic search".
langchain-architecture
Kuingsmile
Design LLM applications using the LangChain framework with agents, memory, and tool integration patterns. Use when building LangChain applications, implementing AI agents, or creating complex LLM workflows.
agentdb-memory-patterns
ruvnet
Implement persistent memory patterns for AI agents using AgentDB. Includes session memory, long-term storage, pattern learning, and context management. Use when building stateful agents, chat systems, or intelligent assistants.