Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/medusajs/medusa/llms.txt

Use this file to discover all available pages before exploring further.

Workflows are composable, transactional business processes in Medusa. They allow you to build complex operations from reusable steps with automatic compensation (rollback) on failure.

What is a Workflow?

A workflow is:
  • A sequence of steps that execute in order
  • Transactional - all steps succeed or all are rolled back
  • Composable - workflows can call other workflows
  • Asynchronous - steps can run in the background
  • Type-safe - fully typed inputs and outputs

Creating Your First Workflow

1

Create a Step

Steps are the building blocks of workflows. Each step has an invocation function and optional compensation function:
src/workflows/brand/steps/create-brand.ts
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { IBrandModuleService } from "../../../modules/brand/types"
import { BRAND_MODULE } from "../../../modules/brand"

export interface CreateBrandStepInput {
  name: string
  description?: string
}

export const createBrandStep = createStep(
  "create-brand",
  async (input: CreateBrandStepInput, { container }) => {
    const brandService = container.resolve<IBrandModuleService>(
      BRAND_MODULE
    )

    const brand = await brandService.createBrands(input)

    return new StepResponse(
      brand,
      { brandId: brand.id } // Compensation input
    )
  },
  async (compensationData, { container }) => {
    if (!compensationData) {
      return
    }

    const brandService = container.resolve<IBrandModuleService>(
      BRAND_MODULE
    )

    await brandService.deleteBrands([compensationData.brandId])
  }
)
The StepResponse takes two arguments:
  1. The output of the step (returned to the workflow)
  2. Data to pass to the compensation function if rollback is needed
2

Create the Workflow

Compose steps into a workflow using createWorkflow:
src/workflows/brand/create-brand.ts
import {
  createWorkflow,
  WorkflowData,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { createBrandStep } from "./steps/create-brand"

export interface CreateBrandWorkflowInput {
  name: string
  description?: string
}

export const createBrandWorkflow = createWorkflow(
  "create-brand",
  (input: WorkflowData<CreateBrandWorkflowInput>) => {
    const brand = createBrandStep(input)

    return new WorkflowResponse(brand)
  }
)
3

Use the Workflow

Execute the workflow from an API route or another workflow:
src/api/admin/brands/route.ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { createBrandWorkflow } from "../../../workflows/brand/create-brand"

export async function POST(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result: brand } = await createBrandWorkflow(req.scope).run({
    input: req.body,
  })

  res.json({ brand })
}

Advanced Workflow Patterns

Multiple Steps with Compensation

Here’s a complete example showing multiple steps with compensation:
src/workflows/brand/steps/delete-brand.ts
import type { IBrandModuleService } from "../../../modules/brand/types"
import { BRAND_MODULE } from "../../../modules/brand"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

