Skip to main content
Mutations allow you to write data to your Convex database. All operations within a mutation are atomic and isolated.

Defining mutations

mutation

Define a public mutation function that can be called from clients.
import { mutation } from "./_generated/server";
import { v } from "convex/values";

export const createTask = mutation({
  args: {
    text: v.string(),
    completed: v.optional(v.boolean()),
  },
  returns: v.id("tasks"),
  handler: async (ctx, args) => {
    const taskId = await ctx.db.insert("tasks", {
      text: args.text,
      completed: args.completed ?? false,
    });
    return taskId;
  },
});
args
PropertyValidators
Argument validation object using validators from convex/values.
args: {
  taskId: v.id("tasks"),
  completed: v.boolean(),
}
returns
Validator
Return value validator for type safety and validation.
returns: v.id("tasks")
handler
function
required
The implementation function that receives a MutationCtx and validated arguments.
ctx
MutationCtx
Mutation context with read-write database access.
args
object
Validated arguments matching the args validator.

internalMutation

Define an internal mutation that can only be called from other Convex functions.
import { internalMutation } from "./_generated/server";
import { v } from "convex/values";

export const deleteUser = internalMutation({
  args: { userId: v.id("users") },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.delete(args.userId);
    return null;
  },
});
Internal mutations are recommended for:
  • Mutations called from actions or scheduled functions
  • Administrative operations
  • Mutations that should not be exposed to clients

Mutation context

MutationCtx

The context object passed to mutation handlers.
db
DatabaseWriter
Read-write database interface. Provides all read methods plus insert, patch, replace, and delete.
// Insert a document
const id = await ctx.db.insert("tasks", { text: "Buy milk" });

// Update specific fields
await ctx.db.patch(id, { completed: true });

// Replace entire document
await ctx.db.replace(id, { text: "Buy milk", completed: true });

// Delete a document
await ctx.db.delete(id);
See Database API for complete details.
auth
Auth
Authentication interface to get the current user’s identity.
const identity = await ctx.auth.getUserIdentity();
if (identity === null) {
  throw new Error("Not authenticated");
}
const userId = identity.subject;
storage
StorageWriter
File storage interface with read and write capabilities.
// Generate upload URL for client
const uploadUrl = await ctx.storage.generateUploadUrl();

// Delete a file
await ctx.storage.delete(storageId);
See Storage API for details.
scheduler
Scheduler
Schedule functions to run in the future.
// Run immediately after this mutation commits
await ctx.scheduler.runAfter(0, internal.emails.send, { userId });

// Run in 24 hours
await ctx.scheduler.runAfter(24 * 60 * 60 * 1000, internal.cleanup.run, {});
See Scheduler API for details.
runQuery
function
Call a query function within the same transaction.
const user = await ctx.runQuery(internal.users.get, { userId });
The query runs within the same transaction, seeing a consistent snapshot of the database.
runMutation
function
Call a mutation function within the same transaction.
await ctx.runMutation(internal.analytics.track, { event: "task_created" });
The mutation runs in a sub-transaction. If it throws an error, all of its writes will be rolled back.

Database operations

Mutations have access to all database write operations:

insert

Insert a new document into a table.
const userId = await ctx.db.insert("users", {
  name: "Alice",
  email: "[email protected]",
});

patch

Shallow merge updates into an existing document.
// Update only the "name" field, leaving other fields unchanged
await ctx.db.patch(userId, { name: "Alice Smith" });

// Remove an optional field by setting it to undefined
await ctx.db.patch(userId, { nickname: undefined });

replace

Completely replace a document with new values.
await ctx.db.replace(userId, {
  name: "Bob",
  email: "[email protected]",
});

delete

Delete a document from the database.
await ctx.db.delete(userId);

// Delete multiple documents
const oldTasks = await ctx.db
  .query("tasks")
  .withIndex("by_completed", (q) => q.eq("completed", true))
  .collect();
for (const task of oldTasks) {
  await ctx.db.delete(task._id);
}

Transaction guarantees

Convex guarantees that all operations within a single mutation are:

Atomic

All writes either succeed together or fail together. You never have to worry about partial writes leaving your data in an inconsistent state.

Isolated

Each mutation sees a consistent snapshot of the database. Concurrent mutations don’t interfere with each other.

Serializable

Mutations execute as if they ran one at a time in some order, even when running concurrently.

Best practices

Always validate arguments

For security, add argument validation to all public mutations in production apps.

Use internal mutations

For mutations called only from actions or scheduled functions, use internalMutation to prevent direct client calls.

Keep mutations focused

Each mutation should do one logical operation. Break complex operations into multiple mutations if needed.

Return useful values

Return the new document ID or other useful information for the client.

Build docs developers (and LLMs) love