- Get Started
- Product
- Resources
- Tools & SDKs
- Framework
- Reference
- Get Started
- Product
- Resources
- Tools & SDKs
- Framework
- Reference
2.4.3. Guide: Schedule Syncing Brands from CMS
In the previous chapters, you've integrated a third-party CMS and implemented the logic to sync created brands from Medusa to the CMS.
However, when you integrate a third-party system, you want the data to be in sync between the Medusa application and the system. One way to do so is by automatically syncing the data once a day.
You can create an action to be automatically executed at a specified interval using scheduled jobs. A scheduled job is an asynchronous function with a specified schedule of when the Medusa application should run it. Scheduled jobs are useful to automate repeated tasks.
In this chapter, you'll create a scheduled job that triggers syncing the brands from the third-party CMS to Medusa once a day. You'll implement the syncing logic in a workflow, and execute that workflow in the scheduled job.
1. Implement Syncing Workflow#
You'll start by implementing the syncing logic in a workflow, then execute the workflow later in the scheduled job.
Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution.
This workflow will have three steps:
retrieveBrandsFromCmsStep
to retrieve the brands from the CMS.createBrandsStep
to create the brands retrieved in the first step that don't exist in Medusa.updateBrandsStep
to update the brands retrieved in the first step that exist in Medusa.
retrieveBrandsFromCmsStep#
To create the step that retrieves the brands from the third-party CMS, create the file src/workflows/sync-brands-from-cms.ts
with the following content:
6import { CMS_MODULE } from "../modules/cms"7 8const retrieveBrandsFromCmsStep = createStep(9 "retrieve-brands-from-cms",10 async (_, { container }) => {11 const cmsModuleService: CmsModuleService = container.resolve(12 CMS_MODULE13 )14 15 const brands = await cmsModuleService.retrieveBrands()16 17 return new StepResponse(brands)18 }19)
You create a retrieveBrandsFromCmsStep
that resolves the CMS Module's service and uses its retrieveBrands
method to retrieve the brands in the CMS. You return those brands in the step's response.
createBrandsStep#
The brands retrieved in the first step may have brands that don't exist in Medusa. So, you'll create a step that creates those brands. Add the step to the same src/workflows/sync-brands-from-cms.ts
file:
7type CreateBrand = {8 name: string9}10 11type CreateBrandsInput = {12 brands: CreateBrand[]13}14 15export const createBrandsStep = createStep(16 "create-brands-step",17 async (input: CreateBrandsInput, { container }) => {18 const brandModuleService: BrandModuleService = container.resolve(19 BRAND_MODULE20 )21 22 const brands = await brandModuleService.createBrands(input.brands)23 24 return new StepResponse(brands, brands)25 },26 async (brands, { container }) => {27 if (!brands) {28 return29 }30 31 const brandModuleService: BrandModuleService = container.resolve(32 BRAND_MODULE33 )34 35 await brandModuleService.deleteBrands(brands.map((brand) => brand.id))36 }37)
The createBrandsStep
accepts the brands to create as an input. It resolves the Brand Module's service and uses the generated createBrands
method to create the brands.
The step passes the created brands to the compensation function, which deletes those brands if an error occurs during the workflow's execution.
Update Brands Step#
The brands retrieved in the first step may also have brands that exist in Medusa. So, you'll create a step that updates their details to match that of the CMS. Add the step to the same src/workflows/sync-brands-from-cms.ts
file:
1// ...2 3type UpdateBrand = {4 id: string5 name: string6}7 8type UpdateBrandsInput = {9 brands: UpdateBrand[]10}11 12export const updateBrandsStep = createStep(13 "update-brands-step",14 async ({ brands }: UpdateBrandsInput, { container }) => {15 const brandModuleService: BrandModuleService = container.resolve(16 BRAND_MODULE17 )18 19 const prevUpdatedBrands = await brandModuleService.listBrands({20 id: brands.map((brand) => brand.id),21 })22 23 const updatedBrands = await brandModuleService.updateBrands(brands)24 25 return new StepResponse(updatedBrands, prevUpdatedBrands)26 },27 async (prevUpdatedBrands, { container }) => {28 if (!prevUpdatedBrands) {29 return30 }31 32 const brandModuleService: BrandModuleService = container.resolve(33 BRAND_MODULE34 )35 36 await brandModuleService.updateBrands(prevUpdatedBrands)37 }38)
The updateBrandsStep
receives the brands to update in Medusa. In the step, you retrieve the brand's details in Medusa before the update to pass them to the compensation function. You then update the brands using the Brand Module's updateBrands
generated method.
In the compensation function, which receives the brand's old data, you revert the update using the same updateBrands
method.
Create Workflow#
Finally, you'll create the workflow that uses the above steps to sync the brands from the CMS to Medusa. Add to the same src/workflows/sync-brands-from-cms.ts
file the following:
1// other imports...2import {3 // ...4 createWorkflow,5 transform,6 WorkflowResponse,7} from "@medusajs/framework/workflows-sdk"8 9// ...10 11export const syncBrandsFromCmsWorkflow = createWorkflow(12 "sync-brands-from-system",13 () => {14 const brands = retrieveBrandsFromCmsStep()15 16 // TODO create and update brands17 }18)
In the workflow, you only use the retrieveBrandsFromCmsStep
for now, which retrieves the brands from the third-party CMS.
Next, you need to identify which brands must be created or updated. Since workflows are constructed internally and are only evaluated during execution, you can't access values to perform data manipulation directly. Instead, use transform from the Workflows SDK that gives you access to the real-time values of the data, allowing you to create new variables using those values.
So, replace the TODO
with the following:
1const { toCreate, toUpdate } = transform(2 {3 brands,4 },5 (data) => {6 const toCreate: CreateBrand[] = []7 const toUpdate: UpdateBrand[] = []8 9 data.brands.forEach((brand) => {10 if (brand.external_id) {11 toUpdate.push({12 id: brand.external_id as string,13 name: brand.name as string,14 })15 } else {16 toCreate.push({17 name: brand.name as string,18 })19 }20 })21 22 return { toCreate, toUpdate }23 }24)25 26// TODO create and update the brands
transform
accepts two parameters:
- The data to be passed to the function in the second parameter.
- A function to execute only when the workflow is executed. Its return value can be consumed by the rest of the workflow.
In transform
's function, you loop over the brands array to check which should be created or updated. This logic assumes that a brand in the CMS has an external_id
property whose value is the brand's ID in Medusa.
You now have the list of brands to create and update. So, replace the new TODO
with the following:
You first run the createBrandsStep
to create the brands that don't exist in Medusa, then the updateBrandsStep
to update the brands that exist in Medusa. You pass the arrays returned by transform
as the inputs for the steps.
Finally, you return an object of the created and updated brands. You'll execute this workflow in the scheduled job next.
2. Schedule Syncing Task#
You now have the workflow to sync the brands from the CMS to Medusa. Next, you'll create a scheduled job that runs this workflow once a day to ensure the data between Medusa and the CMS are always in sync.
A scheduled job is created in a TypeScript or JavaScript file under the src/jobs
directory. So, create the file src/jobs/sync-brands-from-cms.ts
with the following content:
1import { MedusaContainer } from "@medusajs/framework/types"2import { syncBrandsFromCmsWorkflow } from "../workflows/sync-brands-from-cms"3 4export default async function (container: MedusaContainer) {5 const logger = container.resolve("logger")6 7 const { result } = await syncBrandsFromCmsWorkflow(container).run()8 9 logger.info(10 `Synced brands from third-party system: ${11 result.created.length12 } brands created and ${result.updated.length} brands updated.`)13}14 15export const config = {16 name: "sync-brands-from-system",17 schedule: "0 0 * * *", // change to * * * * * for debugging18}
A scheduled job file must export:
- An asynchronous function that will be executed at the specified schedule. This function must be the file's default export.
- An object of scheduled jobs configuration. It has two properties:
name
: A unique name for the scheduled job.schedule
: A string that holds a cron expression indicating the schedule to run the job.
The scheduled job function accepts as a parameter the Medusa container used to resolve framework and commerce tools. You then execute the syncBrandsFromCmsWorkflow
and use its result to log how many brands were created or updated.
Based on the cron expression specified in config.schedule
, Medusa will run the scheduled job every day at midnight. You can also change it to * * * * *
to run it every minute for easier debugging.
Test it Out#
To test out the scheduled job, start the Medusa application:
If you set the schedule to * * * * *
for debugging, the scheduled job will run in a minute. You'll see in the logs how many brands were created or updated.
Summary#
By following the previous chapters, you utilized Medusa's framework and orchestration tools to perform and automate tasks that span across systems.
With Medusa, you can integrate any service from your commerce ecosystem with ease. You don't have to set up separate applications to manage your different customizations, or worry about data inconsistency across systems. Your efforts only go into implementing the business logic that ties your systems together.