How to Create a Custom Medusa Workflow: A Complete Developer's Guide
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 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 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 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:
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 step in 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:
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 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.
Create src/workflows/hello-world/index.ts:
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:
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 datatransform(data, fn): Maps data between steps without side effectsparallelize(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 for a production example of compensation logic that restores database state during failures.
Summary
- Steps are atomic units created with
createStepthat execute business logic and optionally export compensation functions for rollback. - Workflows compose steps using
createWorkflow,WorkflowData<T>for input typing, andWorkflowResponsefor output handling. - The workflow loader at
packages/core/framework/src/workflows/workflow-loader.tsauto-registers any workflow exported from a packageindex.tsfile on server startup. - Invoke workflows programmatically via the
workflowEngineservice accessed throughreq.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) 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 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, 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →