Skip to main content
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