PluginBench
Skill
Review
Audit score 70

convex-functions

waynesutton/convexskills

Write Convex queries, mutations, actions, and HTTP endpoints with validation, error handling, and best practices.

What is convex-functions?

This skill teaches you to build Convex backend functions including read-only queries, transactional mutations, external API actions, and HTTP webhooks. Use it when implementing server-side logic for a Convex backend, handling database operations, external integrations, and API endpoints.

  • Write queries for reactive, cached read-only database access
  • Create mutations for transactional database modifications
  • Build actions that call external APIs with database access via runQuery/runMutation
  • Implement HTTP actions for webhooks and REST API endpoints
  • Use internal functions for sensitive operations callable only from other Convex functions
  • Apply argument validation and error handling patterns

How to install convex-functions

npx skills add https://github.com/waynesutton/convexskills --skill convex-functions
Prerequisites
  • Convex project set up with @convex-dev/eslint-plugin configured
  • Understanding of TypeScript and async/await
  • Familiarity with database concepts (queries, mutations, transactions)
Claude Code
Cursor
Windsurf
Cline

How to use convex-functions

  1. 1.Review the function type overview table to understand when to use queries, mutations, actions, or HTTP actions
  2. 2.Write queries with read-only database access and argument validators using v.id() and v.string()
  3. 3.Create mutations for database modifications with validation and ConvexError for error handling
  4. 4.Build actions with 'use node' directive to call external APIs and use ctx.runQuery/runMutation for database access
  5. 5.Implement HTTP actions in convex/http.ts using httpRouter for webhooks and API endpoints
  6. 6.Use internal functions (internalMutation, internalQuery, internalAction) for sensitive operations not exposed to clients
  7. 7.Apply explicit table names and object syntax with handler property in all functions

Use cases

Good for
  • Fetching user data and task lists with reactive caching in queries
  • Creating and updating records transactionally in mutations
  • Processing payments by calling external APIs from actions
  • Handling Stripe webhooks or other third-party events via HTTP actions
  • Restricting sensitive operations like credit updates to internal functions only
Who it's for
  • Backend developers building Convex applications
  • Full-stack developers implementing server-side logic
  • Teams integrating external payment or email APIs
  • Developers handling webhooks from third-party services

convex-functions FAQ

When should I use a query vs a mutation?

Use queries for read-only database access—they are cached and reactive. Use mutations when you need to modify data; they are transactional but not cached.

Can actions access the database directly?

No. Actions must use ctx.runQuery() and ctx.runMutation() to access the database indirectly, allowing them to call external APIs safely.

What are internal functions used for?

Internal functions are only callable from other Convex functions, not from clients. Use them for sensitive operations like updating credits or processing payments.

How do I handle webhooks from external services?

Use HTTP actions in convex/http.ts with httpRouter. Verify signatures, parse the request body, and call mutations to update your database.

Do I need argument validators on all functions?

Yes. All functions should have argument validators using the v module (v.id, v.string, etc.) for type safety and compliance with @convex-dev/eslint-plugin rules.

Full instructions (SKILL.md)

Source of truth, from waynesutton/convexskills.


name: convex-functions displayName: Convex Functions description: Writing queries, mutations, actions, and HTTP actions with proper argument validation, error handling, internal functions, and runtime considerations version: 1.0.0 author: Convex tags: [convex, functions, queries, mutations, actions, http]

Convex Functions

Master Convex functions including queries, mutations, actions, and HTTP endpoints with proper validation, error handling, and runtime considerations.

Code Quality

All examples in this skill comply with @convex-dev/eslint-plugin rules:

  • Object syntax with handler property
  • Argument validators on all functions
  • Explicit table names in database operations

See the Code Quality section in convex-best-practices for linting setup.

Documentation Sources

Before implementing, do not assume; fetch the latest documentation:

Instructions

Function Types Overview

TypeDatabase AccessExternal APIsCachingUse Case
QueryRead-onlyNoYes, reactiveFetching data
MutationRead/WriteNoNoModifying data
ActionVia runQuery/runMutationYesNoExternal integrations
HTTP ActionVia runQuery/runMutationYesNoWebhooks, APIs

Queries

Queries are reactive, cached, and read-only:

