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:
Insert
Add new documents to a table: const userId = await ctx . db . insert ( "users" , {
name: "Alice" ,
email: "[email protected] " ,
});
Patch
Shallow merge updates (only specified fields change): await ctx . db . patch ( userId , {
name: "Alice Smith" , // Update name
// email remains unchanged
});
Replace
Completely replace a document: await ctx . db . replace ( userId , {
name: "Bob" ,
email: "[email protected] " ,
// All other fields (except system fields) are removed
});
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
Calling external APIs
Sending emails
Complex computation
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):
Public function
Internal function
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
Feature Query Mutation Action Database read Yes (transactional) Yes (transactional) Via runQuery Database write No Yes (atomic) Via runMutation External APIs No No Yes Reactive Yes No (but triggers queries) No Transaction Single snapshot Atomic writes Multiple separate Typical duration Less than 10ms Less than 50ms Seconds to minutes Retry on conflict N/A Automatic Manual Node.js APIs No No Yes
Next steps