Skip to main content
skill CX Codex

convex-agents — Claude Code skill: what it does, how to install

Guides agent setup with the @convex-dev/agent component. Useful when adding chat threads, tool calling for queries and mutations, or RAG to a Convex app.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add waynesutton/builder-skills --skill convex-agents -a claude-code
Open on GitHub

What convex-agents does

This skill defines architectural patterns for AI agents on Convex. It generates code for threads, reactive UI message feeds with useUIMessages, and tools built with createTool that execute queries or mutations via context.

The core constraint is that every LLM call must run inside an action. Mutations must stay deterministic without network access, so calling generateText in a mutation will fail immediately. Instead, mutations store the user input with saveMessage and schedule an internalAction using ctx.scheduler.runAfter.

The setup relies on @convex-dev/agent, ai, @ai-sdk/openai, and zod, alongside an OPENAI_API_KEY environment variable. You also need to run npx convex dev once so the build generates components.agent before defining any agent instance.

What it helps with

  • set up persistent chat threads and message history in Convex
  • expose Convex queries and mutations to language models as typed tools
  • stream and list reactive assistant messages in UI components
  • separate prompt saving in mutations from model inference in actions

Example prompts for the agent

  • Create a support agent using @convex-dev/agent and gpt-4o-mini
  • Define a searchOrders tool using createTool and Zod that calls api.orders.search
  • Implement the startThread and sendMessage mutations with scheduled reply actions

How to install convex-agents

1Skills CLI (Vercel)

npx skills add waynesutton/builder-skills --skill convex-agents -a claude-code

2Manually: put the files in place

~/.claude/skills/convex-agents/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/convex-agents/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about convex-agents

How to install convex-agents in Claude Code?

Run npx skills add waynesutton/builder-skills --skill convex-agents -a claude-code.

What does the convex-agents skill do?

It provides patterns and code samples for building AI agents using @convex-dev/agent, ensuring LLM calls run in actions rather than mutations.

Why can't you run generateText inside a Convex mutation?

Convex mutations must be deterministic and cannot perform network requests. The skill enforces saving the prompt in a mutation and delegating the model call to a scheduled internalAction.

Source text

SKILL.md · Apache-2.0
---
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

```bash
npm install @convex-dev/agent ai @ai-sdk/openai zod
npx convex env set OPENAI_API_KEY sk-...
```

```typescript
// 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

```typescript
// 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.

```typescript
// 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

```typescript
// 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);
  },
});
```

```tsx
// 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.

```typescript
// 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](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](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)` in `convex.config.ts` and `npx convex dev` has run
- [ ] Provider key stored with `npx convex env set`, never in client code
- [ ] `Agent` has a `name`, `languageModel`, and `instructions`
- [ ] Prompts saved in a mutation, replies generated in an `internalAction` with `promptMessageId`
- [ ] 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 `useUIMessages` rather than reading action return values

## Docs

- https://docs.convex.dev/llms.txt
- https://docs.convex.dev/agents
- https://docs.convex.dev/agents/agent-usage
- https://docs.convex.dev/agents/tools
- https://www.convex.dev/components/agent

© waynesutton · Apache-2.0 · waynesutton/builder-skills

Similar skills

All skills for Agent setup: memory, context, orchestration →

From the same set14

convex-cron-jobs Sets up recurring jobs in convex/crons.ts and delayed execution with ctx.scheduler. Helps batch heavy mutations and debug failing queues in Convex. CX Codex +1 2,800installs convex-security-check Checks a Convex backend before merge for exposed functions, missing auth, weak validation, ownership gaps, and secrets in code. CX Codex +1 2,700installs convex-http-actions Adds HTTP endpoints in convex/http.ts: webhook receivers with signature checks, REST routes, CORS, streaming and file uploads. CX Codex +1 2,700installs convex-migrations Walks a live Convex schema change through six steps: optional field, batched backfill, validator flip, cleanup. Useful when npx convex dev rejects existing documents. CX Codex +1 2,600installs convex-file-storage Handles Convex file storage operations: upload URLs, blob persistence in actions, and serving with getUrl. Useful when building user uploads or fetching external assets. CX Codex +1 2,600installs convex-security-audit A skill that walks through a Convex app's security and writes up what it finds. Useful before a launch or after an incident. CX Codex +1 1,500installs avoid-feature-creep Keeps coding agent diffs strictly scoped to the user request. It flags unprompted refactors or extra parameters and moves them to a suggestions list. CX Codex +1 1,300installs convex-component-authoring Walks the agent through a reusable Convex component: its own schema, functions and a typed client wrapper. Handy when a feature moves into a package. CX Codex +1 1,200installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor