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:
This creates one step that returns a hello message.
Create a Workflow
Next, you can create a workflow using the createWorkflow
function:
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<any, WorkflowOutput>(
"hello-world",
function () {
const str1 = step1({})
return {
message: str1,
}
}
)
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 SubscriberConfig,
type SubscriberArgs,
CustomerService,
Customer,
} from "@medusajs/medusa"
import myWorkflow from "../workflows/hello-world"
export default async function handleCustomerCreate({
data, eventName, container, pluginOptions
}: SubscriberArgs<Customer>) {
myWorkflow(container)
.run()
.then(({ result }) => {
console.log(
`New user: ${result.message}`
)
})
}
export const config: SubscriberConfig = {
event: CustomerService.Events.CREATED,
context: {
subscriberId: "hello-customer"
}
}
import {
type ScheduledJobConfig,
type ScheduledJobArgs,
} from "@medusajs/medusa"
import myWorkflow from "../workflows/hello-world"
export default async function handler({
container,
data,
pluginOptions,
}: ScheduledJobArgs) {
myWorkflow(container)
.run()
.then(({ result }) => {
console.log(
result.message
)
})
}
export const config: ScheduledJobConfig = {
name: "run-once-a-day",
schedule: "0 0 * * *",
data: {},
}
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:
Then, update the workflow to accept input and pass it to the new step:
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
}
)
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.
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:
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.
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 = productService.update(id, updateData)
return new StepResponse(product, {
id,
previousProductData,
})
}, async ({ id, previousProductData }, context) => {
const productService: ProductService =
context.container.resolve("productService")
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:
- 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:
1// 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
Learn about workflow's utility methods.