Files
medusa-store/www/apps/docs/content/development/workflows/index.mdx
Shahed Nasser cdc1da5df7 docs: added documentation pages for experimental features (#5671)
* 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
2023-11-27 16:49:12 +00:00

415 lines
11 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 Medusas 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 Workflows 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, its 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 youre 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, youll 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. Its useful to undo or roll back actions youve 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
Lets cover a more realistic example.
For example, you can build a workflow that updates a products 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, its 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` cant 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` cant be an asynchronous function.
- Since the constructor function only defines how the workflow works, you cant 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.'
}
}}
/>