Skip to main content

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:
  1. Output - Data returned to the workflow
  2. 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

Transform Data

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:
1

Step 1 executes

Creates a product → Returns product.id as compensation data
2

Step 2 executes

Creates prices → Returns price.ids as compensation data
3

Step 3 fails

Fails to attach to sales channel → Triggers compensation
4

Step 2 compensates

Deletes prices using price.ids
5

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

Build docs developers (and LLMs) love