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, {});
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).
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 }
);
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.
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
);
A unique name for this scheduled job.
An object with one of: seconds, minutes, or hours (number).
The function to schedule.
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
);
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
);
Hour of day (0-23). Remember, this is UTC.
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
);
Day of week: "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", or "sunday".
Hour of day (0-23). Remember to convert from your timezone to UTC.
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
);
Day of month (1-31). Days greater than 28 will not run every month.
Hour of day (0-23). Remember to convert from your timezone to UTC.
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)
* * * * *
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.