Skip to main content
Skip to main content

Workflows Introduction

A workflow is a series of queries and actions that complete a task. Workflows are made up of a series of steps that interact with Medusa’s commerce modules, custom services, or external systems.

You construct a Workflow similar to how you create a JavaScript function, but unlike regular functions, a Medusa Workflow creates an internal representation of your steps. This makes it possible to keep track of your Workflow’s progress, automatically retry failing steps, and, if necessary, roll back steps.

Workflows can be used to define a flow with interactions across multiple systems, integrate third-party services into your commerce application, or automate actions within your application. Any flow with a series of steps can be implemented as a workflow.

Example: Your First Workflow

The tools to build Workflows are installed by default in Medusa projects. For other Node.js projects, you can install the @medusajs/workflows-sdk package from npm.

Create a Step

A workflow is made of a series of steps. A step is created using the createStep utility function.

Create the file src/workflows/hello-world.ts with the following content:

src/workflows/hello-world.ts
import { 
createStep,
StepResponse,
} from "@medusajs/workflows-sdk"

const step1 = createStep("step-1", async () => {
return new StepResponse(`Hello from step one!`)
})

This creates one step that returns a hello message.

Create a Workflow

Next, you can create a workflow using the createWorkflow function:

src/workflows/hello-world.ts
import {
createStep,
StepResponse,
createWorkflow,
} from "@medusajs/workflows-sdk"

type WorkflowOutput = {
message: string
}

const step1 = createStep("step-1", async () => {
return new StepResponse(`Hello from step one!`)
})

const myWorkflow = createWorkflow<unknown, WorkflowOutput>(
"hello-world",
function () {
const str1 = step1()

return {
message: str1,
}
}
)

export default myWorkflow

This creates a hello-world workflow. When you create a workflow, it’s constructed but not executed yet.

Execute the Workflow

You can execute a workflow from different places within Medusa.

  • Use API Routes if you want the workflow to execute in response to an API request or a webhook.
  • Use Subscribers if you want to execute a workflow when an event is triggered.
  • Use Scheduled Jobs if you want your workflow to execute on a regular schedule.

To execute the workflow, invoke it passing the Medusa container as a parameter, then use its run method:

import type { 
MedusaRequest,
MedusaResponse
} from "@medusajs/medusa";
import myWorkflow from "../../../workflows/hello-world";

export async function GET(
req: MedusaRequest,
res: MedusaResponse
) {
const { result } = await myWorkflow(req.scope)
.run()

res.send(result)
}

If you run your backend and trigger the execution of the workflow (based on where you’re executing it), you should see the message Hello from step one!.

Pass Inputs to Steps

Steps in a workflow can accept parameters.

For example, create a new step that accepts an input and returns a message with that input:

src/workflows/hello-world.ts
type WorkflowInput = {
name: string;
};

const step2 = createStep(
"step-2",
async ({ name }: WorkflowInput) => {
return new StepResponse(`Hello ${name} from step two!`)
}
)

Then, update the workflow to accept input and pass it to the new step:

src/workflows/hello-world.ts
import {
// previous imports
transform,
} from "@medusajs/workflows-sdk"

// ...

const myWorkflow = createWorkflow<
WorkflowInput,
WorkflowOutput
>("hello-world", function (input) {
const str1 = step1()
const str2 = step2(input)

const result = transform(
{
str1,
str2,
},
(input) => ({
message: `${input.str1}\n${input.str2}`,
})
)

return result
})

export default myWorkflow

Notice that to use the results of the steps, you must use the transform utility function. It gives you access to the real-time results of the steps once the workflow is executed.

Then, pass the necessary input to the workflow when you execute it. For example, in the API Route executing the workflow:

src/api/store/custom/route.ts
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
import myWorkflow from "../../../workflows/hello-world"

export async function GET(
req: MedusaRequest,
res: MedusaResponse
) {
const { result } = await myWorkflow(req.scope).run({
input: {
name: req.validatedQuery.name as string,
},
})

res.send(result)
}

