Workflows
Workflows are Medusa’s orchestration engine for building reliable, distributed transactions with automatic compensation (rollback). They compose multiple steps into a single, fault-tolerant operation.
Why Workflows?
Workflows solve critical challenges in distributed commerce systems:
- Automatic rollback - Failed steps trigger compensation functions
- Idempotency - Steps can be safely retried
- Composability - Reuse steps across multiple workflows
- Observability - Track execution state and errors
- Background execution - Async steps for long-running operations
Workflows are the recommended way to implement business logic that spans multiple modules or requires transactional guarantees.
Creating a Step
Steps are the building blocks of workflows. Each step defines an action and optional compensation.
Basic Step Pattern
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { IApiKeyModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
export type CreateApiKeysStepInput = {
api_keys: CreateApiKeyDTO[]
}
export const createApiKeysStep = createStep(
"create-api-keys",
async (data: CreateApiKeysStepInput, { container }) => {
const service = container.resolve<IApiKeyModuleService>(Modules.API_KEY)
const created = await service.createApiKeys(data.api_keys)
return new StepResponse(
created,
created.map((apiKey) => apiKey.id)
)
},
async (createdIds, { container }) => {
if (!createdIds?.length) {
return
}
const service = container.resolve<IApiKeyModuleService>(Modules.API_KEY)
await service.deleteApiKeys(createdIds)
}
)
Source: packages/core/core-flows/src/api-key/steps/create-api-keys.ts:33-52
Step Response
The StepResponse constructor accepts two arguments:
- Output - Data returned to the workflow
- Compensation data - Data passed to the compensation function
return new StepResponse(
{ product, variants }, // Output to workflow
{ productId: product.id } // Data for compensation
)
Only include the minimum data needed for compensation (typically IDs). The compensation function receives this data, not the full output.
Step Context
Steps receive a context object with:
container - Dependency injection container
context - Shared context (transaction metadata, user info)
transactionId - Unique transaction identifier
idempotencyKey - For idempotent execution
export const myStep = createStep(
"my-step",
async (input: MyInput, { container, context }) => {
const service = container.resolve("myService")
const result = await service.doSomething(input, context)
return new StepResponse(result)
}
)
Creating a Workflow
Workflows compose steps into a transaction with automatic compensation.
Basic Workflow Pattern
import {
createWorkflow,
WorkflowData,
WorkflowResponse,
createHook,
} from "@medusajs/framework/workflows-sdk"
import { createApiKeysStep } from "../steps"
import type { ApiKeyDTO, CreateApiKeyDTO } from "@medusajs/framework/types"
export type CreateApiKeysWorkflowInput = {
api_keys: CreateApiKeyDTO[]
}
export const createApiKeysWorkflow = createWorkflow(
"create-api-keys",
(input: WorkflowData<CreateApiKeysWorkflowInput>) => {
const apiKeys = createApiKeysStep(input)
const apiKeysCreated = createHook("apiKeysCreated", {
apiKeys,
})
return new WorkflowResponse(apiKeys, {
hooks: [apiKeysCreated],
})
}
)
Source: packages/core/core-flows/src/api-key/workflows/create-api-keys.ts:51-64
Executing Workflows
Workflows are executed by calling them with a container and running:
import { createApiKeysWorkflow } from "@medusajs/core-flows"
export const POST = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const { result } = await createApiKeysWorkflow(req.scope).run({
input: {
api_keys: [
{
type: "publishable",
title: "Storefront",
created_by: req.auth_context.actor_id,
},
],
},
})
res.json({ api_keys: result })
}
Workflow Composition
Use transform to manipulate step outputs:
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { createProductStep, createPricesStep } from "./steps"
export const myWorkflow = createWorkflow(
"create-product-with-prices",
(input: WorkflowData<WorkflowInput>) => {
const product = createProductStep(input)
const priceData = transform({ product, input }, ({ product, input }) => {
return {
product_id: product.id,
prices: input.prices.map(price => ({
...price,
product_id: product.id,
})),
}
})
const prices = createPricesStep(priceData)
return new WorkflowResponse({ product, prices })
}
)
Source: packages/core/workflows-sdk/src/utils/composer/transform.ts:34-58
You cannot directly manipulate data in the workflow function. Always use transform to access runtime values.
Parallel Execution
Use parallelize to run steps concurrently:
import { createWorkflow, parallelize, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { createPricesStep, attachProductToSalesChannelStep } from "./steps"
export const myWorkflow = createWorkflow(
"my-workflow",
(input: WorkflowInput) => {
const product = createProductStep(input)
const [prices, productSalesChannel] = parallelize(
createPricesStep(product),
attachProductToSalesChannelStep(product)
)
return new WorkflowResponse({
prices,
productSalesChannel,
})
}
)
Source: packages/core/workflows-sdk/src/utils/composer/parallelize.ts:11-42
Conditional Execution
Use when-then for conditional step execution:
import { createWorkflow, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { isActiveStep, anotherStep } from "./steps"
export const workflow = createWorkflow(
"conditional-workflow",
(input: { is_active: boolean }) => {
const result = when(
input,
(input) => input.is_active
).then(() => {
return isActiveStep()
})
const anotherStepResult = anotherStep(result)
return new WorkflowResponse(anotherStepResult)
}
)
Source: packages/core/workflows-sdk/src/utils/composer/when.ts:31-62
You cannot use regular if statements in workflows because they evaluate at composition time, not runtime. Use when-then instead.
Workflow Hooks
Hooks emit events at specific points in workflow execution:
import { createWorkflow, createHook, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
export const deletePromotionsWorkflow = createWorkflow(
"delete-promotions",
(input: WorkflowData<{ ids: string[] }>) => {
const deletedPromotions = deletePromotionsStep(input.ids)
const promotionsDeleted = createHook("promotionsDeleted", {
ids: input.ids,
})
return new WorkflowResponse(deletedPromotions, {
hooks: [promotionsDeleted],
})
}
)
Subscribe to hooks:
import { deletePromotionsWorkflow } from "@medusajs/core-flows"
deletePromotionsWorkflow.hooks.promotionsDeleted(({ ids }) => {
console.log(`Deleted promotions:`, ids)
})
Async Steps
Steps can be marked as async for background execution:
export const longRunningStep = createStep(
{
name: "long-running-step",
async: true, // Run in background
},
async (input: StepInput, { container }) => {
// This step will be executed asynchronously
const result = await performLongOperation(input)
return new StepResponse(result)
},
async (data, { container }) => {
// Compensation also runs async
await rollbackOperation(data)
}
)
Async steps require a workflow engine module (Redis or in-memory) to be configured. They’re ideal for operations like sending emails, processing images, or external API calls.
Compensation Flow
When a step fails, all previous steps are compensated in reverse order:
Step 1 executes
Creates a product → Returns product.id as compensation data
Step 2 executes
Creates prices → Returns price.ids as compensation data
Step 3 fails
Fails to attach to sales channel → Triggers compensation
Step 2 compensates
Deletes prices using price.ids
Step 1 compensates
Deletes product using product.id
export const createProductStep = createStep(
"create-product",
async (data: CreateProductInput, { container }) => {
const productModule = container.resolve(Modules.PRODUCT)
const product = await productModule.createProducts(data)
// Return product as output, product.id for compensation
return new StepResponse(product, product.id)
},
async (productId, { container }) => {
if (!productId) return
const productModule = container.resolve(Modules.PRODUCT)
// Compensation: delete the created product
await productModule.deleteProducts([productId])
}
)
Running Workflows as Steps
Workflows can be composed as steps in other workflows:
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { createProductVariantsWorkflow } from "./create-product-variants"
export const createProductsWorkflow = createWorkflow(
"create-products",
(input: WorkflowInput) => {
const products = createProductsStep(input.products)
// Run another workflow as a step
const variants = createProductVariantsWorkflow.runAsStep({
input: {
product_variants: input.variants,
},
})
return new WorkflowResponse({ products, variants })
}
)
Source: packages/core/workflows-sdk/src/utils/composer/create-workflow.ts:203-296
Best Practices
Keep Steps Focused
Each step should do one thing. Split complex operations into multiple steps.
Always Provide Compensation
Every step that changes state should have a compensation function.
Minimize Compensation Data
Only pass IDs or minimal data needed to rollback.
Use Transform for Data Shaping
Don’t manipulate data directly in the workflow function.
Next Steps
Events
Learn how to emit and subscribe to events
Services
Build services that power your workflow steps