# How to Create a Custom Medusa Workflow: A Complete Developer's Guide

> Master custom Medusa workflows with this developer guide. Learn to define async steps with createStep, compose them with createWorkflow, and auto-register for the workflow engine.

- Repository: [Medusa/medusa](https://github.com/medusajs/medusa)
- Tags: how-to-guide
- Published: 2026-05-19

---

**You create a custom Medusa workflow by defining async steps using `createStep`, composing them with `createWorkflow`, and exporting the result from a package index file so Medusa's [`workflow-loader.ts`](https://github.com/medusajs/medusa/blob/main/workflow-loader.ts) can auto-register it for invocation via the workflow engine.**

Medusa v2 ships with a **declarative, step-based workflow engine** located in the `@medusajs/framework/workflows-sdk` package that automatically handles execution semantics like idempotency, retries, and transaction retention. Understanding how to create a custom Medusa workflow enables you to encapsulate complex business logic into reusable units that can be invoked from API routes, services, or even other workflows across your commerce application.

## How the Workflow Engine Auto-Discovers Your Code

When the Medusa server starts, the **workflow loader** located at [`packages/core/framework/src/workflows/workflow-loader.ts`](https://github.com/medusajs/medusa/blob/main/packages/core/framework/src/workflows/workflow-loader.ts) recursively scans every `*.ts` file that exports a workflow definition. It registers each discovered workflow in an internal registry, making it available for invocation via the `workflowEngine` service. To ensure your custom code is discovered, you must export workflows from an [`index.ts`](https://github.com/medusajs/medusa/blob/main/index.ts) file within your workflows directory structure.

## Step 1: Define Atomic Steps with `createStep`

Individual steps are the building blocks of any workflow. Each step is a pure async function created via `createStep` that receives typed input and a context object containing the Medusa container. Steps can optionally define a **compensation function** as a second argument to handle rollback logic if the workflow fails.

Create a step file at [`src/workflows/hello-world/steps/say-hello.ts`](https://github.com/medusajs/medusa/blob/main/src/workflows/hello-world/steps/say-hello.ts):

```typescript
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"

export const sayHelloStep = createStep(
  "say-hello",
  async (input: { name: string }) => {
    // Core business logic execution
    const greeting = `Hello, ${input.name}!`
    return new StepResponse(greeting)
  },
  // Optional compensation (undo) function
  async (_, { container }) => {
    // Cleanup logic if the workflow fails
  }
)

```

The `StepResponse` constructor wraps the return value and passes it to subsequent steps in the chain. If you examine the built-in [`delete-promotions.ts`](https://github.com/medusajs/medusa/blob/main/delete-promotions.ts) step in [`packages/core/core-flows/src/promotion/steps/delete-promotions.ts`](https://github.com/medusajs/medusa/blob/main/packages/core/core-flows/src/promotion/steps/delete-promotions.ts), you'll see the compensation pattern used to restore soft-deleted records during rollback scenarios.

## Step 2: Compose Steps into a Workflow

Workflows are defined using `createWorkflow`, which receives a unique name and a handler function. Inside the handler, you orchestrate step execution using helpers like `when` for conditional logic, `transform` for data mapping, `parallelize` for concurrent execution, and `createHook` for event emission.

Create the workflow definition at [`src/workflows/hello-world/workflows/greet-user.ts`](https://github.com/medusajs/medusa/blob/main/src/workflows/hello-world/workflows/greet-user.ts):

```typescript
import {
  createWorkflow,
  WorkflowData,
  WorkflowResponse,
  createHook,
} from "@medusajs/framework/workflows-sdk"
import { sayHelloStep } from "../steps/say-hello"

export const greetUserWorkflow = createWorkflow(
  "greet-user",
  (input: WorkflowData<{ name: string }>) => {
    // Execute the step; its result becomes `greeting`
    const greeting = sayHelloStep(input)

    // Define a hook for external subscribers
    const done = createHook("greeting.done", { greeting })

    // Return final response with attached hooks
    return new WorkflowResponse(greeting, {
      hooks: [done],
    })
  }
)

```

The `WorkflowData<T>` type guarantees type safety for the input, while `WorkflowResponse` finalizes the output and registers any hooks for post-execution events.

## Step 3: Export for Auto-Discovery

To ensure the workflow loader can find and register your custom workflow, export it from a package-level [`index.ts`](https://github.com/medusajs/medusa/blob/main/index.ts) file. This mirrors the pattern used in Medusa's official plugins, such as the loyalty plugin's workflow index at [`packages/plugins/loyalty/src/workflows/store-credit/index.ts`](https://github.com/medusajs/medusa/blob/main/packages/plugins/loyalty/src/workflows/store-credit/index.ts).

Create [`src/workflows/hello-world/index.ts`](https://github.com/medusajs/medusa/blob/main/src/workflows/hello-world/index.ts):

```typescript
export * from "./workflows/greet-user"

```

## Step 4: Invoke the Workflow from Application Code

Once registered, invoke the workflow from anywhere with access to the Medusa container, such as an API route or a custom service. Access the workflow engine via `req.scope.resolve("workflowEngine")` and call `run()` with the workflow ID and input payload.

Example invocation from an API route:

```typescript
import { MedusaResponse, AuthenticatedMedusaRequest } from "@medusajs/framework/http"

export const POST = async (
  req: AuthenticatedMedusaRequest<{ name: string }>,
  res: MedusaResponse<{ message: string }>
) => {
  const { name } = req.body

  const { result } = await req.scope
    .resolve("workflowEngine")
    .run("greet-user", { input: { name } })

  res.status(200).json({ message: result })
}

```

The engine handles **idempotency keys**, **automatic retries**, and **retention policies** automatically, allowing you to focus strictly on domain logic.

## Advanced Workflow Patterns

### Conditional Execution and Data Transformation

Medusa's workflow SDK provides functional helpers to control execution flow:

- **`when(condition, then)`**: Executes steps conditionally based on input data
- **`transform(data, fn)`**: Maps data between steps without side effects
- **`parallelize(steps)`**: Runs independent steps concurrently for performance

These utilities are imported from the same `@medusajs/framework/workflows-sdk` package and compose declaratively within the `createWorkflow` handler.

### Compensation and Rollback Strategies

As shown in the `createStep` signature, the second argument is an async compensation function that receives the step's original input and the container. If any step in a workflow fails, the engine automatically executes compensation functions in reverse order for all previously completed steps. Reference [`packages/core/core-flows/src/promotion/steps/delete-promotions.ts`](https://github.com/medusajs/medusa/blob/main/packages/core/core-flows/src/promotion/steps/delete-promotions.ts) for a production example of compensation logic that restores database state during failures.

## Summary

- **Steps** are atomic units created with `createStep` that execute business logic and optionally export compensation functions for rollback.
- **Workflows** compose steps using `createWorkflow`, `WorkflowData<T>` for input typing, and `WorkflowResponse` for output handling.
- The **workflow loader** at [`packages/core/framework/src/workflows/workflow-loader.ts`](https://github.com/medusajs/medusa/blob/main/packages/core/framework/src/workflows/workflow-loader.ts) auto-registers any workflow exported from a package [`index.ts`](https://github.com/medusajs/medusa/blob/main/index.ts) file on server startup.
- Invoke workflows programmatically via the `workflowEngine` service accessed through `req.scope.resolve("workflowEngine")`.
- Built-in handlers for **retries**, **idempotency**, and **event hooks** remove boilerplate from your custom logic.

## Frequently Asked Questions

### How does Medusa discover custom workflows after I create them?

Medusa's **workflow loader** ([`packages/core/framework/src/workflows/workflow-loader.ts`](https://github.com/medusajs/medusa/blob/main/packages/core/framework/src/workflows/workflow-loader.ts)) recursively scans `*.ts` files during server startup. It registers any exported workflow created with `createWorkflow` into the internal registry. To ensure discovery, always export your workflow definitions from an [`index.ts`](https://github.com/medusajs/medusa/blob/main/index.ts) file located within a `workflows/` directory structure.

### What is the difference between a step and a workflow in Medusa?

A **step** is a single, atomic async function created with `createStep` that performs one specific task and optionally defines rollback logic. A **workflow** is a named composition of steps created with `createWorkflow` that defines execution order, conditional branching, data transformation, and hooks. Steps are reusable across multiple workflows, while workflows are the callable units that the engine executes transactionally.

### How do I handle transactions and automatic rollbacks in custom workflows?

Supply a compensation function as the third argument to `createStep`. If any step in a workflow throws an error, the engine automatically invokes compensation functions for all previously completed steps in reverse chronological order. This pattern is implemented in core flows like [`packages/core/core-flows/src/promotion/steps/delete-promotions.ts`](https://github.com/medusajs/medusa/blob/main/packages/core/core-flows/src/promotion/steps/delete-promotions.ts), where the compensation function restores soft-deleted database records.

### Can I invoke a custom workflow from outside an API route?

Yes. Any code with access to the Medusa dependency injection container can invoke workflows. In services, jobs, or subscribers, resolve the workflow engine via `container.resolve("workflowEngine")` (or `this.container.resolve("workflowEngine")` in class-based services) and call `.run("workflow-id", { input })`. The engine manages execution regardless of the invocation context.