Skip to main content
Convex functions are the building blocks of your backend. There are three types of functions, each designed for specific use cases:
  • Queries - Read data from the database (reactive, transactional, fast)
  • Mutations - Write data to the database (transactional, atomic)
  • Actions - Interact with external services (non-transactional, can call APIs)

Queries

Queries are read-only functions that fetch data from your database. They’re reactive - when data changes, queries automatically re-run and update subscribed clients.

Basic query

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

export const list = query({
  args: {},
  handler: async (ctx) => {
    return await ctx.db.query("messages").collect();
  },
});

Query context

Queries receive a context object with:
  • ctx.db - Read-only database access (GenericDatabaseReader)
  • ctx.auth - Current user authentication information
  • ctx.storage - Read-only access to stored files
  • ctx.runQuery - Call another query within the same transaction
import { query } from "./_generated/server";
import { v } from "convex/values";

export const getMessage = query({
  args: { messageId: v.id("messages") },
  handler: async (ctx, args) => {
    // Get authenticated user
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Not authenticated");
    
    // Read from database
    const message = await ctx.db.get(args.messageId);
    if (!message) return null;
    
    // Get file URL from storage
    if (message.imageId) {
      const imageUrl = await ctx.storage.getUrl(message.imageId);
      return { ...message, imageUrl };
    }
    
    return message;
  },
});

Query characteristics

  • Read-only - Cannot modify the database
  • Reactive - Automatically re-run when data changes
  • Transactional - See a consistent snapshot of the database
  • Fast - Typically run in less than 10ms
  • Cached - Results can be cached by the client
Queries see a consistent snapshot of the database at a single point in time. All reads within a query are isolated from concurrent writes.

Mutations

Mutations modify data in your database. All writes within a mutation are atomic - either all succeed or all fail.

Basic mutation

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

export const send = mutation({
  args: {
    body: v.string(),
    author: v.string(),
  },
  handler: async (ctx, args) => {
    const messageId = await ctx.db.insert("messages", {
      body: args.body,
      author: args.author,
    });
    return messageId;
  },
});

Mutation context

Mutations receive a context object with:
  • ctx.db - Read-write database access (GenericDatabaseWriter)
  • ctx.auth - Current user authentication information
  • ctx.storage - Generate upload URLs and delete files
  • ctx.scheduler - Schedule functions to run later
  • ctx.runQuery - Call a query within the same transaction
  • ctx.runMutation - Call another mutation in a sub-transaction
import { mutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";

export const createTask = mutation({
  args: { text: v.string() },
  handler: async (ctx, args) => {
    // Check authentication
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Not authenticated");
    
    // Insert document
    const taskId = await ctx.db.insert("tasks", {
      text: args.text,
      completed: false,
      userId: identity.subject,
    });
    
    // Schedule a function to run later
    await ctx.scheduler.runAfter(
      24 * 60 * 60 * 1000, // 24 hours
      internal.tasks.sendReminder,
      { taskId }
    );
    
    return taskId;
  },
});

Mutation characteristics

  • Read-write - Can read and modify the database
  • Atomic - All writes succeed or all fail (no partial states)
  • Isolated - Concurrent mutations don’t interfere with each other
  • Reactive triggers - Automatically updates subscribed queries
  • Optimistically retried - Automatically retried on conflicts
Mutations may be retried automatically if there are concurrent conflicts. Avoid non-idempotent side effects (like sending emails) in mutations. Use actions for external side effects instead.

Database write operations

Mutations can use four write operations:
1

Insert

Add new documents to a table:
const userId = await ctx.db.insert("users", {
  name: "Alice",
  email: "[email protected]",
});
2

Patch

Shallow merge updates (only specified fields change):
await ctx.db.patch(userId, {
  name: "Alice Smith", // Update name
  // email remains unchanged
});
3

Replace

Completely replace a document:
await ctx.db.replace(userId, {
  name: "Bob",
  email: "[email protected]",
  // All other fields (except system fields) are removed
});
4

Delete

Remove a document:
await ctx.db.delete(userId);

Actions

Actions are functions that can interact with external services and use Node.js APIs. Unlike queries and mutations, they don’t have direct database access.

Basic action

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

export const processPayment = action({
  args: {
    orderId: v.id("orders"),
    amount: v.number(),
  },
  handler: async (ctx, args) => {
    // Read data via runQuery
    const order = await ctx.runQuery(internal.orders.get, {
      orderId: args.orderId,
    });
    
    // Call external API
    const response = await fetch("https://api.stripe.com/v1/charges", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,
      },
      body: JSON.stringify({
        amount: args.amount,
        currency: "usd",
        source: order.paymentToken,
      }),
    });
    
    const result = await response.json();
    
    // Write results via runMutation
    await ctx.runMutation(internal.orders.markPaid, {
      orderId: args.orderId,
      chargeId: result.id,
    });
  },
});

