Documentation Index Fetch the complete documentation index at: https://mintlify.com/medusajs/medusa/llms.txt
Use this file to discover all available pages before exploring further.
Scheduled jobs allow you to run background tasks on a recurring schedule. They’re useful for cleanup operations, data synchronization, report generation, and other periodic maintenance tasks.
What is a Scheduled Job?
A scheduled job:
Runs automatically on a defined schedule (cron expression)
Executes in the background without blocking requests
Has access to the dependency injection container
Can execute workflows and access services
Is defined using a configuration export
Creating a Basic Scheduled Job
Create the Job File
Create a file in src/jobs/ with a default export function and schedule configuration: src/jobs/cleanup-expired-brands.ts
import { MedusaContainer } from "@medusajs/framework/types"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
export default async function cleanupExpiredBrands (
container : MedusaContainer
) {
const logger = container . resolve ( ContainerRegistrationKeys . LOGGER )
logger . info ( "Starting cleanup of expired brands" )
// Your cleanup logic here
}
export const config = {
name: "cleanup-expired-brands" ,
schedule: "0 0 * * *" , // Run daily at midnight
}
Access Services
Resolve services from the container: import { BRAND_MODULE } from "../modules/brand"
import { IBrandModuleService } from "../modules/brand/types"
export default async function cleanupExpiredBrands (
container : MedusaContainer
) {
const logger = container . resolve ( "logger" )
const brandService = container . resolve < IBrandModuleService >( BRAND_MODULE )
const expiredDate = new Date ()
expiredDate . setMonth ( expiredDate . getMonth () - 6 )
const brands = await brandService . listBrands ({
is_active: false ,
updated_at: {
$lt: expiredDate ,
},
})
if ( brands . length > 0 ) {
const ids = brands . map (( b ) => b . id )
await brandService . deleteBrands ( ids )
logger . info ( `Deleted ${ brands . length } expired brands` )
}
}
export const config = {
name: "cleanup-expired-brands" ,
schedule: "0 0 * * *" ,
}
Execute Workflows
Run workflows from scheduled jobs: import { Modules } from "@medusajs/framework/utils"
import { IWorkflowEngineService } from "@medusajs/framework/types"
export default async function generateDailyReport (
container : MedusaContainer
) {
const workflowEngine = container . resolve < IWorkflowEngineService >(
Modules . WORKFLOW_ENGINE
)
await workflowEngine . run ( "generate-sales-report" , {
input: {
date: new Date (). toISOString (),
},
})
}
export const config = {
name: "generate-daily-report" ,
schedule: "0 6 * * *" , // Run daily at 6am
}
Cron Schedule Syntax
Cron expressions define when jobs run:
┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of month (1 - 31)
│ │ │ ┌───────────── month (1 - 12)
│ │ │ │ ┌───────────── day of week (0 - 6) (Sunday = 0)
│ │ │ │ │
* * * * *
Common Examples
// Every minute
schedule : "* * * * *"
// Every hour at minute 0
schedule : "0 * * * *"
// Every day at midnight
schedule : "0 0 * * *"
// Every day at 6am
schedule : "0 6 * * *"
// Every Monday at 9am
schedule : "0 9 * * 1"
// Every 15 minutes
schedule : "*/15 * * * *"
// First day of month at midnight
schedule : "0 0 1 * *"
// Every weekday at 8am
schedule : "0 8 * * 1-5"
Real-World Examples
Cleanup Inactive Data
src/jobs/cleanup-inactive-brands.ts
import { MedusaContainer } from "@medusajs/framework/types"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { BRAND_MODULE } from "../modules/brand"
import { IBrandModuleService } from "../modules/brand/types"
export default async function cleanupInactiveBrands (
container : MedusaContainer
) {
const logger = container . resolve ( ContainerRegistrationKeys . LOGGER )
const brandService = container . resolve < IBrandModuleService >( BRAND_MODULE )
logger . info ( "Starting cleanup of inactive brands" )
try {
// Find brands inactive for 6 months
const sixMonthsAgo = new Date ()
sixMonthsAgo . setMonth ( sixMonthsAgo . getMonth () - 6 )
const inactiveBrands = await brandService . listBrands ({
is_active: false ,
updated_at: {
$lt: sixMonthsAgo ,
},
})
if ( inactiveBrands . length === 0 ) {
logger . info ( "No inactive brands to clean up" )
return
}
const brandIds = inactiveBrands . map (( brand ) => brand . id )
await brandService . softDeleteBrands ( brandIds )
logger . info ( `Soft deleted ${ inactiveBrands . length } inactive brands` )
} catch ( error ) {
logger . error ( "Failed to cleanup inactive brands" , error )
}
}
export const config = {
name: "cleanup-inactive-brands" ,
schedule: "0 2 * * *" , // 2am daily
}
Sync External Data
src/jobs/sync-external-inventory.ts
import { MedusaContainer } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
import { IInventoryService } from "@medusajs/framework/types"
export default async function syncExternalInventory (
container : MedusaContainer
) {
const logger = container . resolve ( "logger" )
const inventoryService = container . resolve < IInventoryService >(
Modules . INVENTORY
)
logger . info ( "Starting inventory sync from external system" )
try {
// Fetch from external API
const response = await fetch ( "https://external-api.com/inventory" )
const externalInventory = await response . json ()
// Update inventory levels
for ( const item of externalInventory ) {
await inventoryService . updateInventoryLevels ([
{
inventory_item_id: item . sku ,
location_id: item . location ,
stocked_quantity: item . quantity ,
},
])
}
logger . info ( `Synced ${ externalInventory . length } inventory items` )
} catch ( error ) {
logger . error ( "Failed to sync external inventory" , error )
}
}
export const config = {
name: "sync-external-inventory" ,
schedule: "*/30 * * * *" , // Every 30 minutes
}
Generate Reports
src/jobs/generate-weekly-report.ts
import { MedusaContainer } from "@medusajs/framework/types"
import {
ContainerRegistrationKeys ,
Modules ,
} from "@medusajs/framework/utils"
import { IWorkflowEngineService } from "@medusajs/framework/types"
export default async function generateWeeklyReport (
container : MedusaContainer
) {
const logger = container . resolve ( ContainerRegistrationKeys . LOGGER )
const workflowEngine = container . resolve < IWorkflowEngineService >(
Modules . WORKFLOW_ENGINE
)
logger . info ( "Generating weekly sales report" )
try {
const endDate = new Date ()
const startDate = new Date ()
startDate . setDate ( startDate . getDate () - 7 )
await workflowEngine . run ( "generate-sales-report" , {
input: {
start_date: startDate . toISOString (),
end_date: endDate . toISOString (),
recipients: [ "admin@example.com" ],
},
})
logger . info ( "Weekly sales report generated successfully" )
} catch ( error ) {
logger . error ( "Failed to generate weekly report" , error )
}
}
export const config = {
name: "generate-weekly-report" ,
schedule: "0 8 * * 1" , // Every Monday at 8am
}
Cache Warming
src/jobs/warm-product-cache.ts
import { MedusaContainer } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
import { IProductModuleService } from "@medusajs/framework/types"
export default async function warmProductCache (
container : MedusaContainer
) {
const logger = container . resolve ( "logger" )
const productService = container . resolve < IProductModuleService >(
Modules . PRODUCT
)
const cacheService = container . resolve ( "cache" )
logger . info ( "Warming product cache" )
try {
// Fetch top products
const products = await productService . listProducts (
{ status: "published" },
{
take: 100 ,
relations: [ "variants" , "images" ],
}
)
// Cache each product
for ( const product of products ) {
const cacheKey = `product: ${ product . id } `
await cacheService . set ( cacheKey , JSON . stringify ( product ), 3600 )
}
logger . info ( `Cached ${ products . length } products` )
} catch ( error ) {
logger . error ( "Failed to warm product cache" , error )
}
}
export const config = {
name: "warm-product-cache" ,
schedule: "0 */6 * * *" , // Every 6 hours
}
Job Configuration Options
export const config = {
// Required: Unique job name
name: "my-job" ,
// Required: Cron schedule
schedule: "0 0 * * *" ,
// Optional: Job metadata
data: {
custom: "metadata" ,
},
}
Error Handling
Always handle errors in scheduled jobs:
export default async function myJob ( container : MedusaContainer ) {
const logger = container . resolve ( "logger" )
try {
// Job logic
} catch ( error ) {
logger . error ( "Job failed" , {
job: "my-job" ,
error: error . message ,
stack: error . stack ,
})
// Optionally: Send alert, notify team, etc.
}
}
Monitoring and Logging
Use structured logging for monitoring:
export default async function myJob ( container : MedusaContainer ) {
const logger = container . resolve ( "logger" )
const startTime = Date . now ()
logger . info ( "Job started" , { job: "my-job" })
try {
// Job logic
const recordsProcessed = 100
logger . info ( "Job completed" , {
job: "my-job" ,
duration: Date . now () - startTime ,
recordsProcessed ,
})
} catch ( error ) {
logger . error ( "Job failed" , {
job: "my-job" ,
duration: Date . now () - startTime ,
error: error . message ,
})
}
}
Best Practices
Use try-catch blocks to handle errors gracefully
Log job start, completion, and errors with structured data
Keep jobs idempotent (safe to run multiple times)
Use workflows for complex operations instead of putting logic in jobs
Set appropriate schedules - don’t run jobs more frequently than needed
Consider timezone implications of cron schedules
Test job logic independently before deploying
Monitor job execution and set up alerts for failures
Use soft deletes when cleaning up data
Be cautious with bulk operations - consider batching
Testing Scheduled Jobs
Test your job logic independently:
// In a test file
import cleanupExpiredBrands from "./cleanup-expired-brands"
import { createContainer } from "../test-utils"
describe ( "cleanupExpiredBrands" , () => {
it ( "should delete expired brands" , async () => {
const container = createContainer ()
await cleanupExpiredBrands ( container )
// Assert expected behavior
})
})
Disabling Jobs
To temporarily disable a job, you can:
Comment out the job file
Rename it to not match the job file pattern
Add a condition to skip execution:
export default async function myJob ( container : MedusaContainer ) {
const config = container . resolve ( "configModule" )
if ( config . featureFlags . disableMyJob ) {
return
}
// Job logic
}
Next Steps
Event Subscribers React to events instead of schedules
Create Workflows Build complex job logic as workflows