Skip to content
Backend
Skill

/convex-agents

Builds AI agents on the Convex agent component: threads, messages, tools that call queries and mutations, streaming, RAG with vector search, and workflows for multi step jobs. Use when adding a chat assistant, tool calling agent, or retrieval feature to a Convex app.

BOOST
From plugin
builder-skills
40619 skills
Install
$ npx -y skills add waynesutton/builder-skills --skill convex-agents --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/convex-agents

Context preview

The summary Claude sees to decide when to auto-load this skill.

Builds AI agents on the Convex agent component: threads, messages, tools that call queries and mutations, streaming, RAG with vector search, and workflows for multi step jobs. Use when adding a chat assistant, tool calling agent, or retrieval feature to a Convex app.

SKILL.md

convex-agents.SKILL.md
name: convex-agents
description: Builds AI agents on the Convex agent component: threads, messages, tools that call queries and mutations, streaming, RAG with vector search, and workflows for multi step jobs. Use when adding a chat assistant, tool calling agent, or retrieval feature to a Convex app.

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 fo
Read more
Ships withbuilder-skills

Agent skills for builders shipping Convex apps. By Wayne Sutton. Seventeen skills.

Get the whole plugin

Other skills on builder-skills.