export const deleteBrandStep = createStep(
  "delete-brand",
  async (ids: string[], { container }) => {
    const brandService = container.resolve<IBrandModuleService>(BRAND_MODULE)

    await brandService.softDeleteBrands(ids)

    return new StepResponse(void 0, ids)
  },
  async (idsToRestore, { container }) => {
    if (!idsToRestore?.length) {
      return
    }

    const brandService = container.resolve<IBrandModuleService>(BRAND_MODULE)

    await brandService.restoreBrands(idsToRestore)
  }
)
src/workflows/brand/delete-brand.ts
import {
  createHook,
  createWorkflow,
  WorkflowData,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { deleteBrandStep } from "./steps/delete-brand"

export interface DeleteBrandWorkflowInput {
  ids: string[]
}

export const deleteBrandWorkflow = createWorkflow(
  "delete-brand",
  (input: WorkflowData<DeleteBrandWorkflowInput>) => {
    const deletedBrands = deleteBrandStep(input.ids)
    
    const brandsDeleted = createHook("brandsDeleted", {
      ids: input.ids,
    })

    return new WorkflowResponse(deletedBrands, {
      hooks: [brandsDeleted],
    })
  }
)

Transform Data Between Steps

Use transform to manipulate data between steps:
import { transform } from "@medusajs/framework/workflows-sdk"

export const updateBrandWorkflow = createWorkflow(
  "update-brand",
  (input: WorkflowData<UpdateBrandInput>) => {
    const brand = getBrandStep(input.id)

    const updateData = transform(
      { brand, input },
      ({ brand, input }) => ({
        id: brand.id,
        name: input.name ?? brand.name,
        description: input.description ?? brand.description,
      })
    )

    const updatedBrand = updateBrandStep(updateData)

    return new WorkflowResponse(updatedBrand)
  }
)

Conditional Execution

Use when to conditionally execute steps:
import { when } from "@medusajs/framework/workflows-sdk"

export const processBrandWorkflow = createWorkflow(
  "process-brand",
  (input: WorkflowData<ProcessBrandInput>) => {
    const brand = createBrandStep(input)

    when({ brand }, ({ brand }) => {
      return brand.logo_url === undefined
    }).then(() => {
      const defaultLogo = assignDefaultLogoStep(brand.id)
    })

    return new WorkflowResponse(brand)
  }
)

Parallel Execution

Use parallelize to run independent steps concurrently:
import { parallelize } from "@medusajs/framework/workflows-sdk"

export const setupBrandWorkflow = createWorkflow(
  "setup-brand",
  (input: WorkflowData<SetupBrandInput>) => {
    const brand = createBrandStep(input.brand)

    const [products, collections] = parallelize(
      createProductsStep(input.products),
      createCollectionsStep(input.collections)
    )

    return new WorkflowResponse({ brand, products, collections })
  }
)

Query Data in Workflows

Use useQueryGraphStep to fetch data:
import { useQueryGraphStep } from "@medusajs/framework/workflows-sdk"

export const getBrandDetailWorkflow = createWorkflow(
  "get-brand-detail",
  (input: WorkflowData<{ id: string; fields: string[] }>) => {
    const { data: brands } = useQueryGraphStep({
      entity: "brand",
      filters: { id: input.id },
      fields: input.fields,
    })

    const brand = transform({ brands }, ({ brands }) => brands[0])

    return new WorkflowResponse(brand)
  }
)

Step Context

The step context provides access to:
  • container - Dependency injection container
  • context - Shared context across steps
  • metadata - Additional metadata
  • idempotencyKey - Unique key for idempotent execution
export const myStep = createStep(
  "my-step",
  async (input, { container, context, metadata }) => {
    const logger = container.resolve("logger")
    logger.info("Executing step", { context, metadata })

    // Your logic here
  }
)

Error Handling

Workflows automatically handle errors and trigger compensation:
import { MedusaError } from "@medusajs/framework/utils"

export const validateBrandStep = createStep(
  "validate-brand",
  async (input: { name: string }) => {
    if (!input.name || input.name.length < 2) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Brand name must be at least 2 characters"
      )
    }

    return new StepResponse({ valid: true })
  }
)
When a step throws an error, all previously executed steps run their compensation functions in reverse order.

Workflow Hooks

Hooks allow you to emit events when a workflow completes:
import { createHook } from "@medusajs/framework/workflows-sdk"

export const createBrandWorkflow = createWorkflow(
  "create-brand",
  (input: WorkflowData<CreateBrandInput>) => {
    const brand = createBrandStep(input)

    const brandCreated = createHook("brandCreated", {
      id: brand.id,
    })

    return new WorkflowResponse(brand, {
      hooks: [brandCreated],
    })
  }
)
Subscribe to hooks in subscribers (see Event Subscribers).

Best Practices

  • Keep steps focused on a single responsibility
  • Always provide compensation functions for steps that modify data
  • Use transform for data manipulation instead of complex logic in steps
  • Name steps and workflows descriptively
  • Type your inputs and outputs for type safety
  • Use parallelize for independent operations
  • Handle errors with proper MedusaError types
  • Test workflows in isolation

Next Steps

Create API Routes

Build HTTP endpoints that use workflows

Event Subscribers

React to workflow events and hooks

Build docs developers (and LLMs) love