import { query } from "./_generated/server";
import { v } from "convex/values";

export const getUser = query({
  args: { userId: v.id("users") },
  returns: v.union(
    v.object({
      _id: v.id("users"),
      _creationTime: v.number(),
      name: v.string(),
      email: v.string(),
    }),
    v.null(),
  ),
  handler: async (ctx, args) => {
    return await ctx.db.get("users", args.userId);
  },
});

// Query with index
export const listUserTasks = query({
  args: { userId: v.id("users") },
  returns: v.array(
    v.object({
      _id: v.id("tasks"),
      _creationTime: v.number(),
      title: v.string(),
      completed: v.boolean(),
    }),
  ),
  handler: async (ctx, args) => {
    return await ctx.db
      .query("tasks")
      .withIndex("by_user", (q) => q.eq("userId", args.userId))
      .order("desc")
      .collect();
  },
});

Mutations

Mutations modify the database and are transactional:

import { mutation } from "./_generated/server";
import { v } from "convex/values";
import { ConvexError } from "convex/values";

export const createTask = mutation({
  args: {
    title: v.string(),
    userId: v.id("users"),
  },
  returns: v.id("tasks"),
  handler: async (ctx, args) => {
    // Validate user exists
    const user = await ctx.db.get("users", args.userId);
    if (!user) {
      throw new ConvexError("User not found");
    }

    return await ctx.db.insert("tasks", {
      title: args.title,
      userId: args.userId,
      completed: false,
      createdAt: Date.now(),
    });
  },
});

export const deleteTask = mutation({
  args: { taskId: v.id("tasks") },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.delete("tasks", args.taskId);
    return null;
  },
});

Actions

Actions can call external APIs but have no direct database access:

"use node";

import { action } from "./_generated/server";
import { v } from "convex/values";
import { api, internal } from "./_generated/api";

export const sendEmail = action({
  args: {
    to: v.string(),
    subject: v.string(),
    body: v.string(),
  },
  returns: v.object({ success: v.boolean() }),
  handler: async (ctx, args) => {
    // Call external API
    const response = await fetch("https://api.email.com/send", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(args),
    });

    return { success: response.ok };
  },
});

// Action calling queries and mutations
export const processOrder = action({
  args: { orderId: v.id("orders") },
  returns: v.null(),
  handler: async (ctx, args) => {
    // Read data via query
    const order = await ctx.runQuery(api.orders.get, { orderId: args.orderId });

    if (!order) {
      throw new Error("Order not found");
    }

    // Call external payment API
    const paymentResult = await processPayment(order);

    // Update database via mutation
    await ctx.runMutation(internal.orders.updateStatus, {
      orderId: args.orderId,
      status: paymentResult.success ? "paid" : "failed",
    });

    return null;
  },
});

HTTP Actions

HTTP actions handle webhooks and external requests:

// convex/http.ts
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import { api, internal } from "./_generated/api";

const http = httpRouter();

// Webhook endpoint
http.route({
  path: "/webhooks/stripe",
  method: "POST",
  handler: httpAction(async (ctx, request) => {
    const signature = request.headers.get("stripe-signature");
    const body = await request.text();

    // Verify webhook signature
    if (!verifyStripeSignature(body, signature)) {
      return new Response("Invalid signature", { status: 401 });
    }

    const event = JSON.parse(body);

    // Process webhook
    await ctx.runMutation(internal.payments.handleWebhook, {
      eventType: event.type,
      data: event.data,
    });

    return new Response("OK", { status: 200 });
  }),
});

// API endpoint
http.route({
  path: "/api/users/:userId",
  method: "GET",
  handler: httpAction(async (ctx, request) => {
    const url = new URL(request.url);
    const userId = url.pathname.split("/").pop();

    const user = await ctx.runQuery(api.users.get, {
      userId: userId as Id<"users">,
    });

    if (!user) {
      return new Response("Not found", { status: 404 });
    }

    return Response.json(user);
  }),
});

export default http;

Internal Functions

Use internal functions for sensitive operations:

import {
  internalMutation,
  internalQuery,
  internalAction,
} from "./_generated/server";
import { v } from "convex/values";

