kitcn

Mutations

Insert, update, and delete operations with Drizzle-style builders

Dedicated Pages

Each mutation type has its own detailed page:

Setup

All mutation examples assume you attached ORM to ctx.orm once in your context (see Quickstart).

Typical import block for mutation handlers:

convex/functions/users.ts
import { z } from 'zod';
import { eq } from 'kitcn/orm';
import { publicMutation } from '../lib/crpc';
import { users } from '../schema';

Shared Concepts

Concepts that apply across insert, update, and delete.

Returning

Every mutation builder supports .returning() to get back the affected rows. Without it, the result is void.

You can return all fields or pick specific columns:

// Return all fields
const [user] = await ctx.orm
  .insert(users)
  .values({ name: 'Ada', email: 'ada@example.com' })
  .returning();

// Return specific fields
const [partial] = await ctx.orm
  .insert(users)
  .values({ name: 'Ada', email: 'ada@example.com' })
  .returning({ id: users.id, email: users.email });

Note: .returning() always returns an array, even for single-row mutations.

Where Clauses

update() and delete() require a .where(...) clause by default. Calling without one throws an error unless you explicitly opt in with .allowFullScan().

// Target specific rows with where
await ctx.orm.update(users).set({ name: 'Updated' }).where(eq(users.id, userId));

// Operate on ALL rows (use with care)
await ctx.orm.delete(users).allowFullScan();

For more on allowFullScan and index compilation, see Querying Data.

Atomicity

Important: Convex mutations are atomic -- all changes in a single mutation call succeed or fail together.

The ORM enforces runtime limits on how many rows a single mutation can touch. See API Reference -- Safety Limits for defaults and override syntax.

Paginated Mutation Batches

For large update/delete workloads that exceed safety limits, you can process rows page-by-page with .paginate(). This requires an index on the filtered field:

// Schema: index('by_role').on(t.role) on users table
const batch = await ctx.orm
  .update(users)
  .set({ role: 'member' })
  .where(eq(users.role, 'pending'))
  .paginate({ cursor: null, limit: 100 });

The paginated result includes:

  • continueCursor -- cursor for the next batch
  • isDone -- true when no more pages remain
  • numAffected -- rows affected in this page
  • page -- returned rows (only when .returning() is used)

Async Mutation Batching

Mutations that affect large sets of rows run in async mode by default. The first batch runs in the current mutation, then remaining batches are scheduled automatically through the Convex scheduler.

convex/functions/users.ts
import { eq } from 'kitcn/orm';
import { publicMutation } from '../lib/crpc';
import { users } from '../schema';

export const renameUsers = publicMutation.mutation(async ({ ctx }) => {
  await ctx.orm
    .update(users)
    .set({ name: 'Updated' })
    .where(eq(users.role, 'pending')); // async by default
});

You can customize batch size and delay per call:

await ctx.orm
  .update(users)
  .set({ name: 'Updated' })
  .where(eq(users.role, 'pending'))
  .execute({ batchSize: 200, delayMs: 0 });

Sync Mode

To force all rows to be processed in a single transaction (no scheduling), opt into sync mode:

  • Per call: .execute({ mode: 'sync' })
  • Global default: defineSchema(..., { defaults: { mutationExecutionMode: 'sync' } })

Sync mode throws if matched rows exceed mutationMaxRows.

API Reference

Safety Limits

The ORM enforces runtime limits on how many rows a single mutation can touch. The key defaults are:

  • mutationBatchSize: 400 (page size for collecting matched rows)
  • mutationMaxRows: 10000 (sync-mode hard cap)
  • mutationLeafBatchSize: 1600 (async FK fan-out batch size)

You can customize these in your schema definition. For the full list of configurable defaults, see Schema Definition -- Runtime Defaults.

Next Steps

On this page