* docs: added documentation pages for experimental features * fix content lint issues * fixed lint errors * added migration step * added workflows introduction * add installation guides * added installation guides for modules + generated workflows reference * added missing workflows reference link * Added warning message for experimental features * fix note
415 lines
11 KiB
Plaintext
415 lines
11 KiB
Plaintext
import Tabs from '@theme/Tabs';
|
||
import TabItem from '@theme/TabItem';
|
||
import DocCard from '@theme/DocCard';
|
||
import Icons from '@theme/Icon';
|
||
|
||
# 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:
|
||
|
||
```ts title=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:
|
||
|
||
```ts title=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<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:
|
||
|
||
<Tabs groupId="resource-type" isCodeTabs={true}>
|
||
<TabItem value="api-route" label="API Route" attributes={{
|
||
title: "src/api/store/custom/route.ts"
|
||
}} default>
|
||
|
||
```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()
|
||
|
||
res.send(result)
|
||
}
|
||
```
|
||
|
||
</TabItem>
|
||
<TabItem value="subscribers" label="Subscribers" attributes={{
|
||
title: "src/subscribers/create-customer.ts"
|
||
}}>
|
||
|
||
```ts
|
||
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"
|
||
}
|
||
}
|
||
```
|
||
|
||
</TabItem>
|
||
<TabItem value="job" label="Scheduled Job" attributes={{
|
||
title: "src/jobs/message-daily.ts"
|
||
}}>
|
||
|
||
```ts
|
||
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: {},
|
||
}
|
||
```
|
||
|
||
</TabItem>
|
||
</Tabs>
|
||
|
||
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:
|
||
|
||
```ts title=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:
|
||
|
||
```ts title=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
|
||
}
|
||
)
|
||
```
|
||
|
||
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:
|
||
|
||
```ts title=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.
|
||
|
||
:::note[Try it Out]
|
||
|
||
You can try out this guide on [Stackblitz](https://stackblitz.com/edit/stackblitz-starters-etznpy?file=compensation-demo.ts&view=editor).
|
||
|
||
:::
|
||
|
||
---
|
||
|
||
## 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:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```ts
|
||
// 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:
|
||
|
||
```ts
|
||
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
|
||
|
||
<DocCard item={{
|
||
type: 'link',
|
||
href: '/references/workflows',
|
||
label: 'Workflows API Reference',
|
||
customProps: {
|
||
icon: Icons['academic-cap-solid'],
|
||
description: 'Learn about workflow\'s utility methods.'
|
||
}
|
||
}}
|
||
/>
|