# How to Define Custom Steps in Medusa Workflows: A Complete Guide

> Define custom steps in Medusa workflows with our complete guide. Learn to create callable steps, implement compensation logic, and integrate them seamlessly into your projects.

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

---

**To define custom steps in Medusa workflows, import `createStep` from `@medusajs/framework/workflows-sdk`, write an invoke function that returns a `StepResponse`, optionally add a compensation function for rollbacks, and export the resulting callable to use inside any workflow.**

The **Medusa** framework's workflow engine, located in the `@medusajs/framework/workflows-sdk` package, treats steps as the fundamental unit of composable work. When you define custom steps in Medusa workflows, you create reusable, atomic operations that automatically support distributed transactions and rollback capabilities. This implementation relies on the core `createStep` utility found in [`packages/core/workflows-sdk/src/utils/composer/create-step.ts`](https://github.com/medusajs/medusa/blob/main/packages/core/workflows-sdk/src/utils/composer/create-step.ts).

## Anatomy of a Medusa Workflow Step

A step consists of three core components working together within the handler generated by `createStep`.

### The Invoke Function

The invoke function receives the step's input data and a `StepExecutionContext` containing the dependency `container`. This is where your business logic executes. According to the source code in [`packages/core/workflows-sdk/src/utils/composer/create-step.ts`](https://github.com/medusajs/medusa/blob/main/packages/core/workflows-sdk/src/utils/composer/create-step.ts), the function must return a `StepResponse` instance.

### StepResponse and Compensation

The **StepResponse** class wraps your step's output. Its constructor accepts two arguments: the primary output (forwarded to the next step) and optional compensation data (forwarded to the rollback handler).

The **compensation function** runs automatically if the workflow rolls back. It receives the second argument passed to `StepResponse` and the execution context, allowing you to undo side effects like database writes or API calls.

## Defining a Minimal Custom Step

Start with a pure step that transforms data without side effects. Since no persistent state changes occur, the compensation function remains empty.

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

export const doubleNumberStep = createStep(
  "double-number",
  async (input: { value: number }, { container }) => {
    const result = input.value * 2;
    return new StepResponse(result);
  },
  async (output, { container }) => {
    // No compensation needed for pure calculations
  }
);

```

Use this step inside a workflow by calling it within `createWorkflow`:

```typescript
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk";
import { doubleNumberStep } from "./steps/double-number";

export const demoWorkflow = createWorkflow(
  "demo-workflow",
  (input: { start: number }) => {
    const doubled = doubleNumberStep({ value: input.start });
    return new WorkflowResponse(doubled);
  }
);

```

## Implementing Compensation Logic for Database Transactions

Real-world steps often persist data that must be rolled back on failure. The Medusa loyalty plugin demonstrates this pattern in [`packages/plugins/loyalty/src/workflows/store-credit/steps/credit-account.ts`](https://github.com/medusajs/medusa/blob/main/packages/plugins/loyalty/src/workflows/store-credit/steps/credit-account.ts).

This step credits store-credit accounts and passes the created transaction IDs to its compensation function:

```typescript
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk";
import { IStoreCreditModuleService, PluginModule } from "../../../types";

export type CreditAccountStepInput = ModuleCreditAccount[];

export const creditAccountStep = createStep(
  "credit-account",
  async (input: CreditAccountStepInput, { container }) => {
    const module = container.resolve<IStoreCreditModuleService>(
      PluginModule.STORE_CREDIT
    );
    const transactions = await module.creditAccounts(input);
    return new StepResponse(transactions, transactions.map(t => t.id));
  },
  async (ids, { container }) => {
    if (!ids?.length) return;
    const module = container.resolve<IStoreCreditModuleService>(
      PluginModule.STORE_CREDIT
    );
    await module.deleteTransactions(ids);
  }
);

```

The corresponding workflow in [`packages/plugins/loyalty/src/workflows/store-credit/workflows/credit-accounts.ts`](https://github.com/medusajs/medusa/blob/main/packages/plugins/loyalty/src/workflows/store-credit/workflows/credit-accounts.ts) composes this step:

```typescript
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk";
import { creditAccountStep } from "../steps/credit-account";

export const creditAccountsWorkflow = createWorkflow(
  "credit-accounts",
  (input: ModuleCreditAccount[]) => {
    return new WorkflowResponse(creditAccountStep(input));
  }
);

```

## Configuring Conditional and Async Execution

Beyond basic invocation, Medusa steps support conditional logic and asynchronous execution patterns.

### Conditional Step Execution

Use the `.if()` method to guard step execution with a predicate function. The conditional logic is handled by `wrapConditionalStep` in [`packages/core/workflows-sdk/src/utils/composer/create-step.ts`](https://github.com/medusajs/medusa/blob/main/packages/core/workflows-sdk/src/utils/composer/create-step.ts):

```typescript
const conditionalStep = createStep(
  "conditionally-run",
  async (input, { container }) => {
    return new StepResponse("executed");
  }
);

// Inside a workflow:
const result = conditionalStep.if(
  {},
  () => Math.random() > 0.5
);

```

### Async Step Configuration

Mark steps as asynchronous by passing a `TransactionStepsDefinition` configuration object. Set `async: true` to let the orchestration engine automatically mark the step complete when the promise resolves, or `compensateAsync: true` for async compensation functions.

## Summary

- **Import `createStep`** from `@medusajs/framework/workflows-sdk` to register steps with the orchestration layer and generate unique UUIDs for each step instance.
- **Return `StepResponse`** from your invoke function to pass data forward and supply rollback information to compensation handlers.
- **Implement compensation functions** for any step that creates, updates, or deletes persistent data, using the second argument from `StepResponse` to receive rollback identifiers.
- **Reference the source** at [`packages/core/workflows-sdk/src/utils/composer/create-step.ts`](https://github.com/medusajs/medusa/blob/main/packages/core/workflows-sdk/src/utils/composer/create-step.ts) to understand UUID generation and handler wiring.
- **Use conditional execution** via `step.if()` when steps should only run based on runtime predicates evaluated by the workflow engine.
- **Export step callables** as named exports so workflows can import and invoke them within `createWorkflow`.

## Frequently Asked Questions

### How do you handle errors in a custom step?

Medusa automatically catches errors thrown in the invoke function and triggers the compensation functions for all previously completed steps in the workflow. Throw standard errors in your invoke function to initiate rollback proceedings. The orchestration engine manages the transaction boundaries according to the handler logic in [`packages/core/workflows-sdk/src/utils/composer/create-step-handler.ts`](https://github.com/medusajs/medusa/blob/main/packages/core/workflows-sdk/src/utils/composer/create-step-handler.ts).

### What data can you access from the container in a step?

The `StepExecutionContext` provides access to Medusa's dependency injection container, which resolves any registered module services or custom services. For example, you can resolve `IStoreCreditModuleService` or database repositories to perform CRUD operations. The container is the same instance used throughout the workflow execution, ensuring consistent transaction scopes.

### Can you define a step without a compensation function?

Yes, the compensation function is optional. For pure computation steps or read-only operations, omit the third argument to `createStep` or pass an empty async function. The workflow engine only calls compensation for steps that successfully completed before a failure occurred, and only if compensation data was provided via `StepResponse`.

### How do you compose multiple custom steps in a single workflow?

Chain steps by passing the output of one step as input to another using the `WorkflowData` object returned by each step call. Medusa's workflow builder tracks these dependencies automatically. For example: `const resultA = stepA(input); const resultB = stepB(resultA);`. The engine ensures sequential execution based on these data dependencies.