4.6.9. Advanced Workflow Example

In this chapter, you’ll create an advanced workflow.

Workflows are most useful when performing tasks across services and integrations:

  • You simplify complex flows or processes by splitting them into a series of steps.
  • You avoid data inconsistency and loss through retry mechanisms and rollbacks.
  • You execute the workflow from anywhere in your Medusa application.

The workflow you’ll build is an update-product workflow. You'll use an example ErpService that has methods to manage the product in a third-party ERP system, and the ProductService.


1. Create Workflow File#

Start by creating the file src/workflows/update-product-erp/index.ts that will hold the constructed workflow.

In the file, add the type of the expected workflow input:

src/workflows/update-product-erp/index.ts
1import { UpsertProductDTO } from "@medusajs/types"2
3export type UpdateProductAndErpWorkflowInput = UpsertProductDTO

The expected input is the data to update in the product along with the product’s ID.


2. Create Update Product Step#

The first step in the workflow receives the product’s ID and the data to update, then updates the product.

Create the file src/workflows/update-product-erp/steps/update-product.ts with the following content:

src/workflows/update-product-erp/steps/update-product.ts
8  async (input: UpdateProductAndErpWorkflowInput, context) => {9    const productModuleService: IProductModuleService =10      context.container.resolve(ModuleRegistrationName.PRODUCT)11
12    const { id } = input13    const previousProductData =14      await productModuleService.retrieveProduct(id)15
16    const product = await productModuleService.updateProducts(id, input)17
18    return new StepResponse(product, {19      // pass to compensation function20      previousProductData,21    })22  },23  // compensation function24  async ({ previousProductData }, context) => {25    const productModuleService: IProductModuleService =26      context.container.resolve(ModuleRegistrationName.PRODUCT)27
28    const { id, type, options, variants, ...previousData } = previousProductData29
30    await productModuleService.updateProducts(31      id,32      {33        ...previousData,34        variants: variants.map((variant) => {35          const variantOptions = {}36
37        variant.options.forEach((option) => {38          variantOptions[option.option.title] = option.value39        })40
41        return {42          ...variant,43          options: variantOptions,44        }45      }),46      options: options.map((option) => ({47        ...option,48        values: option.values.map((value) => value.value),49      })),50      type_id: type.id,51    })52  }53)54
55export default updateProduct

In the step:

  • You resolve the Product Module's main service from the Medusa container.
  • You retrieve the previousProductData to pass it to the compensation function.
  • You update and return the product.

You also pass a compensation function as a second parameter to createStep. The compensation function runs if an error occurs during the workflow execution.

In the compensation function, you revert the product’s data using the previousProductData passed from the step to the compensation function.


3. Create Step 2: Update ERP#

The second step in the workflow receives the same input. It updates the product’s details in the ERP system.

Note

The ErpModuleService used is assumed to be created in a module.

Create the file src/workflows/update-product-erp/steps/update-erp.ts with the following content:

src/workflows/update-product-erp/steps/update-erp.ts
7  async (input: UpdateProductAndErpWorkflowInput, context) => {8    const erpModuleService: ErpModuleService =9      context.container.resolve("erpModuleService")10
11    const { id, ...updatedData } = input12
13    // get previous ERP data14    const previousErpData = await erpModuleService.retrieveProductErpDetails(id)15
16    const updatedErpData = await erpModuleService.updateProductErpData(17      id,18      updatedData19    )20
21    return new StepResponse(updatedErpData, {22      // pass to compensation function23      previousErpData,24      productId: id,25    })26  },27  // compensation function28  async ({ previousErpData, productId }, context) => {29    const erpService: ErpModuleService = context.container.resolve("erpService")30
31    await erpService.updateProductErpData(productId, previousErpData)32  }33)34
35export default updateErp

In the step:

  • You resolve the erpModuleService from the Medusa container.
  • You retrieve the previousErpData to pass it to the compensation function.
  • You update the product’s ERP data and return the data from the ERP system.

You also pass a compensation function as a second parameter to createStep. In the compensation function, you revert the product's data in the ERP system to its previous state.


4. Create Workflow#

With the steps ready, you'll create the workflow that runs these steps to update the product’s data both in Medusa and the external ERP system.

Change the content of src/workflows/update-product-erp/index.ts to the following:

src/workflows/update-product-erp/index.ts
5
6export type UpdateProductAndErpWorkflowInput = UpsertProductDTO7
8type WorkflowOutput = {9  product: ProductDTO10  erpData: Record<string, unknown>11}12
13const updateProductAndErpWorkflow = createWorkflow<14  UpdateProductAndErpWorkflowInput,15  WorkflowOutput16>("update-product-and-erp", function (input) {17  const product = updateProduct(input)18  const erpData = updateErp(input)19
20  return {21    product,22    erpData,23  }24})25
26export default updateProductAndErpWorkflow

In the workflow construction function, you first run the updateProduct step, then the updateErp step. You return as the workflow’s result an object holding the updated product and ERP data.


5. Execute Workflow#

You can now use and execute your workflow in your Medusa application.

To execute the workflow in an API route, create the file src/api/store/products/[id]/erp/route.ts with the following content:

src/api/store/products/[id]/erp/route.ts
1import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"2import updateProductAndErpWorkflow, {3  UpdateProductAndErpWorkflowInput,4} from "../../../../../workflows/update-product-erp"5
6type ProductErpReq = Omit<UpdateProductAndErpWorkflowInput, "id">7
8export const POST = async (9  req: MedusaRequest<ProductErpReq>,10  res: MedusaResponse11) => {12  // skipping validation for simplicity13  const productData: UpdateProductAndErpWorkflowInput = {14    id: req.params.id,15    ...req.body,16  }17
18  const { result } = await updateProductAndErpWorkflow(req.scope).run({19    input: productData,20  })21
22  res.json(result)23}

In this POST API route, you retrieve the product’s ID from the path parameter and the data to update from the request body. You then execute the workflow by passing it the retrieved data as an input.

The route returns the result of the workflow, which is an object holding both the update product’s details and the ERP details.

Was this chapter helpful?