// Only callable from other Convex functions
export const _updateUserCredits = internalMutation({
  args: {
    userId: v.id("users"),
    amount: v.number(),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    const user = await ctx.db.get("users", args.userId);
    if (!user) return null;

    await ctx.db.patch("users", args.userId, {
      credits: (user.credits || 0) + args.amount,
    });
    return null;
  },
});

// Call internal function from action
export const purchaseCredits = action({
  args: { userId: v.id("users"), amount: v.number() },
  returns: v.null(),
  handler: async (ctx, args) => {
    // Process payment externally
    await processPayment(args.amount);

    // Update credits via internal mutation
    await ctx.runMutation(internal.users._updateUserCredits, {
      userId: args.userId,
      amount: args.amount,
    });

    return null;
  },
});

Scheduling Functions

Schedule functions to run later:

import { mutation, internalMutation } from "./_generated/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";

export const scheduleReminder = mutation({
  args: {
    userId: v.id("users"),
    message: v.string(),
    delayMs: v.number(),
  },
  returns: v.id("_scheduled_functions"),
  handler: async (ctx, args) => {
    return await ctx.scheduler.runAfter(
      args.delayMs,
      internal.notifications.sendReminder,
      { userId: args.userId, message: args.message },
    );
  },
});

export const sendReminder = internalMutation({
  args: {
    userId: v.id("users"),
    message: v.string(),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.insert("notifications", {
      userId: args.userId,
      message: args.message,
      sentAt: Date.now(),
    });
    return null;
  },
});

Examples

Complete Function File

// convex/messages.ts
import { query, mutation, internalMutation } from "./_generated/server";
import { v } from "convex/values";
import { ConvexError } from "convex/values";
import { internal } from "./_generated/api";

const messageValidator = v.object({
  _id: v.id("messages"),
  _creationTime: v.number(),
  channelId: v.id("channels"),
  authorId: v.id("users"),
  content: v.string(),
  editedAt: v.optional(v.number()),
});

// Public query
export const list = query({
  args: {
    channelId: v.id("channels"),
    limit: v.optional(v.number()),
  },
  returns: v.array(messageValidator),
  handler: async (ctx, args) => {
    const limit = args.limit ?? 50;
    return await ctx.db
      .query("messages")
      .withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
      .order("desc")
      .take(limit);
  },
});

// Public mutation
export const send = mutation({
  args: {
    channelId: v.id("channels"),
    authorId: v.id("users"),
    content: v.string(),
  },
  returns: v.id("messages"),
  handler: async (ctx, args) => {
    if (args.content.trim().length === 0) {
      throw new ConvexError("Message cannot be empty");
    }

    const messageId = await ctx.db.insert("messages", {
      channelId: args.channelId,
      authorId: args.authorId,
      content: args.content.trim(),
    });

    // Schedule notification
    await ctx.scheduler.runAfter(0, internal.messages.notifySubscribers, {
      channelId: args.channelId,
      messageId,
    });

    return messageId;
  },
});

// Internal mutation
export const notifySubscribers = internalMutation({
  args: {
    channelId: v.id("channels"),
    messageId: v.id("messages"),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    // Get channel subscribers and notify them
    const subscribers = await ctx.db
      .query("subscriptions")
      .withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
      .collect();

    for (const sub of subscribers) {
      await ctx.db.insert("notifications", {
        userId: sub.userId,
        messageId: args.messageId,
        read: false,
      });
    }
    return null;
  },
});

Best Practices

  • Never run npx convex deploy unless explicitly instructed
  • Never run any git commands unless explicitly instructed
  • Always define args and returns validators
  • Use queries for read operations (they are cached and reactive)
  • Use mutations for write operations (they are transactional)
  • Use actions only when calling external APIs
  • Use internal functions for sensitive operations
  • Add "use node"; at the top of action files using Node.js APIs
  • Handle errors with ConvexError for user-facing messages

Common Pitfalls

  1. Using actions for database operations - Use queries/mutations instead
  2. Calling external APIs from queries/mutations - Use actions
  3. Forgetting to add "use node" - Required for Node.js APIs in actions
  4. Missing return validators - Always specify returns
  5. Not using internal functions for sensitive logic - Protect with internalMutation

References