> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/get-convex/convex-backend/llms.txt
> Use this file to discover all available pages before exploring further.

# Scheduled functions and cron jobs

> Schedule functions to run in the future or on recurring schedules

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

<Note>
  **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.
</Note>

### Scheduler interface

Access the scheduler via `ctx.scheduler` in mutations and actions:

```typescript theme={null}
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:

```typescript theme={null}
// 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, {});
```

<ParamField path="delayMs" type="number">
  Delay in milliseconds. Must be non-negative. If the delay is zero, the scheduled function will execute immediately after the scheduling function completes.
</ParamField>

<ParamField path="functionReference" type="FunctionReference<'mutation' | 'action'>">
  A reference to the function to schedule (e.g., `internal.module.function`).
</ParamField>

<ParamField path="args" type="object">
  Arguments to pass to the scheduled function.
</ParamField>

**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:

```typescript theme={null}
// 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 }
);
```

<ParamField path="timestamp" type="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.
</ParamField>

<ParamField path="functionReference" type="FunctionReference<'mutation' | 'action'>">
  A reference to the function to schedule.
</ParamField>

<ParamField path="args" type="object">
  Arguments to pass to the scheduled function.
</ParamField>

**Returns:** The `Id<"_scheduled_functions">` of the scheduled function.

### cancel

Cancel a previously scheduled function:

```typescript theme={null}
await ctx.scheduler.cancel(scheduledFunctionId);
```

<ParamField path="id" type="Id<'_scheduled_functions'>">
  The ID of the scheduled function to cancel (returned by `runAfter` or `runAt`).
</ParamField>

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:

```typescript theme={null}
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`):

```typescript theme={null}
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:

```typescript theme={null}
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
);
```

<ParamField path="cronIdentifier" type="string">
  A unique name for this scheduled job.
</ParamField>

<ParamField path="schedule" type="Interval">
  An object with one of: `seconds`, `minutes`, or `hours` (number).
</ParamField>

<ParamField path="functionReference" type="FunctionReference">
  The function to schedule.
</ParamField>

<ParamField path="args" type="object" optional>
  Arguments to pass to the function.
</ParamField>

#### Hourly

Run at a specific minute past each hour:

```typescript theme={null}
crons.hourly(
  "Reset high scores",
  { minuteUTC: 30 }, // Run at :30 past every hour
  api.scores.reset
);
```

<ParamField path="schedule.minuteUTC" type="number">
  Minutes past the hour (0-59).
</ParamField>

#### Daily

Run at a specific time each day (UTC):

```typescript theme={null}
crons.daily(
  "Daily backup",
  {
    hourUTC: 2, // 2:00 AM UTC
    minuteUTC: 0,
  },
  api.backup.run
);
```

<ParamField path="schedule.hourUTC" type="number">
  Hour of day (0-23). Remember, this is UTC.
</ParamField>

<ParamField path="schedule.minuteUTC" type="number">
  Minute of hour (0-59). Remember, this is UTC.
</ParamField>

#### Weekly

Run at a specific day and time each week:

```typescript theme={null}
crons.weekly(
  "Weekly newsletter",
  {
    dayOfWeek: "monday",
    hourUTC: 9,
    minuteUTC: 0,
  },
  api.emails.sendNewsletter
);
```

<ParamField path="schedule.dayOfWeek" type="string">
  Day of week: `"monday"`, `"tuesday"`, `"wednesday"`, `"thursday"`, `"friday"`, `"saturday"`, or `"sunday"`.
</ParamField>

<ParamField path="schedule.hourUTC" type="number">
  Hour of day (0-23). Remember to convert from your timezone to UTC.
</ParamField>

<ParamField path="schedule.minuteUTC" type="number">
  Minute of hour (0-59).
</ParamField>

#### Monthly

Run on a specific day of the month:

```typescript theme={null}
crons.monthly(
  "Bill customers",
  {
    day: 1, // First day of the month
    hourUTC: 0,
    minuteUTC: 0,
  },
  api.billing.billCustomers
);
```

<ParamField path="schedule.day" type="number">
  Day of month (1-31). Days greater than 28 will not run every month.
</ParamField>

<ParamField path="schedule.hourUTC" type="number">
  Hour of day (0-23). Remember to convert from your timezone to UTC.
</ParamField>

<ParamField path="schedule.minuteUTC" type="number">
  Minute of hour (0-59).
</ParamField>

**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:

```typescript theme={null}
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)
 * * * * *
```

<ParamField path="cron" type="string">
  A cron string specifying the schedule.
</ParamField>

## Common patterns

### Delayed notifications

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
// 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;
```

```typescript theme={null}
// 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.
