Shaw
Shawโ€ข5mo ago

Programmatically creating cron jobs

I've been attempting to programmatically set up cron jobs for different users with different cron expressions, calling them via actions and mutations but with no luck. It seems scouring other docs and other peoples questions that this isn't currently possible or even considered an anti-pattern. I'm not sure how to implement my use case just using scheduled functions. My current implementation:
import { cronJobs } from "convex/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";
import { internalAction } from "./_generated/server";

const crons = cronJobs();

export const scheduleRecurringReminder = internalAction({
args: {
reminderId: v.id("reminders"),
cronExpression: v.string(),
},
handler: async (_ctx, { reminderId, cronExpression }) => {
console.info(
`Scheduling recurring reminder ${reminderId} with cron expression ${cronExpression}`,
);
crons.cron(
`Job ${reminderId}`,
cronExpression,
internal.reminders.scheduled.sendReminder,
{
reminderId,
},
);
},
});

export default crons;
import { cronJobs } from "convex/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";
import { internalAction } from "./_generated/server";

const crons = cronJobs();

export const scheduleRecurringReminder = internalAction({
args: {
reminderId: v.id("reminders"),
cronExpression: v.string(),
},
handler: async (_ctx, { reminderId, cronExpression }) => {
console.info(
`Scheduling recurring reminder ${reminderId} with cron expression ${cronExpression}`,
);
crons.cron(
`Job ${reminderId}`,
cronExpression,
internal.reminders.scheduled.sendReminder,
{
reminderId,
},
);
},
});

export default crons;
If I need to do this via scheduled functions, not exactly sure where to start. For simple cron expressions it seems doable, but for my complex like:
00 10 * * 2,4
00 10 * * 2,4
would be At 10:00 AM, only on Tuesday and Thursday Not sure how'd I structure scheduled functions to get me the same behavior as a cron. Any advice appreciated! Thank in advanc ๐Ÿ™‚
8 Replies
jamwt
jamwtโ€ข5mo ago
hi! our CTO @james recently did this as a fun project: https://stack.convex.dev/cron-jobs
Cron Jobs in User Space
Even though Convex supports the creation of cron jobs out of the box, until now, we've only supported static jobs. With user space crons, we now suppo...
jamwt
jamwtโ€ข5mo ago
hopefully his code can be useful here he's also going to turn this into a component when we ship components, so you can just install the component and then use its engine to do programmable jobs at runtime etc.
jamwt
jamwtโ€ข5mo ago
but in the mean time, feel free to steal anything from the "cronvex" codebase (https://github.com/JamesCowling/cronvex)
GitHub
GitHub - jamescowling/cronvex: Send http requests on a periodic sch...
Send http requests on a periodic schedule. Contribute to jamescowling/cronvex development by creating an account on GitHub.
jamwt
jamwtโ€ข5mo ago
running version: https://www.cronvex.com/
Cronvex
Send http requests on a periodic schedule. Cronvex is free and built as a demonstration of scheduled jobs on Convex.
jamwt
jamwtโ€ข5mo ago
GitHub
cronvex/convex/cronlib.ts at main ยท jamescowling/cronvex
Send http requests on a periodic schedule. Contribute to jamescowling/cronvex development by creating an account on GitHub.
jamwt
jamwtโ€ข5mo ago
and you're right that using the built-in "crons" object for this isn't intended it's mean to be fully specified at deploy time this (among other limitations) is why we're investigating to switching to something like james's approach as the primary one after the component framework ships
Shaw
ShawOPโ€ข5mo ago
Amazing, I'll take a look. Just asking the question help me come up with an dirty interim solution:
export const scheduleRecurringReminder = internalAction({
args: {
reminderId: v.id("reminders"),
cronExpression: v.string(),
},
handler: async (ctx, { reminderId, cronExpression }) => {
const interval = cronParser.parseExpression(cronExpression);
const nextDateTime = interval.next().toDate();
const now = new Date();
const delay = nextDateTime.getTime() - now.getTime();

// Schedule the next run
await ctx.scheduler.runAfter(
delay,
internal.reminders.scheduled.sendReminder,
{
reminderId,
},
);

// Reschedule this function to run again
await ctx.scheduler.runAfter(
delay + 1000, // Add 1 second to ensure we're past the execution time
internal.reminders.scheduled.scheduleRecurringReminder,
{
reminderId,
cronExpression,
},
);
},
});
export const scheduleRecurringReminder = internalAction({
args: {
reminderId: v.id("reminders"),
cronExpression: v.string(),
},
handler: async (ctx, { reminderId, cronExpression }) => {
const interval = cronParser.parseExpression(cronExpression);
const nextDateTime = interval.next().toDate();
const now = new Date();
const delay = nextDateTime.getTime() - now.getTime();

// Schedule the next run
await ctx.scheduler.runAfter(
delay,
internal.reminders.scheduled.sendReminder,
{
reminderId,
},
);

// Reschedule this function to run again
await ctx.scheduler.runAfter(
delay + 1000, // Add 1 second to ensure we're past the execution time
internal.reminders.scheduled.scheduleRecurringReminder,
{
reminderId,
cronExpression,
},
);
},
});
Just to recursively set up the next schedule function every time the current one is ran, will see. Thanks for the quick response!
jamwt
jamwtโ€ข5mo ago
yeah, if you want to kick off your own loop, a self-scheduling mutation will do it quick note: don't try to self-schedule an action. actions can fail so there's a decent chance an ephemeral failure causes your job to stop running have an 'outer' mutation that does two things 1. reschedules itself at INTERVAL 2. schedules the effectful action right now (runAfter(0, ...)) the mutation will retry etc until success the action could fail and then you've "lost" your background job

Did you find this page helpful?