If you execute the workflow again, you’ll see:

  • A Hello from step one! message, indicating that step one ran first.
  • A Hello {name} from step two message, indicating that step two ran after.

Add Error Handling

Errors can occur in a workflow. To avoid data inconsistency, you can pass a compensation function as a third parameter to the createStep function.

The compensation function only runs if an error occurs throughout the Workflow. It’s useful to undo or roll back actions you’ve performed in a step.

For example, change step one to add a compensation function and step two to throw an error:

src/workflows/hello-world.ts
const step1 = createStep(
"step-1",
async () => {
const message = `Hello from step one!`

console.log(message)

return new StepResponse(message)
},
async () => {
console.log("Oops! Rolling back my changes...")
}
)

const step2 = createStep(
"step-2",
async ({ name }: WorkflowInput) => {
throw new Error("Throwing an error...")
}
)

If you execute the Workflow, you should see:

  • Hello from step one! logged, indicating that the first step ran successfully.
  • Oops! Rolling back my changes... logged, indicating that the second step failed and the compensation function of the first step ran consequently.
Try it Out

You can try out this guide on Stackblitz.


More Advanced Example

Let’s cover a more realistic example.

For example, you can build a workflow that updates a product’s CMS details both in Medusa and an external CMS service:

import { createWorkflow } from "@medusajs/workflows-sdk"
import { Product } from "@medusajs/medusa"
import { updateProduct, sendProductDataToCms } from "./steps"

type WorkflowInput = {
id: string
title: string,
description: string,
images: string[]
}

const updateProductCmsWorkflow = createWorkflow<
WorkflowInput,
Product
>("update-product-cms", function (input) {
const product = updateProduct(input)
sendProductDataToCms(product)

return product
})

As these steps are making changes to data in the Medusa backend and a third-party service, it’s useful to provide a compensation function for each step that rolls back the changes.

For example, you can pass a compensation function to the updateProduct step that reverts the product update in case an error occurs:

const updateProduct = createStep(
"update-product",
async (input: WorkflowInput, context) => {
const productService: ProductService =
context.container.resolve("productService")

const { id, ...updateData } = input
const previousProductData = await productService.retrieve(
id, {
select: ["title", "description", "images"],
}
)
const product = await productService.update(id, updateData)

return new StepResponse(product, {
id,
previousProductData,
})
}, async ({ id, previousProductData }, context) => {
const productService: ProductService =
context.container.resolve("productService")

await productService.update(id, previousProductData)
}
)

Your steps may interact with external systems. For example, the sendProductDataToCms step communicates with an external CMS service. With the error handling and roll-back features that workflows provide, developers can ensure data delivery between multiple systems in their stack.


Constraints on Workflow Constructor Function

The Workflow Builder, createWorkflow, comes with a set of constraints:

  • The function passed to the createWorkflow can’t be an arrow function:
// Don't
const myWorkflow = createWorkflow<
WorkflowInput,
WorkflowOutput
>("hello-world", (input) => {
// ...
}
)

// Do
const myWorkflow = createWorkflow<WorkflowInput, WorkflowOutput>
("hello-world", function (input) {
// ...
}
)
  • The function passed to the createWorkflow can’t be an asynchronous function.
  • Since the constructor function only defines how the workflow works, you can’t directly manipulate data within the function. To do that, you must use the transform function:
// Don't
const myWorkflow = createWorkflow<
WorkflowInput,
WorkflowOutput
>("hello-world", function (input) {
const str1 = step1(input)
const str2 = step2(input)

return {
message: `${input.str1}${input.str2}`,
}
}
)

// Do
const myWorkflow = createWorkflow<
WorkflowInput,
WorkflowOutput
>("hello-world", function (input) {
const str1 = step1(input)
const str2 = step2(input)

const result = transform({
str1,
str2,
}, (input) => ({
message: `${input.str1}${input.str2}`,
}))

return result
}
)

Next Steps

Was this section helpful?