> ## 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.

# Create Scheduled Jobs

> Learn how to create scheduled jobs in Medusa for recurring background tasks.

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

<Steps>
  <Step title="Create the Job File">
    Create a file in `src/jobs/` with a default export function and schedule configuration:

    ```typescript title="src/jobs/cleanup-expired-brands.ts" theme={null}
    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
    }
    ```
  </Step>

  <Step title="Access Services">
    Resolve services from the container:

    ```typescript theme={null}
    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 * * *",
    }
    ```
  </Step>

  <Step title="Execute Workflows">
    Run workflows from scheduled jobs:

    ```typescript theme={null}
    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
    }
    ```
  </Step>
</Steps>

## 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

```typescript theme={null}
// 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

```typescript title="src/jobs/cleanup-inactive-brands.ts" theme={null}
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

```typescript title="src/jobs/sync-external-inventory.ts" theme={null}
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

```typescript title="src/jobs/generate-weekly-report.ts" theme={null}
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

```typescript title="src/jobs/warm-product-cache.ts" theme={null}
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

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
// 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:

1. Comment out the job file
2. Rename it to not match the job file pattern
3. Add a condition to skip execution:

```typescript theme={null}
export default async function myJob(container: MedusaContainer) {
  const config = container.resolve("configModule")

  if (config.featureFlags.disableMyJob) {
    return
  }

  // Job logic
}
```

## Next Steps

<CardGroup cols={2}>
  <Card title="Event Subscribers" icon="bell" href="/development/subscribers">
    React to events instead of schedules
  </Card>

  <Card title="Create Workflows" icon="diagram-project" href="/development/workflows">
    Build complex job logic as workflows
  </Card>
</CardGroup>
