Scheduled Jobs
In this chapter, you'll learn about scheduled jobs and how to use them.
What is a Scheduled Job?#
When building your commerce application, you may need to automate tasks and run them repeatedly at a specific schedule. For example, you need to automatically sync products to a third-party service once a day.
In other commerce platforms, this feature isn't natively supported. Instead, you have to set up a separate application to execute cron jobs, which adds complexity as to how you expose this task to be executed in a cron job, or how you debug it when it's not running within the platform's tooling.
MedusaGo removes this overhead by supporting this feature natively with scheduled jobs. A scheduled job is a function that the MedusaGo application runs at the interval you specify during the application's runtime. Your efforts are only spent on implementing the functionality performed by the job, such as syncing products to an ERP.
- You want the action to execute at a specified schedule while the MedusaGo application isn't running. Instead, create a custom CLI script and execute it using the operating system's equivalent of a cron job.
- You want to execute the action once when the application loads. Use loaders or custom CLI scripts instead.
- You want to execute the action if an event occurs. Use subscribers instead.
How to Create a Scheduled Job?#
A scheduled job has two parts: the function to run, and a CronJobConfig from the framework/src/jobs package that indicates the schedule to run the function.
For example, create the following job in the src/jobs directory of your project:
// greetingJob is the function the job runs. It receives the container, and
// a context with the time the run was scheduled for.
func greetingJob(ctx context.Context, container composer.Container, jc frameworkjobs.ScheduledJobContext) (any, error) {
logger, _ := container.Resolve(common.ContainerRegistrationKeys.LOGGER).(*slog.Logger)
logger.Info("Greeting!", "scheduled_for", jc.ScheduledFor)
return nil, nil
}
// greetingConfig holds the name of the job, which must be unique, and its
// schedule, a cron expression.
var greetingConfig = &frameworkjobs.CronJobConfig{
Name: "greeting-every-minute",
Schedule: "* * * * *",
}
The job function is of type ScheduledJobHandler. It receives:
- A
context.Context. - The Medusa container, which you use to resolve resources. In the function, you resolve the Logger from the container and log a message.
- A
ScheduledJobContext, which has the time the run was scheduled for.
The function returns a value and an error. The value is the job's result, and you can return nil if you don't need it. If the function returns an error, it's logged and the job's run fails. The job is still run at its next scheduled time.
The CronJobConfig has the following properties:
Name: A unique name for the job.Schedule: A string that holds a cron expression indicating the schedule to run the job. The expression has five fields, or six if you want to specify the seconds.ScheduleOptions: (optional) Options to schedule the job in another way. Learn more in the Schedule Options section. You must set eitherScheduleorScheduleOptions.
This scheduled job executes every minute and logs the message Greeting!.
Register the Scheduled Job#
MedusaGo doesn't scan the file system for scheduled jobs. You register a job with the Register function of the jobs package, in a function that you pass in the OnLoad option of the CLI. The CLI runs these functions after the application is loaded.
For example:
// RegisterJobs registers the jobs of the project. Pass it to
// cli.Options.OnLoad.
func RegisterJobs(ctx context.Context, app *loaders.MedusaApp) error {
if _, err := frameworkjobs.Register(greetingConfig, greetingJob); err != nil {
return err
}
if _, err := frameworkjobs.Register(syncProductsConfig, syncProductsJob); err != nil {
return err
}
return nil
}
Register returns an error if the configuration is invalid, such as when the name or the schedule is missing.
Then, pass the registration function in the main.go file of your application:
// Options are the options main.go passes to the CLI.
func Options() cli.Options {
return cli.Options{
Config: medusaConfig,
OnLoad: []func(ctx context.Context, app *loaders.MedusaApp) error{
RegisterJobs,
},
}
}
Test the Scheduled Job#
To test out your scheduled job, start the application:
After a minute, the message Greeting! is logged to the terminal.
Disable a Scheduled Job#
To disable a scheduled job, remove its Register call from your registration function. If you want to remove a job that's already registered while the application is running, call jobs.Unregister with the job's name.
Example: Sync Products Once a Day#
In a realistic scenario like syncing products to an ERP once a day, you should create a workflow and execute it in a scheduled job.
A workflow is a task made up of a series of steps, and you construct it like you would a regular function, but it's a special function that supports rollback mechanism in case of errors, background execution, and more.
You can learn how to create a workflow in the Workflows chapter, but this example assumes you already have a SyncProductsToErpWorkflow implemented. To execute this workflow once a day, create the following scheduled job:
// syncProductsJob runs the workflow once a day.
func syncProductsJob(ctx context.Context, container composer.Container, jc frameworkjobs.ScheduledJobContext) (any, error) {
_, err := SyncProductsToErpWorkflow.Run(ctx, container, struct{}{})
return nil, err
}
// syncProductsConfig schedules the job at midnight, every day.
var syncProductsConfig = &frameworkjobs.CronJobConfig{
Name: "sync-products-job",
Schedule: "0 0 * * *",
}
In the scheduled job function, you execute the SyncProductsToErpWorkflow by calling its Run method with the context and the container. You also specify in the configuration the schedule 0 0 * * * which indicates midnight time of every day.
The next time you start the application, it will run this job every day at midnight.
Check the Scheduled Time of a Job#
MedusaGo runs scheduled jobs at the pattern you specify in the Schedule property of the job's configurations. During a run, you can check the exact date and time the job was scheduled to run at using the ScheduledJobContext parameter of the scheduled job function.
The first example in this chapter logs it:
// greetingJob is the function the job runs. It receives the container, and
// a context with the time the run was scheduled for.
func greetingJob(ctx context.Context, container composer.Container, jc frameworkjobs.ScheduledJobContext) (any, error) {
logger, _ := container.Resolve(common.ContainerRegistrationKeys.LOGGER).(*slog.Logger)
logger.Info("Greeting!", "scheduled_for", jc.ScheduledFor)
return nil, nil
}
// greetingConfig holds the name of the job, which must be unique, and its
// schedule, a cron expression.
var greetingConfig = &frameworkjobs.CronJobConfig{
Name: "greeting-every-minute",
Schedule: "* * * * *",
}
The ScheduledJobContext parameter is a struct that has the following property:
ScheduledFor: Atime.Timevalue indicating the exact date and time the job was scheduled to run at.
Schedule Options#
To schedule a job at a fixed interval, or to limit how many times it runs, use the ScheduleOptions property of the configuration. It's a *composer.SchedulerOptions from the workflows SDK package, which has the following properties:
Cron: A cron expression. It's whatSchedulesets.Interval: The time between runs, in milliseconds. Set eitherCronorInterval.NumberOfExecutions: (optional) A pointer to the number of times the job runs. After that, the job isn't scheduled again.
For example:
// reportConfig runs the job every ten seconds, five times: the job isn't
// scheduled again after that. new(5) is a pointer to the number 5.
var reportConfig = &frameworkjobs.CronJobConfig{
Name: "report-job",
ScheduleOptions: &composer.SchedulerOptions{
// the interval is in milliseconds
Interval: 10_000,
NumberOfExecutions: new(5),
},
}
Scheduled Jobs in Multiple Instances#
Scheduled jobs are run by the workflow engine module:
- With the in-memory workflow engine, which is the default, every process of your application runs the jobs it registered on its own timers. If you run multiple instances of the application, each instance runs every job.
- With the Redis workflow engine, the schedules are stored in Redis, and each scheduled run is processed by one instance that isn't in
serverworker mode. Register the jobs in every instance, since the instance that picks up a run must have the job registered. If it doesn't, the schedule is removed.