Skip to main content
Convex provides two ways to run functions in the future: scheduled functions (one-time execution) and cron jobs (recurring schedules).

Scheduled functions

Schedule mutations or actions to run once at a specific time. Available via ctx.scheduler in mutations and actions.

Execution guarantees

Scheduled mutations are guaranteed to execute exactly once. They are automatically retried on transient errors.Scheduled actions execute at most once. They are not retried and may fail due to transient errors.

Scheduler interface

Access the scheduler via ctx.scheduler in mutations and actions:
import { mutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";

export const createOrder = mutation({
  args: { items: v.array(v.string()) },
  returns: v.null(),
  handler: async (ctx, args) => {
    const orderId = await ctx.db.insert("orders", { items: args.items });

    // Run immediately after this mutation commits:
    await ctx.scheduler.runAfter(0, internal.emails.sendConfirmation, {
      orderId,
    });

    // Run cleanup in 7 days:
    await ctx.scheduler.runAfter(
      7 * 24 * 60 * 60 * 1000,
      internal.orders.archiveOrder,
      { orderId }
    );

    return null;
  },
});

runAfter

Schedule a function to execute after a delay:
// Schedule to run as soon as possible (after current mutation commits):
await ctx.scheduler.runAfter(0, internal.tasks.process, { taskId });

// Run after 5 seconds:
await ctx.scheduler.runAfter(5000, internal.tasks.process, { taskId });

// Run after 1 hour:
await ctx.scheduler.runAfter(60 * 60 * 1000, internal.cleanup.run, {});
delayMs
number
Delay in milliseconds. Must be non-negative. If the delay is zero, the scheduled function will execute immediately after the scheduling function completes.
functionReference
FunctionReference<'mutation' | 'action'>
A reference to the function to schedule (e.g., internal.module.function).
args
object
Arguments to pass to the scheduled function.
Returns: The Id<"_scheduled_functions"> of the scheduled function. Use this to cancel it later if needed.

runAt

Schedule a function to execute at a specific time:
// Run at a specific Date:
await ctx.scheduler.runAt(
  new Date("2030-01-01T00:00:00Z"),
  internal.events.triggerNewYear,
  {}
);

// Run at a timestamp (milliseconds since epoch):
await ctx.scheduler.runAt(
  Date.now() + 60000,
  internal.tasks.process,
  { taskId }
);
timestamp
number | Date
A Date or timestamp (milliseconds since epoch). If the timestamp is in the past, the function executes immediately after the scheduling function completes. Must be within 5 years in the past or future.
functionReference
FunctionReference<'mutation' | 'action'>
A reference to the function to schedule.
args
object
Arguments to pass to the scheduled function.
Returns: The Id<"_scheduled_functions"> of the scheduled function.

cancel

Cancel a previously scheduled function:
await ctx.scheduler.cancel(scheduledFunctionId);
id
Id<'_scheduled_functions'>
The ID of the scheduled function to cancel (returned by runAfter or runAt).
Cancellation behavior:
  • Scheduled actions: If the action has not started, it will not run. If it is already in progress, it continues running but any new functions it schedules will be canceled. Throws an error if already completed.
  • Scheduled mutations: The mutation will either cancel entirely or fail to cancel if it has committed. Mutations are atomic transactions that either run to completion or fully roll back.

Schedulable functions

Only mutations and actions (public or internal) can be scheduled. Queries cannot be scheduled. Best practice: Use internalMutation or internalAction to ensure scheduled functions cannot be called directly from clients:
import { internalMutation } from "./_generated/server";

export const processPayment = internalMutation({
  args: { orderId: v.id("orders") },
  handler: async (ctx, args) => {
    // This can only be called from other Convex functions,
    // not directly from clients
  },
});

Cron jobs

Schedule functions to run on recurring schedules using the cronJobs() API. Define cron jobs in convex/crons.ts (or crons.js):
import { cronJobs } from "convex/server";
import { api } from "./_generated/api";

const crons = cronJobs();

crons.weekly(
  "weekly re-engagement email",
  {
    dayOfWeek: "monday",
    hourUTC: 17, // 9:30am Pacific / 10:30am Daylight Savings Pacific
    minuteUTC: 30,
  },
  api.emails.send
);

export default crons;

Cron schedules

Convex supports several schedule types:

Interval

Run every N seconds, minutes, or hours:
crons.interval(
  "Clear presence data",
  { seconds: 30 },
  api.presence.clear
);

crons.interval(
  "Sync data",
  { minutes: 5 },
  api.sync.run
);

crons.interval(
  "Generate reports",
  { hours: 24 },
  api.reports.generate
);
cronIdentifier
string
A unique name for this scheduled job.
schedule
Interval
An object with one of: seconds, minutes, or hours (number).
functionReference
FunctionReference
The function to schedule.
args
object
Arguments to pass to the function.

Hourly

Run at a specific minute past each hour:
crons.hourly(
  "Reset high scores",
  { minuteUTC: 30 }, // Run at :30 past every hour
  api.scores.reset
);
schedule.minuteUTC
number
Minutes past the hour (0-59).

Daily

Run at a specific time each day (UTC):
crons.daily(
  "Daily backup",
  {
    hourUTC: 2, // 2:00 AM UTC
    minuteUTC: 0,
  },
  api.backup.run
);
schedule.hourUTC
number
Hour of day (0-23). Remember, this is UTC.
schedule.minuteUTC
number
Minute of hour (0-59). Remember, this is UTC.

Weekly

Run at a specific day and time each week:
crons.weekly(
  "Weekly newsletter",
  {
    dayOfWeek: "monday",
    hourUTC: 9,
    minuteUTC: 0,
  },
  api.emails.sendNewsletter
);
schedule.dayOfWeek
string
Day of week: "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", or "sunday".
schedule.hourUTC
number
Hour of day (0-23). Remember to convert from your timezone to UTC.
schedule.minuteUTC
number
Minute of hour (0-59).

Monthly

Run on a specific day of the month:
crons.monthly(
  "Bill customers",
  {
    day: 1, // First day of the month
    hourUTC: 0,
    minuteUTC: 0,
  },
  api.billing.billCustomers
);
schedule.day
number
Day of month (1-31). Days greater than 28 will not run every month.
schedule.hourUTC
number
Hour of day (0-23). Remember to convert from your timezone to UTC.
schedule.minuteUTC
number
Minute of hour (0-59).
Note: Some months have fewer than 31 days, so a function scheduled for the 30th or 31st will not run in February.

Cron string

Use traditional cron syntax for complex schedules:
crons.cron(
  "Complex schedule",
  "15 7 * * *", // Every day at 7:15 AM UTC
  api.tasks.run
);
Cron string format:
 ┌─ minute (0 - 59)
 │ ┌─ hour (0 - 23)
 │ │ ┌─ day of the month (1 - 31)
 │ │ │ ┌─ month (1 - 12)
 │ │ │ │ ┌─ day of the week (0 - 6) (Sunday to Saturday)
 * * * * *
cron
string
A cron string specifying the schedule.

Common patterns

Delayed notifications

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

export const createReminder = mutation({
  args: { text: v.string(), delayMinutes: v.number() },
  handler: async (ctx, args) => {
    const reminderId = await ctx.db.insert("reminders", {
      text: args.text,
    });

    // Schedule notification
    await ctx.scheduler.runAfter(
      args.delayMinutes * 60 * 1000,
      internal.notifications.sendReminder,
      { reminderId }
    );

    return reminderId;
  },
});

export const sendReminder = internalMutation({
  args: { reminderId: v.id("reminders") },
  handler: async (ctx, args) => {
    const reminder = await ctx.db.get(args.reminderId);
    if (!reminder) return;

    // Send notification (implementation depends on notification system)
    console.log("Reminder:", reminder.text);
  },
});

Cancellable scheduled tasks

export const scheduleTask = mutation({
  args: { taskId: v.id("tasks"), delayMs: v.number() },
  handler: async (ctx, args) => {
    // Schedule the task
    const scheduledId = await ctx.scheduler.runAfter(
      args.delayMs,
      internal.tasks.execute,
      { taskId: args.taskId }
    );

    // Store the scheduled function ID so we can cancel it later
    await ctx.db.patch(args.taskId, { scheduledFunctionId: scheduledId });

    return scheduledId;
  },
});

export const cancelTask = mutation({
  args: { taskId: v.id("tasks") },
  handler: async (ctx, args) => {
    const task = await ctx.db.get(args.taskId);
    if (!task?.scheduledFunctionId) return;

    // Cancel the scheduled function
    await ctx.scheduler.cancel(task.scheduledFunctionId);

    // Clear the scheduled ID
    await ctx.db.patch(args.taskId, { scheduledFunctionId: undefined });
  },
});

Recurring cleanup with cron

// convex/crons.ts
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";

const crons = cronJobs();

// Clean up expired sessions daily at 3 AM UTC
crons.daily(
  "cleanup expired sessions",
  { hourUTC: 3, minuteUTC: 0 },
  internal.cleanup.expiredSessions
);

// Send weekly summary every Sunday at 9 AM UTC
crons.weekly(
  "weekly summary",
  { dayOfWeek: "sunday", hourUTC: 9, minuteUTC: 0 },
  internal.emails.sendWeeklySummary
);

export default crons;
// convex/cleanup.ts
import { internalMutation } from "./_generated/server";

export const expiredSessions = internalMutation({
  args: {},
  handler: async (ctx) => {
    const expiredSessions = await ctx.db
      .query("sessions")
      .filter((q) => q.lt(q.field("expiresAt"), Date.now()))
      .collect();

    for (const session of expiredSessions) {
      await ctx.db.delete(session._id);
    }

    return { deleted: expiredSessions.length };
  },
});

Best practices

  • Use internal functions - Make scheduled functions internalMutation or internalAction to prevent direct client access.
  • Store scheduled IDs for cancellation - Save the returned ID if you need to cancel the scheduled function later.
  • Handle missing data gracefully - Scheduled functions may run after related data is deleted.
  • Remember UTC for crons - All times in cron jobs are UTC. Convert from your local timezone.
  • Use mutations for guaranteed execution - Scheduled mutations retry on failure, actions do not.
  • Avoid long delays in mutations - Schedule actions for long-running operations, not mutations.

Build docs developers (and LLMs) love