docs: revise main docs outline (#10502)
This commit is contained in:
1 parent
c8cb9b5c1a
commit
0ae98c51eb
141 files changed
+814
-1181
No files matched your search
@@ -0,0 +1,52 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Access Workflow Errors`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to access errors that occur during a workflow’s execution.
|
||||
|
||||
## How to Access Workflow Errors?
|
||||
|
||||
By default, when an error occurs in a workflow, it throws that error, and the execution stops.
|
||||
|
||||
You can configure the workflow to return the errors instead so that you can access and handle them differently.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["11", "errors", "`errors` is an array of errors that occur during the workflow's execution."],
|
||||
["14", "throwOnError", "Specify that errors occuring during the workflow's execution should be returned, not thrown."],
|
||||
]
|
||||
|
||||
```ts title="src/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-6" expandButtonLabel="Show Imports"
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import myWorkflow from "../../../workflows/hello-world"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) {
|
||||
const { result, errors } = await myWorkflow(req.scope)
|
||||
.run({
|
||||
// ...
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
if (errors.length) {
|
||||
return res.send({
|
||||
errors: errors.map((error) => error.error),
|
||||
})
|
||||
}
|
||||
|
||||
res.send(result)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The object passed to the `run` method accepts a `throwOnError` property. When disabled, the errors are returned in the `errors` property of `run`'s output.
|
||||
|
||||
The value of `errors` is an array of error objects. Each object has an `error` property, whose value is the name or text of the thrown error.
|
||||
@@ -0,0 +1,90 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Expose a Workflow Hook`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to expose a hook in your workflow.
|
||||
|
||||
## When to Expose a Hook
|
||||
|
||||
<Note title="Expose workflow hooks when" type="success">
|
||||
|
||||
Your workflow is reusable in other applications, and you allow performing an external action at some point in your workflow.
|
||||
|
||||
</Note>
|
||||
|
||||
<Note title="Don't expose workflow hooks if" type="error">
|
||||
|
||||
Your workflow isn't reusable by other applications. Use a step that performs what a hook handler would instead.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## How to Expose a Hook in a Workflow?
|
||||
|
||||
To expose a hook in your workflow, use the `createHook` function imported from `@medusajs/framework/workflows-sdk`.
|
||||
|
||||
For example:
|
||||
|
||||
export const hookHighlights = [
|
||||
["13", "createHook", "Add a hook to the workflow."],
|
||||
["14", `"productCreated"`, "The hook's name."],
|
||||
["15", "productId", "The data to pass to the hook handler."],
|
||||
["19", "hooks", "Return the list of hooks in the workflow."]
|
||||
]
|
||||
|
||||
```ts title="src/workflows/my-workflow/index.ts" highlights={hookHighlights}
|
||||
import {
|
||||
createStep,
|
||||
createHook,
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { createProductStep } from "./steps/create-product"
|
||||
|
||||
export const myWorkflow = createWorkflow(
|
||||
"my-workflow",
|
||||
function (input) {
|
||||
const product = createProductStep(input)
|
||||
const productCreatedHook = createHook(
|
||||
"productCreated",
|
||||
{ productId: product.id }
|
||||
)
|
||||
|
||||
return new WorkflowResponse(product, {
|
||||
hooks: [productCreatedHook],
|
||||
})
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The `createHook` function accepts two parameters:
|
||||
|
||||
1. The first is a string indicating the hook's name. You use this to consume the hook later.
|
||||
2. The second is the input to pass to the hook handler.
|
||||
|
||||
The workflow must also pass an object having a `hooks` property as a second parameter to the `WorkflowResponse` constructor. Its value is an array of the workflow's hooks.
|
||||
|
||||
### How to Consume the Hook?
|
||||
|
||||
To consume the hook of the workflow, create the file `src/workflows/hooks/my-workflow.ts` with the following content:
|
||||
|
||||
export const handlerHighlights = [
|
||||
["3", "productCreated", "Invoke the hook, passing it a step function as a parameter."],
|
||||
]
|
||||
|
||||
```ts title="src/workflows/hooks/my-workflow.ts" highlights={handlerHighlights}
|
||||
import { myWorkflow } from "../my-workflow"
|
||||
|
||||
myWorkflow.hooks.productCreated(
|
||||
async ({ productId }, { container }) => {
|
||||
// TODO perform an action
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The hook is available on the workflow's `hooks` property using its name `productCreated`.
|
||||
|
||||
You invoke the hook, passing a step function (the hook handler) as a parameter.
|
||||
@@ -0,0 +1,276 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Compensation Function`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn what a compensation function is and how to add it to a step.
|
||||
|
||||
## What is a Compensation Function
|
||||
|
||||
A compensation function rolls back or undoes changes made by a step when an error occurs in the workflow.
|
||||
|
||||
For example, if a step creates a record, the compensation function deletes the record when an error occurs later in the workflow.
|
||||
|
||||
By using compensation functions, you provide a mechanism that guarantees data consistency in your application and across systems.
|
||||
|
||||
---
|
||||
|
||||
## How to add a Compensation Function?
|
||||
|
||||
A compensation function is passed as a second parameter to the `createStep` function.
|
||||
|
||||
For example, create the file `src/workflows/hello-world.ts` with the following content:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts" highlights={[["15"], ["16"], ["17"]]} collapsibleLines="1-5" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
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...")
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Each step can have a compensation function. The compensation function only runs if an error occurs throughout the workflow.
|
||||
|
||||
---
|
||||
|
||||
## Test the Compensation Function
|
||||
|
||||
Create a step in the same `src/workflows/hello-world.ts` file that throws an error:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts"
|
||||
const step2 = createStep(
|
||||
"step-2",
|
||||
async () => {
|
||||
throw new Error("Throwing an error...")
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Then, create a workflow that uses the steps:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts" collapsibleLines="1-8" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
// other imports...
|
||||
|
||||
// steps...
|
||||
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input) {
|
||||
const str1 = step1()
|
||||
step2()
|
||||
|
||||
return new WorkflowResponse({
|
||||
message: str1,
|
||||
})
|
||||
})
|
||||
|
||||
export default myWorkflow
|
||||
```
|
||||
|
||||
Finally, execute the workflow from an API route:
|
||||
|
||||
```ts title="src/api/workflow/route.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports"
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import myWorkflow from "../../../workflows/hello-world"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) {
|
||||
const { result } = await myWorkflow(req.scope)
|
||||
.run()
|
||||
|
||||
res.send(result)
|
||||
}
|
||||
```
|
||||
|
||||
Run the Medusa application and send a `GET` request to `/workflow`:
|
||||
|
||||
```bash
|
||||
curl http://localhost:9000/workflow
|
||||
```
|
||||
|
||||
In the console, you'll see:
|
||||
|
||||
- `Hello from step one!` logged in the terminal, indicating that the first step ran successfully.
|
||||
- `Oops! Rolling back my changes...` logged in the terminal, indicating that the second step failed and the compensation function of the first step ran consequently.
|
||||
|
||||
---
|
||||
|
||||
## Pass Input to Compensation Function
|
||||
|
||||
If a step creates a record, the compensation function must receive the ID of the record to remove it.
|
||||
|
||||
To pass input to the compensation function, pass a second parameter in the `StepResponse` returned by the step.
|
||||
|
||||
For example:
|
||||
|
||||
export const inputHighlights = [
|
||||
["11", "", "The data to pass as an input to the compensation function."],
|
||||
["14", "{ message }", "The data received as an input from `StepResponse`'s second parameter."]
|
||||
]
|
||||
|
||||
```ts highlights={inputHighlights}
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
const step1 = createStep(
|
||||
"step-1",
|
||||
async () => {
|
||||
return new StepResponse(
|
||||
`Hello from step one!`,
|
||||
{ message: "Oops! Rolling back my changes..." }
|
||||
)
|
||||
},
|
||||
async ({ message }) => {
|
||||
console.log(message)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
In this example, the step passes an object as a second parameter to `StepResponse`.
|
||||
|
||||
The compensation function receives the object and uses its `message` property to log a message.
|
||||
|
||||
---
|
||||
|
||||
## Resolve Resources from the Medusa Container
|
||||
|
||||
The compensation function receives an object second parameter. The object has a `container` property that you use to resolve resources from the Medusa container.
|
||||
|
||||
For example:
|
||||
|
||||
export const containerHighlights = [
|
||||
["15", "container", "Access the container in the second parameter object."],
|
||||
["16", "resolve", "Use the container to resolve resources."]
|
||||
]
|
||||
|
||||
```ts
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
||||
|
||||
const step1 = createStep(
|
||||
"step-1",
|
||||
async () => {
|
||||
return new StepResponse(
|
||||
`Hello from step one!`,
|
||||
{ message: "Oops! Rolling back my changes..." }
|
||||
)
|
||||
},
|
||||
async ({ message }, { container }) => {
|
||||
const logger = container.resolve(
|
||||
ContainerRegistrationKeys.LOGGER
|
||||
)
|
||||
|
||||
logger.info(message)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
In this example, you use the `container` property in the second object parameter of the compensation function to resolve the logger.
|
||||
|
||||
You then use the logger to log a message.
|
||||
|
||||
---
|
||||
|
||||
## Handle Errors in Loops
|
||||
|
||||
<Note>
|
||||
|
||||
This feature is only available after [Medusa v2.0.5](https://github.com/medusajs/medusa/releases/tag/v2.0.5).
|
||||
|
||||
</Note>
|
||||
|
||||
Consider you have a module that integrates a third-party ERP system, and you're creating a workflow that deletes items in that ERP. You may have the following step:
|
||||
|
||||
```ts
|
||||
// other imports...
|
||||
import { promiseAll } from "@medusajs/framework/utils"
|
||||
|
||||
type StepInput = {
|
||||
ids: string[]
|
||||
}
|
||||
|
||||
const step1 = createStep(
|
||||
"step-1",
|
||||
async ({ ids }: StepInput, { container }) => {
|
||||
const erpModuleService = container.resolve(
|
||||
ERP_MODULE
|
||||
)
|
||||
const prevData: unknown[] = []
|
||||
|
||||
await promiseAll(
|
||||
ids.map(async (id) => {
|
||||
const data = await erpModuleService.retrieve(id)
|
||||
|
||||
await erpModuleService.delete(id)
|
||||
|
||||
prevData.push(id)
|
||||
})
|
||||
)
|
||||
|
||||
return new StepResponse(ids, prevData)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
In the step, you loop over the IDs to retrieve the item's data, store them in a `prevData` variable, then delete them using the ERP Module's service. You then pass the `prevData` variable to the compensation function.
|
||||
|
||||
However, if an error occurs in the loop, the `prevData` variable won't be passed to the compensation function as the execution never reached the return statement.
|
||||
|
||||
To handle errors in the loop so that the compensation function receives the last version of `prevData` before the error occurred, you wrap the loop in a try-catch block. Then, in the catch block, you invoke and return the `StepResponse.permanentFailure` function:
|
||||
|
||||
export const highlights = [
|
||||
["1", "try", "Wrap in a try-catch block."],
|
||||
["12", "permanentFailure", "Fail the step and the workflow."],
|
||||
["14", "prevData", "Data to pass to the compensation function."]
|
||||
]
|
||||
|
||||
```ts highlights={highlights}
|
||||
try {
|
||||
await promiseAll(
|
||||
ids.map(async (id) => {
|
||||
const data = await erpModuleService.retrieve(id)
|
||||
|
||||
await erpModuleService.delete(id)
|
||||
|
||||
prevData.push(id)
|
||||
})
|
||||
)
|
||||
} catch (e) {
|
||||
return StepResponse.permanentFailure(
|
||||
`An error occurred: ${e}`,
|
||||
prevData
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The `StepResponse.permanentFailure` fails the step and its workflow, triggering current and previous steps' compensation functions. The `permanentFailure` function accepts as a first parameter the error message, which is saved in the workflow's error details, and as a second parameter the data to pass to the compensation function.
|
||||
|
||||
So, if an error occurs during the loop, the compensation function will still receive the `prevData` variable to undo the changes made before the step failed.
|
||||
@@ -0,0 +1,82 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Conditions in Workflows with When-Then`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to execute an action based on a condition in a workflow using the when-then utility.
|
||||
|
||||
## Why If-Conditions Aren't Allowed in Workflows?
|
||||
|
||||
Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps.
|
||||
|
||||
At that point, variables in the workflow don't have any values. They only do when you execute the workflow.
|
||||
|
||||
So, you can't use an if-condition that checks a variable's value, as the condition will be evaluated when Medusa creates the internal representation of the workflow, rather than during execution.
|
||||
|
||||
Instead, use the when-then utility.
|
||||
|
||||
---
|
||||
|
||||
## What is the When-Then Utility?
|
||||
|
||||
The when-then utility functions execute an action if a condition is satisfied.
|
||||
|
||||
The `when` function accepts as a parameter a function that returns a boolean value, and the `then` function is chained to `when`. `then` accepts as a parameter a function that's executed if `when`'s parameter function returns a `true` value.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["15", "input", "The data to pass as a parameter to the function in the second parameter"],
|
||||
["17", "return", "The function must return a boolean value indicating whether\nthe callback function passed to `then` should be executed."],
|
||||
["19", "() => {", "The function to execute if `when`'s second parameter returns a `true` value."]
|
||||
]
|
||||
|
||||
```ts highlights={highlights}
|
||||
import {
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
when,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
// step imports...
|
||||
|
||||
const workflow = createWorkflow(
|
||||
"workflow",
|
||||
function (input: {
|
||||
is_active: boolean
|
||||
}) {
|
||||
|
||||
const result = when(
|
||||
input,
|
||||
(input) => {
|
||||
return input.is_active
|
||||
}
|
||||
).then(() => {
|
||||
const stepResult = isActiveStep()
|
||||
return stepResult
|
||||
})
|
||||
|
||||
// executed without condition
|
||||
const anotherStepResult = anotherStep(result)
|
||||
|
||||
return new WorkflowResponse(
|
||||
anotherStepResult
|
||||
)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
In this code snippet, you execute the `isActiveStep` only if the `input.is_active`'s value is `true`.
|
||||
|
||||
### When Parameters
|
||||
|
||||
`when` utility is a function imported from `@medusajs/framework/workflows-sdk`. It accepts the following parameters:
|
||||
|
||||
1. The first parameter is either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter.
|
||||
2. The second parameter is a function that returns a boolean indicating whether to execute the action in `then`.
|
||||
|
||||
### Then Parameters
|
||||
|
||||
To specify the action to perform if the condition is satisfied, chain a `then` function to `when` and pass it a callback function.
|
||||
|
||||
The callback function is only executed if `when`'s second parameter function returns a `true` value.
|
||||
@@ -0,0 +1,246 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Workflow Constraints`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
This chapter lists constraints of defining a workflow or its steps.
|
||||
|
||||
## Workflow Constraints
|
||||
|
||||
### No Async Functions
|
||||
|
||||
The function passed to `createWorkflow` can’t be an async function:
|
||||
|
||||
```ts highlights={[["4", "async", "Function can't be async."], ["11", "", "Correct way of defining the function."]]}
|
||||
// Don't
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
async function (input: WorkflowInput) {
|
||||
// ...
|
||||
})
|
||||
|
||||
// Do
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input: WorkflowInput) {
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
### No Direct Variable Manipulation
|
||||
|
||||
You can’t directly manipulate variables within the workflow's constructor function.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about why you can't manipulate variables [in this chapter](../variable-manipulation/page.mdx)
|
||||
|
||||
</Note>
|
||||
|
||||
Instead, use the `transform` utility function imported from `@medusajs/framework/workflows-sdk`:
|
||||
|
||||
export const highlights = [
|
||||
["9", "", "Don't manipulate variables directly."],
|
||||
["20", "transform", "Use the `transform` function to manipulate variables."]
|
||||
]
|
||||
|
||||
```ts highlights={highlights}
|
||||
// Don't
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input: WorkflowInput) {
|
||||
const str1 = step1(input)
|
||||
const str2 = step2(input)
|
||||
|
||||
return new WorkflowResponse({
|
||||
message: `${str1}${str2}`,
|
||||
})
|
||||
})
|
||||
|
||||
// Do
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input: WorkflowInput) {
|
||||
const str1 = step1(input)
|
||||
const str2 = step2(input)
|
||||
|
||||
const result = transform(
|
||||
{
|
||||
str1,
|
||||
str2,
|
||||
},
|
||||
(input) => ({
|
||||
message: `${input.str1}${input.str2}`,
|
||||
})
|
||||
)
|
||||
|
||||
return new WorkflowResponse(result)
|
||||
})
|
||||
```
|
||||
|
||||
### Create Dates in transform
|
||||
|
||||
When you use `new Date()` in a workflow's constructor function, the date is evaluated when Medusa creates the internal representation of the workflow, not during execution.
|
||||
|
||||
Instead, create the date using `transform`.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about how Medusa creates an internal representation of a workflow [in this chapter](../variable-manipulation/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
For example:
|
||||
|
||||
export const dateHighlights = [
|
||||
["5", "new Date()", "Don't create a date directly in the constructor function."],
|
||||
["16", "transform", "Use the `transform` function to create a date variable."]
|
||||
]
|
||||
|
||||
```ts highlights={dateHighlights}
|
||||
// Don't
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input: WorkflowInput) {
|
||||
const today = new Date()
|
||||
|
||||
return new WorkflowResponse({
|
||||
today,
|
||||
})
|
||||
})
|
||||
|
||||
// Do
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input: WorkflowInput) {
|
||||
const today = transform({}, () => new Date())
|
||||
|
||||
return new WorkflowResponse({
|
||||
today,
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### No If Conditions
|
||||
|
||||
You can't use if-conditions in a workflow.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about why you can't use if-conditions [in this chapter](../conditions/page.mdx#why-if-conditions-arent-allowed-in-workflows)
|
||||
|
||||
</Note>
|
||||
|
||||
Instead, use the when-then utility function imported from `@medusajs/framework/workflows-sdk`:
|
||||
|
||||
```ts
|
||||
// Don't
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input: WorkflowInput) {
|
||||
if (input.is_active) {
|
||||
// perform an action
|
||||
}
|
||||
})
|
||||
|
||||
// Do (explained in the next chapter)
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input: WorkflowInput) {
|
||||
when(input, (input) => {
|
||||
return input.is_active
|
||||
})
|
||||
.then(() => {
|
||||
// perform an action
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### No Conditional Operators
|
||||
|
||||
You can't use conditional operators in a workflow, such as `??` or `||`.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about why you can't use if-conditions [in this chapter](../conditions/page.mdx#why-if-conditions-arent-allowed-in-workflows)
|
||||
|
||||
</Note>
|
||||
|
||||
Instead, use `transform` to store the desired value in a variable.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
// Don't
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input: WorkflowInput) {
|
||||
const message = input.message || "Hello"
|
||||
})
|
||||
|
||||
// Do
|
||||
// other imports...
|
||||
import { transform } from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input: WorkflowInput) {
|
||||
const message = transform(
|
||||
{
|
||||
input,
|
||||
},
|
||||
(data) => data.input.message || "hello"
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step Constraints
|
||||
|
||||
### Returned Values
|
||||
|
||||
A step must only return serializable values, such as [primitive values](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#primitive_values) or an object.
|
||||
|
||||
Values of other types, such as Maps, aren't allowed.
|
||||
|
||||
```ts
|
||||
// Don't
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
const step1 = createStep(
|
||||
"step-1",
|
||||
(input, { container }) => {
|
||||
const myMap = new Map()
|
||||
|
||||
// ...
|
||||
|
||||
return new StepResponse({
|
||||
myMap,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// Do
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
const step1 = createStep(
|
||||
"step-1",
|
||||
(input, { container }) => {
|
||||
const myObj: Record<string, unknown> = {}
|
||||
|
||||
// ...
|
||||
|
||||
return new StepResponse({
|
||||
myObj,
|
||||
})
|
||||
}
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,155 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Execute Another Workflow`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to execute a workflow in another.
|
||||
|
||||
## Execute in a Workflow
|
||||
|
||||
To execute a workflow in another, use the `runAsStep` method that every workflow has.
|
||||
|
||||
For example:
|
||||
|
||||
export const workflowsHighlights = [
|
||||
["11", "runAsStep", "Use the `runAsStep` method to run the workflow as a step."],
|
||||
["12", "input", "Pass the input as you did in the `run` method before."]
|
||||
]
|
||||
|
||||
```ts highlights={workflowsHighlights} collapsibleLines="1-7" expandMoreButton="Show Imports"
|
||||
import {
|
||||
createWorkflow,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import {
|
||||
createProductsWorkflow,
|
||||
} from "@medusajs/medusa/core-flows"
|
||||
|
||||
const workflow = createWorkflow(
|
||||
"hello-world",
|
||||
async (input) => {
|
||||
const products = createProductsWorkflow.runAsStep({
|
||||
input: {
|
||||
products: [
|
||||
// ...
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
// ...
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Instead of invoking the workflow and passing it the container, you use its `runAsStep` method and pass it an object as a parameter.
|
||||
|
||||
The object has an `input` property to pass input to the workflow.
|
||||
|
||||
---
|
||||
|
||||
## Preparing Input Data
|
||||
|
||||
If you need to perform some data manipulation to prepare the other workflow's input data, use the `transform` utility function imported from `@medusajs/framework/workflows-sdk`.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn about the transform utility in [this chapter](../variable-manipulation/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
For example:
|
||||
|
||||
export const transformHighlights = [
|
||||
["16", "transform", "Make changes to the input data before passing it to the workflow."],
|
||||
["26", "createProductsData", "Pass the data prepared with the `transform` function to the workflow."]
|
||||
]
|
||||
|
||||
```ts highlights={transformHighlights} collapsibleLines="1-12"
|
||||
import {
|
||||
createWorkflow,
|
||||
transform,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import {
|
||||
createProductsWorkflow,
|
||||
} from "@medusajs/medusa/core-flows"
|
||||
|
||||
type WorkflowInput = {
|
||||
title: string
|
||||
}
|
||||
|
||||
const workflow = createWorkflow(
|
||||
"hello-product",
|
||||
async (input: WorkflowInput) => {
|
||||
const createProductsData = transform({
|
||||
input,
|
||||
}, (data) => [
|
||||
{
|
||||
title: `Hello ${data.input.title}`,
|
||||
},
|
||||
])
|
||||
|
||||
const products = createProductsWorkflow.runAsStep({
|
||||
input: {
|
||||
products: createProductsData,
|
||||
},
|
||||
})
|
||||
|
||||
// ...
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
In this example, you use the `transform` function to prepend `Hello` to the title of the product. Then, you pass the result as an input to the `createProductsWorkflow`.
|
||||
|
||||
---
|
||||
|
||||
## Run Workflow Conditionally
|
||||
|
||||
To run a workflow in another based on a condition, use the when-then utility functions imported from `@medusajs/framework/workflows-sdk`.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn about the when-then utility in [this chapter](../conditions/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
For example:
|
||||
|
||||
export const whenHighlights = [
|
||||
["20", "when", "If `should_create` passed in the input is enabled, then run the function passed to `then`."],
|
||||
["22", "createProductsWorkflow", "Workflow only runs if `when`'s condition is `true`."]
|
||||
]
|
||||
|
||||
```ts highlights={whenHighlights} collapsibleLines="1-16"
|
||||
import {
|
||||
createWorkflow,
|
||||
when,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import {
|
||||
createProductsWorkflow,
|
||||
} from "@medusajs/medusa/core-flows"
|
||||
import {
|
||||
CreateProductWorkflowInputDTO,
|
||||
} from "@medusajs/framework/types"
|
||||
|
||||
type WorkflowInput = {
|
||||
product?: CreateProductWorkflowInputDTO
|
||||
should_create?: boolean
|
||||
}
|
||||
|
||||
const workflow = createWorkflow(
|
||||
"hello-product",
|
||||
async (input: WorkflowInput) => {
|
||||
const product = when(input, ({ should_create }) => should_create)
|
||||
.then(() => {
|
||||
return createProductsWorkflow.runAsStep({
|
||||
input: {
|
||||
products: [input.product],
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
In this example, you use the `when` utility to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled.
|
||||
@@ -0,0 +1,384 @@
|
||||
import { TypeList } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Long-Running Workflows`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn what a long-running workflow is and how to configure it.
|
||||
|
||||
## What is a Long-Running Workflow?
|
||||
|
||||
When you execute a workflow, you wait until the workflow finishes execution to receive the output.
|
||||
|
||||
A long-running workflow is a workflow that continues its execution in the background. You don’t receive its output immediately. Instead, you subscribe to the workflow execution to listen to status changes and receive its result once the execution is finished.
|
||||
|
||||
### Why use Long-Running Workflows?
|
||||
|
||||
Long-running workflows are useful if:
|
||||
|
||||
- A task takes too long. For example, you're importing data from a CSV file.
|
||||
- The workflow's steps wait for an external action to finish before resuming execution. For example, before you import the data from the CSV file, you wait until the import is confirmed by the user.
|
||||
|
||||
---
|
||||
|
||||
## Configure Long-Running Workflows
|
||||
|
||||
A workflow is considered long-running if at least one step has its `async` configuration set to `true` and doesn't return a step response.
|
||||
|
||||
For example, consider the following workflow and steps:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts" highlights={[["15"]]} collapsibleLines="1-11" expandButtonLabel="Show More"
|
||||
import {
|
||||
createStep,
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
StepResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
const step1 = createStep("step-1", async () => {
|
||||
return new StepResponse({})
|
||||
})
|
||||
|
||||
const step2 = createStep(
|
||||
{
|
||||
name: "step-2",
|
||||
async: true,
|
||||
},
|
||||
async () => {
|
||||
console.log("Waiting to be successful...")
|
||||
}
|
||||
)
|
||||
|
||||
const step3 = createStep("step-3", async () => {
|
||||
return new StepResponse("Finished three steps")
|
||||
})
|
||||
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function () {
|
||||
step1()
|
||||
step2()
|
||||
const message = step3()
|
||||
|
||||
return new WorkflowResponse({
|
||||
message,
|
||||
})
|
||||
})
|
||||
|
||||
export default myWorkflow
|
||||
```
|
||||
|
||||
The second step has in its configuration object `async` set to `true` and it doesn't return a step response. This indicates that this step is an asynchronous step.
|
||||
|
||||
So, when you execute the `hello-world` workflow, it continues its execution in the background once it reaches the second step.
|
||||
|
||||
<Note>
|
||||
|
||||
A workflow is also considered long-running if one of its steps has their `retryInterval` option set as explained in [this chapter](../retry-failed-steps/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Change Step Status
|
||||
|
||||
Once the workflow's execution reaches an async step, it'll wait in the background for the step to succeed or fail before it moves to the next step.
|
||||
|
||||
To fail or succeed a step, use the Workflow Engine Module's main service that is registered in the Medusa Container under the `Modules.WORKFLOW_ENGINE` (or `workflowsModuleService`) key.
|
||||
|
||||
### Retrieve Transaction ID
|
||||
|
||||
Before changing the status of a workflow execution's async step, you must have the execution's transaction ID.
|
||||
|
||||
When you execute the workflow, the object returned has a `transaction` property, which is an object that holds the details of the workflow execution's transaction. Use its `transactionId` to later change async steps' statuses:
|
||||
|
||||
```ts
|
||||
const { transaction } = await myWorkflow(req.scope)
|
||||
.run()
|
||||
|
||||
// use transaction.transactionId later
|
||||
```
|
||||
|
||||
### Change Step Status to Successful
|
||||
|
||||
The Workflow Engine Module's main service has a `setStepSuccess` method to set a step's status to successful. If you use it on a workflow execution's async step, the workflow continues execution to the next step.
|
||||
|
||||
For example, consider the following step:
|
||||
|
||||
export const successStatusHighlights = [
|
||||
["17", "transactionId", "Receive the workflow execution's transaction ID as an input to the step."],
|
||||
["20", "resolve", "Resolve the workflow engine's main service."],
|
||||
["24", "setStepSuccess", "Change the step's status to successful."],
|
||||
["28", "stepId", "The ID of the step as passed to `createStep`'s first parameter when it was created."],
|
||||
["29", "workflowId", "The ID of the workflow as passed to `createWorkflow`'s first parameter when it was created."],
|
||||
["31", "stepResponse", "The response returned by the step, since an `async` step can't return a response in its definition."]
|
||||
]
|
||||
|
||||
```ts highlights={successStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
Modules,
|
||||
TransactionHandlerType,
|
||||
} from "@medusajs/framework/utils"
|
||||
import {
|
||||
StepResponse,
|
||||
createStep,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
type SetStepSuccessStepInput = {
|
||||
transactionId: string
|
||||
};
|
||||
|
||||
export const setStepSuccessStep = createStep(
|
||||
"set-step-success-step",
|
||||
async function (
|
||||
{ transactionId }: SetStepSuccessStepInput,
|
||||
{ container }
|
||||
) {
|
||||
const workflowEngineService = container.resolve(
|
||||
Modules.WORKFLOW_ENGINE
|
||||
)
|
||||
|
||||
await workflowEngineService.setStepSuccess({
|
||||
idempotencyKey: {
|
||||
action: TransactionHandlerType.INVOKE,
|
||||
transactionId,
|
||||
stepId: "step-2",
|
||||
workflowId: "hello-world",
|
||||
},
|
||||
stepResponse: new StepResponse("Done!"),
|
||||
options: {
|
||||
container,
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
In this step (which you use in a workflow other than the long-running workflow), you resolve the Workflow Engine Module's main service and set `step-2` of the previous workflow as successful.
|
||||
|
||||
The `setStepSuccess` method of the workflow engine's main service accepts as a parameter an object having the following properties:
|
||||
|
||||
<TypeList
|
||||
types={[
|
||||
{
|
||||
name: "idempotencyKey",
|
||||
type: "`object`",
|
||||
description: "The details of the workflow execution.",
|
||||
optional: false,
|
||||
children: [
|
||||
{
|
||||
name: "action",
|
||||
type: "`invoke` | `compensate`",
|
||||
description: "If the step's compensation function is running, use `compensate`. Otherwise, use `invoke`.",
|
||||
optional: false
|
||||
},
|
||||
{
|
||||
name: "transactionId",
|
||||
type: "`string`",
|
||||
description: "The ID of the workflow execution's transaction.",
|
||||
optional: false
|
||||
},
|
||||
{
|
||||
name: "stepId",
|
||||
type: "`string`",
|
||||
description: "The ID of the step to change its status. This is the first parameter passed to `createStep` when creating the step.",
|
||||
optional: false
|
||||
},
|
||||
{
|
||||
name: "workflowId",
|
||||
type: "`string`",
|
||||
description: "The ID of the workflow. This is the first parameter passed to `createWorkflow` when creating the workflow.",
|
||||
optional: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "stepResponse",
|
||||
type: "`StepResponse`",
|
||||
description: "Set the response of the step. This is similar to the response you return in a step's definition, but since the `async` step doesn't have a response, you set its response when changing its status.",
|
||||
optional: false
|
||||
},
|
||||
{
|
||||
name: "options",
|
||||
type: "`Record<string, any>`",
|
||||
description: "Options to pass to the step.",
|
||||
optional: true,
|
||||
children: [
|
||||
{
|
||||
name: "container",
|
||||
type: "`MedusaContainer`",
|
||||
description: "An instance of the Medusa Container",
|
||||
optional: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
### Change Step Status to Failed
|
||||
|
||||
The Workflow Engine Module's main service also has a `setStepFailure` method that changes a step's status to failed. It accepts the same parameter as `setStepSuccess`.
|
||||
|
||||
After changing the async step's status to failed, the workflow execution fails and the compensation functions of previous steps are executed.
|
||||
|
||||
For example:
|
||||
|
||||
export const failureStatusHighlights = [
|
||||
["17", "transactionId", "Receive the workflow execution's transaction ID as an input to the step."],
|
||||
["20", "resolve", "Resolve the workflow engine's main service."],
|
||||
["24", "setStepSuccess", "Change the step's status to successful."],
|
||||
["28", "stepId", "The ID of the step as passed to `createStep`'s first parameter when it was created."],
|
||||
["29", "workflowId", "The ID of the workflow as passed to `createWorkflow`'s first parameter when it was created."],
|
||||
["31", "stepResponse", "The response returned by the step, since an `async` step can't return a response in its definition."]
|
||||
]
|
||||
|
||||
```ts highlights={failureStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
Modules,
|
||||
TransactionHandlerType,
|
||||
} from "@medusajs/framework/utils"
|
||||
import {
|
||||
StepResponse,
|
||||
createStep,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
type SetStepFailureStepInput = {
|
||||
transactionId: string
|
||||
};
|
||||
|
||||
export const setStepFailureStep = createStep(
|
||||
"set-step-success-step",
|
||||
async function (
|
||||
{ transactionId }: SetStepFailureStepInput,
|
||||
{ container }
|
||||
) {
|
||||
const workflowEngineService = container.resolve(
|
||||
Modules.WORKFLOW_ENGINE
|
||||
)
|
||||
|
||||
await workflowEngineService.setStepFailure({
|
||||
idempotencyKey: {
|
||||
action: TransactionHandlerType.INVOKE,
|
||||
transactionId,
|
||||
stepId: "step-2",
|
||||
workflowId: "hello-world",
|
||||
},
|
||||
stepResponse: new StepResponse("Failed!"),
|
||||
options: {
|
||||
container,
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
You use this step in another workflow that changes the status of an async step in a long-running workflow's execution to failed.
|
||||
|
||||
---
|
||||
|
||||
## Access Long-Running Workflow Status and Result
|
||||
|
||||
To access the status and result of a long-running workflow execution, use the `subscribe` and `unsubscribe` methods of the Workflow Engine Module's main service.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["18", "resolve", "Resolve the workflow engine from the Medusa container."],
|
||||
["30", "subscribe", "Subscribe to status changes of the workflow execution."],
|
||||
]
|
||||
|
||||
```ts title="src/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-11" expandButtonLabel="Show Imports"
|
||||
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import myWorkflow from "../../../workflows/hello-world"
|
||||
import {
|
||||
IWorkflowEngineService,
|
||||
} from "@medusajs/framework/types"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
|
||||
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
||||
const { transaction, result } = await myWorkflow(req.scope).run()
|
||||
|
||||
const workflowEngineService = req.scope.resolve<
|
||||
IWorkflowEngineService
|
||||
>(
|
||||
Modules.WORKFLOW_ENGINE
|
||||
)
|
||||
|
||||
const subscriptionOptions = {
|
||||
workflowId: "hello-world",
|
||||
transactionId: transaction.transactionId,
|
||||
subscriberId: "hello-world-subscriber",
|
||||
}
|
||||
|
||||
await workflowEngineService.subscribe({
|
||||
...subscriptionOptions,
|
||||
subscriber: async (data) => {
|
||||
if (data.eventType === "onFinish") {
|
||||
console.log("Finished execution", data.result)
|
||||
// unsubscribe
|
||||
await workflowEngineService.unsubscribe({
|
||||
...subscriptionOptions,
|
||||
subscriberOrId: subscriptionOptions.subscriberId,
|
||||
})
|
||||
} else if (data.eventType === "onStepFailure") {
|
||||
console.log("Workflow failed", data.step)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
res.send(result)
|
||||
}
|
||||
```
|
||||
|
||||
In the above example, you execute the long-running workflow `hello-world` and resolve the Workflow Engine Module's main service from the Medusa container.
|
||||
|
||||
### subscribe Method
|
||||
|
||||
The main service's `subscribe` method allows you to listen to changes in the workflow execution’s status. It accepts an object having three properties:
|
||||
|
||||
<TypeList
|
||||
types={[
|
||||
{
|
||||
name: "workflowId",
|
||||
type: "`string`",
|
||||
description: "The name of the workflow.",
|
||||
},
|
||||
{
|
||||
name: "transactionId",
|
||||
type: "`string`",
|
||||
description:
|
||||
"The ID of the workflow exection's transaction. The transaction's details are returned in the response of the workflow execution.",
|
||||
},
|
||||
{
|
||||
name: "subscriberId",
|
||||
type: "`string`",
|
||||
description:
|
||||
"The ID of the subscriber.",
|
||||
},
|
||||
{
|
||||
name: "subscriber",
|
||||
type: "`(data: { eventType: string, result?: any }) => Promise<void>`",
|
||||
description:
|
||||
"The function executed when the workflow execution's status changes. The function receives a data object. It has an `eventType` property, which you use to check the status of the workflow execution.",
|
||||
},
|
||||
]}
|
||||
sectionTitle="Access Long-Running Workflow Status and Result"
|
||||
/>
|
||||
|
||||
If the value of `eventType` in the `subscriber` function's first parameter is `onFinish`, the workflow finished executing. The first parameter then also has a `result` property holding the workflow's output.
|
||||
|
||||
### unsubscribe Method
|
||||
|
||||
You can unsubscribe from the workflow using the workflow engine's `unsubscribe` method, which requires the same object parameter as the `subscribe` method.
|
||||
|
||||
However, instead of the `subscriber` property, it requires a `subscriberOrId` property whose value is the same `subscriberId` passed to the `subscribe` method.
|
||||
|
||||
---
|
||||
|
||||
## Example: Restaurant-Delivery Recipe
|
||||
|
||||
To find a full example of a long-running workflow, refer to the [restaurant-delivery recipe](!resources!/recipes/marketplace/examples/restaurant-delivery).
|
||||
|
||||
In the recipe, you use a long-running workflow that moves an order from placed to completed. The workflow waits for the restaurant to accept the order, the driver to pick up the order, and other external actions.
|
||||
@@ -0,0 +1,80 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Multiple Step Usage in Workflow`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to use a step multiple times in a workflow.
|
||||
|
||||
## Problem Reusing a Step in a Workflow
|
||||
|
||||
In some cases, you may need to use a step multiple times in the same workflow.
|
||||
|
||||
The most common example is using the `useQueryGraphStep` multiple times in a workflow to retrieve multiple unrelated data, such as customers and products.
|
||||
|
||||
Each workflow step must have a unique ID, which is the ID passed as a first parameter when creating the step:
|
||||
|
||||
```ts
|
||||
const useQueryGraphStep = createStep(
|
||||
"use-query-graph"
|
||||
// ...
|
||||
)
|
||||
```
|
||||
|
||||
This causes an error when you use the same step multiple times in a workflow, as it's registered in the workflow as two steps having the same ID:
|
||||
|
||||
```ts
|
||||
const helloWorkflow = createWorkflow(
|
||||
"hello",
|
||||
() => {
|
||||
const { data: products } = useQueryGraphStep({
|
||||
entity: "product",
|
||||
fields: ["id"],
|
||||
})
|
||||
|
||||
// ERROR OCCURS HERE: A STEP HAS THE SAME ID AS ANOTHER IN THE WORKFLOW
|
||||
const { data: customers } = useQueryGraphStep({
|
||||
entity: "customer",
|
||||
fields: ["id"],
|
||||
})
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The next section explains how to fix this issue to use the same step multiple times in a workflow.
|
||||
|
||||
---
|
||||
|
||||
## How to Use a Step Multiple Times in a Workflow?
|
||||
|
||||
When you execute a step in a workflow, you can chain a `config` method to it to change the step's config.
|
||||
|
||||
Use the `config` method to change a step's ID for a single execution.
|
||||
|
||||
So, this is the correct way to write the example above:
|
||||
|
||||
export const highlights = [
|
||||
["13", "name", "Change the step's ID for this execution."]
|
||||
]
|
||||
|
||||
```ts highlights={highlights}
|
||||
const helloWorkflow = createWorkflow(
|
||||
"hello",
|
||||
() => {
|
||||
const { data: products } = useQueryGraphStep({
|
||||
entity: "product",
|
||||
fields: ["id"],
|
||||
})
|
||||
|
||||
// ✓ No error occurs, the step has a different ID.
|
||||
const { data: customers } = useQueryGraphStep({
|
||||
entity: "customer",
|
||||
fields: ["id"],
|
||||
}).config({ name: "fetch-customers" })
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The `config` method accepts an object with a `name` property. Its value is a new ID of the step to use for this execution only.
|
||||
|
||||
The first `useQueryGraphStep` usage has the ID `use-query-graph`, and the second `useQueryGraphStep` usage has the ID `fetch-customers`.
|
||||
@@ -0,0 +1,293 @@
|
||||
import { CodeTabs, CodeTab } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Workflows`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about workflows and how to define and execute them.
|
||||
|
||||
## What is a Workflow?
|
||||
|
||||
In digital commerce you typically have many systems involved in your operations. For example, you may have an ERP system that holds product master data and accounting reports, a CMS system for content, a CRM system for managing customer campaigns, a payment service to process credit cards, and so on. Sometimes you may even have custom built applications that need to participate in the commerce stack. One of the biggest challenges when operating a stack like this is ensuring consistency in the data spread across systems.
|
||||
|
||||
Medusa has a built-in durable execution engine to help complete tasks that span multiple systems. You orchestrate your operations across systems in Medusa instead of having to manage it yourself. Other commerce platforms don't have this capability, which makes them a bottleneck to building customizations and iterating quickly.
|
||||
|
||||
A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow similar to how you create a JavaScript function.
|
||||
|
||||
However, unlike regular functions, workflows:
|
||||
|
||||
- Create an internal representation of your steps, allowing you to track them and their progress.
|
||||
- Support defining roll-back logic for each step, so that you can handle errors gracefully and your data remain consistent across systems.
|
||||
- Perform long actions asynchronously, giving you control over when a step starts and finishes.
|
||||
|
||||
You implement all custom flows within workflows, then execute them from [API routes](../api-routes/page.mdx), [subscribers](../../fundamentals/events-and-subscribers/page.mdx), and [scheduled jobs](../../fundamentals/scheduled-jobs/page.mdx).
|
||||
|
||||
---
|
||||
|
||||
## How to Create and Execute a Workflow?
|
||||
|
||||
### 1. Create the Steps
|
||||
|
||||
A workflow is made of a series of steps. A step is created using the `createStep` utility function imported from `@medusajs/framework/workflows-sdk`.
|
||||
|
||||
Create the file `src/workflows/hello-world.ts` with the following content:
|
||||
|
||||

|
||||
|
||||
export const step1Highlights = [
|
||||
["4", `"step-1"`, "The step's unique name."],
|
||||
["6", "`Hello from step one!`", "The step's returned data."]
|
||||
]
|
||||
|
||||
```ts title="src/workflows/hello-world.ts" highlights={step1Highlights}
|
||||
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
const step1 = createStep(
|
||||
"step-1",
|
||||
async () => {
|
||||
return new StepResponse(`Hello from step one!`)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The `createStep` function accepts the step's unique name as a first parameter, and the step's function as a second parameter.
|
||||
|
||||
Steps must return an instance of `StepResponse`, whose parameter is the data to return to the workflow executing the step.
|
||||
|
||||
Steps can accept input parameters. For example, add the following to `src/workflows/hello-world.ts`:
|
||||
|
||||
export const step2Highlights = [
|
||||
["7", "name", "Recieve step input as a parameter."]
|
||||
]
|
||||
|
||||
```ts title="src/workflows/hello-world.ts" highlights={step2Highlights}
|
||||
type WorkflowInput = {
|
||||
name: string
|
||||
}
|
||||
|
||||
const step2 = createStep(
|
||||
"step-2",
|
||||
async ({ name }: WorkflowInput) => {
|
||||
return new StepResponse(`Hello ${name} from step two!`)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This adds another step whose function accepts as a parameter an object with a `name` property.
|
||||
|
||||
### 2. Create a Workflow
|
||||
|
||||
Next, add the following to the same file to create the workflow using the `createWorkflow` function:
|
||||
|
||||
export const workflowHighlights = [
|
||||
["10", `"hello-world"`, "The workflow's unique name."],
|
||||
["11", "input", "The workflow's input is passed as a parameter."],
|
||||
["16", "WorkflowResponse", "The workflow returns an instance of WorkflowResponse."]
|
||||
]
|
||||
|
||||
```ts title="src/workflows/hello-world.ts" highlights={workflowHighlights}
|
||||
import {
|
||||
// other imports...
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
// ...
|
||||
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input: WorkflowInput) {
|
||||
const str1 = step1()
|
||||
// to pass input
|
||||
const str2 = step2(input)
|
||||
|
||||
return new WorkflowResponse({
|
||||
message: str2,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export default myWorkflow
|
||||
```
|
||||
|
||||
The `createWorkflow` function accepts the workflow's unique name as a first parameter, and the workflow's function as a second parameter. The workflow can accept input which is passed as a parameter to the function.
|
||||
|
||||
The workflow must return an instance of `WorkflowResponse`, whose parameter is returned to workflow executors.
|
||||
|
||||
### 3. Execute the Workflow
|
||||
|
||||
You can execute a workflow from different customizations:
|
||||
|
||||
- Execute in an API route to expose the workflow's functionalities to clients.
|
||||
- Execute in a subscriber to use the workflow's functionalities when a commerce operation is performed.
|
||||
- Execute in a scheduled job to run the workflow's functionalities automatically at a specified repeated interval.
|
||||
|
||||
To execute the workflow, invoke it passing the [Medusa container](../medusa-container/page.mdx) as a parameter. Then, use its `run` method:
|
||||
|
||||
<CodeTabs group="resource-types">
|
||||
<CodeTab label="API Route" value="api-route">
|
||||
|
||||
```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import myWorkflow from "../../workflows/hello-world"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) {
|
||||
const { result } = await myWorkflow(req.scope)
|
||||
.run({
|
||||
input: {
|
||||
name: "John",
|
||||
},
|
||||
})
|
||||
|
||||
res.send(result)
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="Subscriber" value="subscriber">
|
||||
|
||||
```ts title="src/subscribers/customer-created.ts" highlights={[["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
type SubscriberConfig,
|
||||
type SubscriberArgs,
|
||||
} from "@medusajs/framework"
|
||||
import myWorkflow from "../workflows/hello-world"
|
||||
|
||||
export default async function handleCustomerCreate({
|
||||
event: { data },
|
||||
container,
|
||||
}: SubscriberArgs<{ id: string }>) {
|
||||
const { result } = await myWorkflow(container)
|
||||
.run({
|
||||
input: {
|
||||
name: "John",
|
||||
},
|
||||
})
|
||||
|
||||
console.log(result)
|
||||
}
|
||||
|
||||
export const config: SubscriberConfig = {
|
||||
event: "order.placed",
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="Scheduled Job" value="scheduled-job">
|
||||
|
||||
```ts title="src/jobs/message-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"], ["11"], ["12"]]}
|
||||
import { MedusaContainer } from "@medusajs/framework/types"
|
||||
import myWorkflow from "../workflows/hello-world"
|
||||
|
||||
export default async function myCustomJob(
|
||||
container: MedusaContainer
|
||||
) {
|
||||
const { result } = await myWorkflow(container)
|
||||
.run({
|
||||
input: {
|
||||
name: "John",
|
||||
},
|
||||
})
|
||||
|
||||
console.log(result.message)
|
||||
}
|
||||
|
||||
export const config = {
|
||||
name: "run-once-a-day",
|
||||
schedule: `0 0 * * *`,
|
||||
};
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
### 4. Test Workflow
|
||||
|
||||
To test out your workflow, start your Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, if you added the API route above, send a `GET` request to `/workflow`:
|
||||
|
||||
```bash
|
||||
curl http://localhost:9000/workflow
|
||||
```
|
||||
|
||||
You’ll receive the following response:
|
||||
|
||||
```json title="Example Response"
|
||||
{
|
||||
"message": "Hello John from step two!"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Access Medusa Container in Workflow Steps
|
||||
|
||||
A step receives an object as a second parameter with configurations and context-related properties. One of these properties is the `container` property, which is the [Medusa container](../medusa-container/page.mdx) to allow you to resolve framework and commerce tools in your application.
|
||||
|
||||
For example, consider you want to implement a workflow that returns the total products in your application. Create the file `src/workflows/product-count.ts` with the following content:
|
||||
|
||||
export const highlights = [
|
||||
["10", "container", "Receive the Medusa container as a parameter"],
|
||||
["11", "resolve", "Resolve the Product Module's main service."],
|
||||
[
|
||||
"11",
|
||||
`"product"`,
|
||||
"The resource registration name imported from `@medusajs/framework/utils`.",
|
||||
],
|
||||
]
|
||||
|
||||
```ts title="src/workflows/product-count.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
const getProductCountStep = createStep(
|
||||
"get-product-count",
|
||||
async (_, { container }) => {
|
||||
const productModuleService = container.resolve("product")
|
||||
|
||||
const [, count] = await productModuleService.listAndCountProducts()
|
||||
|
||||
return new StepResponse(count)
|
||||
}
|
||||
)
|
||||
|
||||
const productCountWorkflow = createWorkflow(
|
||||
"product-count",
|
||||
function () {
|
||||
const count = getProductCountStep()
|
||||
|
||||
return new WorkflowResponse({
|
||||
count,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export default productCountWorkflow
|
||||
```
|
||||
|
||||
In `getProductCountStep`, you use the `container` to resolve the Product Module's main service. Then, you use its `listAndCountProducts` method to retrieve the total count of products and return it in the step's response. You then execute this step in the `productCountWorkflow`.
|
||||
|
||||
You can now execute this workflow in a custom API route, scheduled job, or subscriber to get the total count of products.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Find a full list of the registered resources in the Medusa container and their registration key in [this reference](!resources!/medusa-container-resources). You can use these resources in your custom workflows.
|
||||
|
||||
</Note>
|
||||
@@ -0,0 +1,61 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Run Workflow Steps in Parallel`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to run workflow steps in parallel.
|
||||
|
||||
## parallelize Utility Function
|
||||
|
||||
If your workflow has steps that don’t rely on one another’s results, run them in parallel using the `parallelize` utility function imported from the `@medusajs/framework/workflows-sdk`.
|
||||
|
||||
The workflow waits until all steps passed to the `parallelize` function finish executing before continuing to the next step.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["22", "[prices, productSalesChannel]", "The result of the steps. `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`."],
|
||||
["22", "parallelize", "Run the steps passed as parameters in parallel."],
|
||||
]
|
||||
|
||||
```ts highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
parallelize,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import {
|
||||
createProductStep,
|
||||
getProductStep,
|
||||
createPricesStep,
|
||||
attachProductToSalesChannelStep,
|
||||
} from "./steps"
|
||||
|
||||
interface WorkflowInput {
|
||||
title: string
|
||||
}
|
||||
|
||||
const myWorkflow = createWorkflow(
|
||||
"my-workflow",
|
||||
(input: WorkflowInput) => {
|
||||
const product = createProductStep(input)
|
||||
|
||||
const [prices, productSalesChannel] = parallelize(
|
||||
createPricesStep(product),
|
||||
attachProductToSalesChannelStep(product)
|
||||
)
|
||||
|
||||
const id = product.id
|
||||
const refetchedProduct = getProductStep(product.id)
|
||||
|
||||
return new WorkflowResponse(refetchedProduct)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The `parallelize` function accepts the steps to run in parallel as a parameter.
|
||||
|
||||
It returns an array of the steps' results in the same order they're passed to the `parallelize` function.
|
||||
|
||||
So, `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`.
|
||||
@@ -0,0 +1,88 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Retry Failed Steps`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to configure steps to allow retrial on failure.
|
||||
|
||||
## Configure a Step’s Retrial
|
||||
|
||||
By default, when an error occurs in a step, the step and the workflow fail, and the execution stops.
|
||||
|
||||
You can configure the step to retry on failure. The `createStep` function can accept a configuration object instead of the step’s name as a first parameter.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts" highlights={[["10"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
const step1 = createStep(
|
||||
{
|
||||
name: "step-1",
|
||||
maxRetries: 2,
|
||||
},
|
||||
async () => {
|
||||
console.log("Executing step 1")
|
||||
|
||||
throw new Error("Oops! Something happened.")
|
||||
}
|
||||
)
|
||||
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function () {
|
||||
const str1 = step1()
|
||||
|
||||
return new WorkflowResponse({
|
||||
message: str1,
|
||||
})
|
||||
})
|
||||
|
||||
export default myWorkflow
|
||||
```
|
||||
|
||||
The step’s configuration object accepts a `maxRetries` property, which is a number indicating the number of times a step can be retried when it fails.
|
||||
|
||||
When you execute the above workflow, you’ll see the following result in the terminal:
|
||||
|
||||
```bash
|
||||
Executing step 1
|
||||
Executing step 1
|
||||
Executing step 1
|
||||
error: Oops! Something happened.
|
||||
Error: Oops! Something happened.
|
||||
```
|
||||
|
||||
The first line indicates the first time the step was executed, and the next two lines indicate the times the step was retried. After that, the step and workflow fail.
|
||||
|
||||
---
|
||||
|
||||
## Step Retry Intervals
|
||||
|
||||
By default, a step is retried immediately after it fails. To specify a wait time before a step is retried, pass a `retryInterval` property to the step's configuration object. Its value is a number of seconds to wait before retrying the step.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts" highlights={[["5"]]}
|
||||
const step1 = createStep(
|
||||
{
|
||||
name: "step-1",
|
||||
maxRetries: 2,
|
||||
retryInterval: 2, // 2 seconds
|
||||
},
|
||||
async () => {
|
||||
// ...
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Interval Changes Workflow to Long-Running
|
||||
|
||||
By setting `retryInterval` on a step, a workflow becomes a [long-running workflow](../long-running-workflow/page.mdx) that runs asynchronously in the background. So, you won't receive its result or errors immediately when you execute the workflow.
|
||||
|
||||
Instead, you must subscribe to the workflow's execution using the Workflow Engine Module Service. Learn more about it in [this chapter](../long-running-workflow/page.mdx#access-long-running-workflow-status-and-result).
|
||||
@@ -0,0 +1,212 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Variable Manipulation in Workflows with transform`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to use the `transform` utility to manipulate variables in a workflow.
|
||||
|
||||
## Why Variable Manipulation isn't Allowed in Workflows
|
||||
|
||||
Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps.
|
||||
|
||||
At that point, variables in the workflow don't have any values. They only do when you execute the workflow.
|
||||
|
||||
So, you can only pass variables as parameters to steps. But, in a workflow, you can't change a variable's value or, if the variable is an array, loop over its items.
|
||||
|
||||
Instead, use the transform utility.
|
||||
|
||||
---
|
||||
|
||||
## What is the transform Utility?
|
||||
|
||||
The `transform` utility function creates a new variable as the result of manipulating other variables.
|
||||
|
||||
For example, consider you have two strings as the output of two steps:
|
||||
|
||||
```ts
|
||||
const str1 = step1()
|
||||
const str2 = step2()
|
||||
```
|
||||
|
||||
To concatenate the strings, you create a new variable `str3` using the `transform` function:
|
||||
|
||||
export const highlights = [
|
||||
["14", "str3", "Holds the result returned by `transform`'s second parameter function."],
|
||||
["15", "", "Specify the data to pass as a parameter to the function in the second parameter."],
|
||||
["16", "data", "The data passed in the first parameter of `transform`."],
|
||||
["16", "`${data.str1}${data.str2}`", "Return the concatenated strings."]
|
||||
]
|
||||
|
||||
```ts highlights={highlights}
|
||||
import {
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
transform,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
// step imports...
|
||||
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input) {
|
||||
const str1 = step1(input)
|
||||
const str2 = step2(input)
|
||||
|
||||
const str3 = transform(
|
||||
{ str1, str2 },
|
||||
(data) => `${data.str1}${data.str2}`
|
||||
)
|
||||
|
||||
return new WorkflowResponse(str3)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The `transform` utility function is imported from `@medusajs/framework/workflows-sdk`. It accepts two parameters:
|
||||
|
||||
1. The first parameter is an object of variables to manipulate. The object is passed as a parameter to `transform`'s second parameter function.
|
||||
2. The second parameter is the function performing the variable manipulation.
|
||||
|
||||
The value returned by the second parameter function is returned by `transform`. So, the `str3` variable holds the concatenated string.
|
||||
|
||||
You can use the returned value in the rest of the workflow, either to pass it as an input to other steps or to return it in the workflow's response.
|
||||
|
||||
---
|
||||
|
||||
## Example: Looping Over Array
|
||||
|
||||
Use `transform` to loop over arrays to create another variable from the array's items.
|
||||
|
||||
For example:
|
||||
|
||||
```ts collapsibleLines="1-7" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
transform,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
// step imports...
|
||||
|
||||
type WorkflowInput = {
|
||||
items: {
|
||||
id: string
|
||||
name: string
|
||||
}[]
|
||||
}
|
||||
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function ({ items }: WorkflowInput) {
|
||||
const ids = transform(
|
||||
{ items },
|
||||
(data) => data.items.map((item) => item.id)
|
||||
)
|
||||
|
||||
doSomethingStep(ids)
|
||||
|
||||
// ...
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This workflow receives an `items` array in its input.
|
||||
|
||||
You use the `transform` utility to create an `ids` variable, which is an array of strings holding the `id` of each item in the `items` array.
|
||||
|
||||
You then pass the `ids` variable as a parameter to the `doSomethingStep`.
|
||||
|
||||
---
|
||||
|
||||
## Example: Creating a Date
|
||||
|
||||
If you create a date with `new Date()` in a workflow's constructor function, Medusa evaluates the date's value when it creates the internal representation of the workflow, not when the workflow is executed.
|
||||
|
||||
So, use `transform` instead to create a date variable with `new Date()`.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
() => {
|
||||
const today = transform({}, () => new Date())
|
||||
|
||||
doSomethingStep(today)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
In this workflow, `today` is only evaluated when the workflow is executed.
|
||||
|
||||
---
|
||||
|
||||
## Caveats
|
||||
|
||||
### Transform Evaluation
|
||||
|
||||
The transform utility's value is only evaluated if you pass its output to a step or in the workflow response.
|
||||
|
||||
For example, if you have the following workflow:
|
||||
|
||||
```ts
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input) {
|
||||
const str = transform(
|
||||
{ input },
|
||||
(data) => `${data.input.str1}${data.input.str2}`
|
||||
)
|
||||
|
||||
return new WorkflowResponse("done")
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Since `str`'s value isn't used as a step's input or passed to `WorkflowResponse`, its value is never evaluated.
|
||||
|
||||
### Data Validation
|
||||
|
||||
The `transform` utility should only be used to perform variable or data manipulation.
|
||||
|
||||
If you want to perform some validation on the data, use a step or the [when-then utility](../conditions/page.mdx) instead.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
// DON'T
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input) {
|
||||
const str = transform(
|
||||
{ input },
|
||||
(data) => {
|
||||
if (!input.str1) {
|
||||
throw new Error("Not allowed!")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// DO
|
||||
const validateHasStr1Step = createStep(
|
||||
"validate-has-str1",
|
||||
({ input }) => {
|
||||
if (!input.str1) {
|
||||
throw new Error("Not allowed!")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input) {
|
||||
validateHasStr1({
|
||||
input,
|
||||
})
|
||||
|
||||
// workflow continues its execution only if
|
||||
// the step doesn't throw the error.
|
||||
}
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,150 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Workflow Hooks`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn what a workflow hook is and how to consume them.
|
||||
|
||||
## What is a Workflow Hook?
|
||||
|
||||
A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler.
|
||||
|
||||
Medusa exposes hooks in many of its workflows that are used in its API routes. You can consume those hooks to add your custom logic.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Refer to the [Workflows Reference](!resources!/medusa-workflows-reference) to view all workflows and their hooks.
|
||||
|
||||
</Note>
|
||||
|
||||
<Note title="Consume workflow hooks when" type="success">
|
||||
|
||||
You want to perform a custom action during a workflow's execution, such as when a product is created.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## How to Consume a Hook?
|
||||
|
||||
A workflow has a special `hooks` property which is an object that holds its hooks.
|
||||
|
||||
So, in a TypeScript or JavaScript file created under the `src/workflows/hooks` directory:
|
||||
|
||||
- Import the workflow.
|
||||
- Access its hook using the `hooks` property.
|
||||
- Pass the hook a step function as a parameter to consume it.
|
||||
|
||||
For example, to consume the `productsCreated` hook of Medusa's `createProductsWorkflow`, create the file `src/workflows/hooks/product-created.ts` with the following content:
|
||||
|
||||
export const handlerHighlights = [
|
||||
["3", "productsCreated", "Invoke the hook, passing it a step function as a parameter."],
|
||||
]
|
||||
|
||||
```ts title="src/workflows/hooks/product-created.ts" highlights={handlerHighlights}
|
||||
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
|
||||
|
||||
createProductsWorkflow.hooks.productsCreated(
|
||||
async ({ products }, { container }) => {
|
||||
// TODO perform an action
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The `productsCreated` hook is available on the workflow's `hooks` property by its name.
|
||||
|
||||
You invoke the hook, passing a step function (the hook handler) as a parameter.
|
||||
|
||||
Now, when a product is created using the [Create Product API route](!api!/admin#products_postproducts), your hook handler is executed after the product is created.
|
||||
|
||||
<Note>
|
||||
|
||||
A hook can have only one handler.
|
||||
|
||||
</Note>
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Refer to the [createProductsWorkflow reference](!resources!/references/medusa-workflows/createProductsWorkflow) to see at which point the hook handler is executed.
|
||||
|
||||
</Note>
|
||||
|
||||
### Hook Handler Parameter
|
||||
|
||||
Since a hook handler is essentially a step function, it receives the hook's input as a first parameter, and an object holding a `container` property as a second parameter.
|
||||
|
||||
Each hook has different input. For example, the `productsCreated` hook receives an object having a `products` property holding the created product.
|
||||
|
||||
### Hook Handler Compensation
|
||||
|
||||
Since the hook handler is a step function, you can set its compensation function as a second parameter of the hook.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/workflows/hooks/product-created.ts"
|
||||
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
|
||||
|
||||
createProductsWorkflow.hooks.productCreated(
|
||||
async ({ productId }, { container }) => {
|
||||
// TODO perform an action
|
||||
|
||||
return new StepResponse(undefined, { ids })
|
||||
},
|
||||
async ({ ids }, { container }) => {
|
||||
// undo the performed action
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The compensation function is executed if an error occurs in the workflow to undo the actions performed by the hook handler.
|
||||
|
||||
The compensation function receives as an input the second parameter passed to the `StepResponse` returned by the step function.
|
||||
|
||||
It also accepts as a second parameter an object holding a `container` property to resolve resources from the Medusa container.
|
||||
|
||||
### Additional Data Property
|
||||
|
||||
Medusa's workflows pass in the hook's input an `additional_data` property:
|
||||
|
||||
```ts title="src/workflows/hooks/product-created.ts" highlights={[["4", "additional_data"]]}
|
||||
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
|
||||
|
||||
createProductsWorkflow.hooks.productsCreated(
|
||||
async ({ products, additional_data }, { container }) => {
|
||||
// TODO perform an action
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This property is an object that holds additional data passed to the workflow through the request sent to the API route using the workflow.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn how to pass `additional_data` in requests to API routes in [this chapter](../../api-routes/additional-data/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
### Pass Additional Data to Workflow
|
||||
|
||||
You can also pass that additional data when executing the workflow. Pass it as a parameter to the `.run` method of the workflow:
|
||||
|
||||
```ts title="src/workflows/hooks/product-created.ts" highlights={[["10", "additional_data"]]}
|
||||
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
|
||||
|
||||
export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
||||
await createProductsWorkflow(req.scope).run({
|
||||
input: {
|
||||
products: [
|
||||
// ...
|
||||
],
|
||||
additional_data: {
|
||||
custom_field: "test",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Your hook handler then receives that passed data in the `additional_data` object.
|
||||
@@ -0,0 +1,100 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Workflow Timeout`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to set a timeout for workflows and steps.
|
||||
|
||||
## What is a Workflow Timeout?
|
||||
|
||||
By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs.
|
||||
|
||||
You can configure a workflow’s timeout to indicate how long the workflow can execute. If a workflow's execution time passes the configured timeout, it is failed and an error is thrown.
|
||||
|
||||
### Timeout Doesn't Stop Step Execution
|
||||
|
||||
Configuring a timeout doesn't stop the execution of a step in progress. The timeout only affects the status of the workflow and its result.
|
||||
|
||||
---
|
||||
|
||||
## Configure Workflow Timeout
|
||||
|
||||
The `createWorkflow` function can accept a configuration object instead of the workflow’s name.
|
||||
|
||||
In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More"
|
||||
import {
|
||||
createStep,
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
const step1 = createStep(
|
||||
"step-1",
|
||||
async () => {
|
||||
// ...
|
||||
}
|
||||
)
|
||||
|
||||
const myWorkflow = createWorkflow({
|
||||
name: "hello-world",
|
||||
timeout: 2, // 2 seconds
|
||||
}, function () {
|
||||
const str1 = step1()
|
||||
|
||||
return new WorkflowResponse({
|
||||
message: str1,
|
||||
})
|
||||
})
|
||||
|
||||
export default myWorkflow
|
||||
|
||||
```
|
||||
|
||||
This workflow's executions fail if they run longer than two seconds.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
A workflow’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](../access-workflow-errors/page.mdx). The error’s name is `TransactionTimeoutError`.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Configure Step Timeout
|
||||
|
||||
Alternatively, you can configure the timeout for a step rather than the entire workflow.
|
||||
|
||||
<Note>
|
||||
|
||||
As mentioned in the previous section, the timeout doesn't stop the execution of the step. It only affects the step's status and output.
|
||||
|
||||
</Note>
|
||||
|
||||
The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds.
|
||||
|
||||
For example:
|
||||
|
||||
```tsx
|
||||
const step1 = createStep(
|
||||
{
|
||||
name: "step-1",
|
||||
timeout: 2, // 2 seconds
|
||||
},
|
||||
async () => {
|
||||
// ...
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This step's executions fail if they run longer than two seconds.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
A step’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](../access-workflow-errors/page.mdx). The error’s name is `TransactionStepTimeoutError`.
|
||||
|
||||
</Note>
|
||||
Reference in new issue
Block a user