convex-scheduling
SKILL.md
Convex Scheduling — Delayed Functions & Cron Jobs
Schedule functions for future execution and define recurring cron jobs — durable, no external infrastructure needed.
Scheduling API
runAfter — delay in milliseconds
import { internal } from "./_generated/api";
// Schedule deletion in 5 seconds
const scheduledId = await ctx.scheduler.runAfter(5000, internal.messages.destruct, {
messageId: id,
});
runAt — specific timestamp (ms since epoch)
await ctx.scheduler.runAt(args.remindAt, internal.reminders.send, { reminderId });
runAfter(0) — immediate, conditional on mutation success
Like setTimeout(fn, 0). Use to trigger an action from a mutation — only runs if mutation succeeds.
export const createUser = mutation({
handler: async (ctx, args) => {
const userId = await ctx.db.insert("users", args);
// Only runs if insert succeeds (atomic with mutation)
await ctx.scheduler.runAfter(0, internal.users.generateAIProfile, { userId });
return userId;
},
});
cancel
await ctx.scheduler.cancel(scheduledFunctionId);
| State when canceled | Behavior |
|---|---|
| Not started | Won't run |
| Already started | Continues, but its scheduled children won't run |
Mutation vs Action Scheduling
| Scheduling from | Atomicity | On failure |
|---|---|---|
| Mutation | Atomic with the rest of the mutation | Nothing scheduled if mutation fails |
| Action | NOT atomic | Scheduled functions still execute even if action throws |
Rule: Prefer scheduling from mutations for guaranteed consistency. If scheduling from an action, be aware that scheduled children survive parent failure.
Tracking Status
runAfter/runAt return a Id<"_scheduled_functions">. Query the system table:
// Get all scheduled functions
const all = await ctx.db.system.query("_scheduled_functions").collect();
// Get specific one
const fn = await ctx.db.system.get(scheduledId);
// fn.state.kind: "pending" | "inProgress" | "success" | "failed" | "canceled"
// fn.scheduledTime, fn.completedTime, fn.name, fn.args
Results available for 7 days after completion.
Cancellable Pattern
Store the scheduled ID to allow cancellation later:
export const createReminder = mutation({
handler: async (ctx, args) => {
const scheduledId = await ctx.scheduler.runAfter(
args.delayMs, internal.reminders.send, { message: args.message }
);
return await ctx.db.insert("reminders", {
message: args.message,
scheduledFunctionId: scheduledId,
status: "scheduled",
});
},
});
export const cancelReminder = mutation({
args: { reminderId: v.id("reminders") },
handler: async (ctx, { reminderId }) => {
const reminder = await ctx.db.get(reminderId);
if (!reminder) throw new Error("Not found");
await ctx.scheduler.cancel(reminder.scheduledFunctionId);
await ctx.db.patch(reminderId, { status: "canceled" });
},
});
Error Handling
| Function type | Execution guarantee | Auto-retry |
|---|---|---|
| Scheduled mutation | Exactly once | Yes (internal errors) |
| Scheduled action | At most once | No — permanently fails on transient errors/timeout |
Retry pattern for actions
export const reliableAction = internalAction({
args: { taskId: v.id("tasks") },
handler: async (ctx, args) => {
try {
await fetch("https://api.example.com/process");
await ctx.runMutation(internal.tasks.markComplete, { taskId: args.taskId });
} catch (error) {
// Schedule retry through a mutation (atomic retry count check)
await ctx.scheduler.runAfter(60000, internal.tasks.retryAction, {
taskId: args.taskId,
});
}
},
});
export const retryAction = internalMutation({
args: { taskId: v.id("tasks") },
handler: async (ctx, { taskId }) => {
const task = await ctx.db.get(taskId);
if (task?.completed) return;
if ((task?.retries ?? 0) >= 3) {
await ctx.db.patch(taskId, { status: "failed" });
return;
}
await ctx.db.patch(taskId, { retries: (task?.retries ?? 0) + 1 });
await ctx.scheduler.runAfter(0, internal.tasks.reliableAction, { taskId });
},
});
Authentication
Auth is NOT propagated from the scheduling function to the scheduled function. Pass user info explicitly:
export const scheduleUserTask = mutation({
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
await ctx.scheduler.runAfter(5000, internal.tasks.process, {
userId: identity.subject, // pass explicitly
taskData: args.taskData,
});
},
});
Cron Jobs
Define in convex/crons.ts:
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";
const crons = cronJobs();
// Interval (first run on deploy)
crons.interval("check queue", { seconds: 30 }, internal.queue.process);
crons.interval("cleanup", { minutes: 5 }, internal.files.cleanTemp);
crons.interval("sync", { hours: 2 }, internal.sync.fetchExternal);
// Hourly
crons.hourly("metrics", { minuteUTC: 0 }, internal.metrics.collect);
// Daily
crons.daily("digest", { hourUTC: 8, minuteUTC: 0 }, internal.emails.sendDigest);
// Weekly (dayOfWeek: "monday" | "tuesday" | ... | "sunday")
crons.weekly("backup", { dayOfWeek: "sunday", hourUTC: 2, minuteUTC: 0 }, internal.backup.run);
// Monthly
crons.monthly("billing", { day: 1, hourUTC: 9, minuteUTC: 0 }, internal.billing.process);
// Traditional cron syntax (UTC) — "minute hour day-of-month month day-of-week"
crons.cron("weekday reminder", "0 17 * * 1-5", internal.reminders.send);
// With arguments
crons.daily("report", { hourUTC: 8, minuteUTC: 0 }, internal.reports.generate, {
type: "daily",
});
export default crons;
Cron rules:
- At most ONE run executing at a time per cron job
- If a run takes too long, following runs may be skipped (logged in dashboard)
- Same error guarantees as scheduled functions (mutations: exactly once, actions: at most once)
Limits & Rules
| Limit | Value |
|---|---|
| Max scheduled per mutation/action | 1000 |
| Max total argument size | 8 MB |
| Results retention | 7 days |
| Auth propagation | None — pass userId explicitly |
| Mutation scheduling | Atomic (all-or-nothing) |
| Action scheduling | Non-atomic (survives failure) |
| Cron concurrency | At most 1 concurrent run per job |
Reference Files
- Full examples: Self-destructing messages, payment reminders, background job queue, rate limiting, complete cron file → See references/examples.md
Weekly Installs
9
Repository
imfa-solutions/skillsGitHub Stars
1
First Seen
Feb 24, 2026
Security Audits
Installed on
opencode9
gemini-cli9
antigravity9
claude-code9
github-copilot9
codex9