Action context

Actions receive a context object with:
  • ctx.runQuery - Run a query (separate read transaction)
  • ctx.runMutation - Run a mutation (separate write transaction)
  • ctx.runAction - Call another action
  • ctx.scheduler - Schedule functions to run later
  • ctx.auth - Current user authentication information
  • ctx.storage - Generate upload URLs and get file URLs
  • ctx.vectorSearch - Perform vector similarity searches
ctx.db is not available in actions. Use ctx.runQuery and ctx.runMutation to interact with the database.

Action characteristics

  • Non-transactional - Can make multiple separate database calls
  • External access - Can call third-party APIs and use Node.js libraries
  • No reactivity - Don’t automatically re-run when data changes
  • Longer timeout - Can run for several minutes (vs milliseconds for queries/mutations)
  • Environment variables - Can access process.env

When to use actions

import { action } from "./_generated/server";
import { OpenAI } from "openai";

export const generateText = action({
  args: { prompt: v.string() },
  handler: async (ctx, args) => {
    const openai = new OpenAI({
      apiKey: process.env.OPENAI_API_KEY,
    });
    
    const response = await openai.chat.completions.create({
      model: "gpt-4",
      messages: [{ role: "user", content: args.prompt }],
    });
    
    return response.choices[0].message.content;
  },
});

Argument validation

All function types support argument validation using the args field:
import { mutation } from "./_generated/server";
import { v } from "convex/values";

export const createMessage = mutation({
  args: {
    body: v.string(),
    channelId: v.id("channels"),
    metadata: v.optional(v.object({
      priority: v.union(v.literal("high"), v.literal("normal")),
      tags: v.array(v.string()),
    })),
  },
  handler: async (ctx, args) => {
    // args is fully typed based on the validator
    // TypeScript knows: args.body is string
    //                   args.channelId is Id<"channels">
    //                   args.metadata is optional
  },
});
For security, always add argument validation to public functions. This prevents users from passing unexpected or malicious input.

Return value validation

You can optionally validate return values:
import { query } from "./_generated/server";
import { v } from "convex/values";

export const getUser = query({
  args: { userId: v.id("users") },
  returns: v.object({
    _id: v.id("users"),
    _creationTime: v.number(),
    name: v.string(),
    email: v.string(),
  }),
  handler: async (ctx, args) => {
    const user = await ctx.db.get(args.userId);
    if (!user) throw new Error("User not found");
    return user;
  },
});

Public vs internal functions

Functions can be public (callable from clients) or internal (only callable from other functions):
import { query } from "./_generated/server";

// Exported from _generated/server - public by default
export const list = query({
  args: {},
  handler: async (ctx) => {
    return await ctx.db.query("messages").collect();
  },
});
Use internal functions for:
  • Administrative operations
  • Functions called by scheduled jobs
  • Helper functions shared between other functions
  • Operations that should only run server-side

Calling functions

Functions call each other using generated references:
import { mutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";

export const createOrder = mutation({
  args: { items: v.array(v.id("items")) },
  handler: async (ctx, args) => {
    const orderId = await ctx.db.insert("orders", {
      items: args.items,
      status: "pending",
    });
    
    // Schedule an action to process payment
    await ctx.scheduler.runAfter(
      0,
      internal.payments.processOrder,
      { orderId }
    );
    
    return orderId;
  },
});

Function comparison

FeatureQueryMutationAction
Database readYes (transactional)Yes (transactional)Via runQuery
Database writeNoYes (atomic)Via runMutation
External APIsNoNoYes
ReactiveYesNo (but triggers queries)No
TransactionSingle snapshotAtomic writesMultiple separate
Typical durationLess than 10msLess than 50msSeconds to minutes
Retry on conflictN/AAutomaticManual
Node.js APIsNoNoYes

Next steps

Build docs developers (and LLMs) love