convex-agents

Persistent, stateful AI agents with thread management, tool integration, streaming, and RAG on Convex. Thread management for multi-turn conversations with automatic persistence across restarts and real-time streaming responses to clients Tool integration allowing agents to execute Convex functions as callable tools for knowledge search, task creation, and external API calls Built-in vector search and RAG patterns for embedding documents and retrieving relevant context to augment agent responses Multi-step workflow orchestration for complex agent tasks like research pipelines with status tracking and synthesis across steps Complete schema and React integration examples for rapid chat application development with message history and tool call tracking

INSTALLATION
npx skills add https://github.com/waynesutton/convexskills --skill convex-agents
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

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

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,

});

Thread Management

// 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,

    });

  },

});

Sending Messages and Streaming Responses

// 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;

  },

});

Tool Integration

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();

  },

});

Agent with Tools

// 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;

  },

});

RAG (Retrieval Augmented Generation)

// 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,

    }));

  },

});

Workflow Orchestration

// 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;

  },

});

Examples

Complete Chat Application Schema

// 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,

  }),

});

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();

    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>

  );

}

Best Practices

  • Never run npx 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

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card