OneBase

Scheduling & crons

#Scheduling a function

// Run in 5 minutes
await ctx.scheduler.runAfter(5 * 60_000, internal.emails.sendReminder, { userId });

// Run at a specific time
const jobId = await ctx.scheduler.runAt(new Date("2026-12-31T23:59:00Z"), internal.party.start, {});

// Cancel a job that hasn't run yet
await ctx.scheduler.cancel(jobId);
  • From a mutation: the job is created only if the mutation commits. Throw, and nothing is scheduled.
  • Storage: jobs are kept in the database, so they survive restarts. A job that was running when the server stopped runs again on the next start.
  • Mutations run exactly once: a scheduled mutation is marked done inside its own transaction.
  • Actions and queries run at most once, unless the server crashes mid-run (then the job is retried).
  • Failures: a failed job is logged and kept with its error for 7 days.
  • Several instances: each job is claimed by exactly one instance.

A common pattern is to start long work from a mutation without making the user wait:

export const requestSummary = mutation({
  args: { id: v.id("posts") },
  handler: async (ctx, { id }) => {
    await ctx.db.patch(id, { summary: "pending" });
    await ctx.scheduler.runAfter(0, internal.ai.summarize, { id }); // an action
  },
});

Scheduled functions all start when they're due and aren't retried. For many jobs at once, a limit on how many run together, retries or a callback when each finishes, use the work queue.

#Cron jobs

Declare recurring jobs in onebase/crons.ts:

import { cronJobs } from "onebase/server";
import { internal } from "./_generated/api";

const crons = cronJobs();

crons.interval("sync feeds", { minutes: 15 }, internal.feeds.sync);
crons.hourly("rollup", { minuteUTC: 5 }, internal.stats.rollup);
crons.daily("prune", { hourUTC: 3, minuteUTC: 0 }, internal.messages.prune, { days: 30 });
crons.weekly("digest", { dayOfWeek: "monday", hourUTC: 8, minuteUTC: 0 }, internal.email.digest);
crons.monthly("invoices", { day: 1, hourUTC: 6, minuteUTC: 0 }, internal.billing.invoice);
crons.cron("weekday mornings", "30 9 * * mon-fri", internal.reports.daily); // standard 5-field cron

export default crons;
  • Times: all times are UTC.
  • Missed runs: the next run time is stored in the database. A restart doesn't repeat or skip runs, and a run missed while the server was down happens once on startup.
  • No overlap: a job that is still running when it comes due again is skipped.
  • Several instances: each run happens on exactly one of them.
  • Changing a schedule: edit crons.ts. Changed jobs get a new schedule, and removed jobs are deleted.

onebase doctor reports crons that point at functions that don't exist, and the server refuses to start with one.