> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mercurjs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Workflows

> Where all business logic and mutations live — composition-function constraints, one-mutation-per-step with compensation, reusing built-in steps, and the query engine.

A workflow is the orchestration layer: it coordinates a business operation across one or more modules as a series of **steps**, with automatic rollback (**compensation**) if any step fails. Every mutation in Mercur runs inside a workflow — this is the single most important rule in the [architecture](/rc/resources/best-practices/overview).

<Warning>
  **All mutations go through a workflow.** API routes, subscribers, and scheduled jobs never write to the database directly — they *run a workflow*. That is what gives every mutation validation, event emission, and rollback.
</Warning>

<Note>
  Workflows are a [Medusa framework primitive](https://docs.medusajs.com/learn/fundamentals/workflows). This page focuses on the constraints and conventions that trip people (and agents) up.
</Note>

## The composition function is not normal JavaScript

The function you pass to `createWorkflow` is a **composition function**. It runs once at build time to wire steps together — it does **not** execute your business logic at request time. Because of that, it has hard constraints:

<Warning>
  Inside a `createWorkflow` composition function you must **not**:

  * use `async` / `await`
  * use arrow functions for the composition body (use a named `function`)
  * use `if` / `else`, `for`, `while`, or `try/catch`
  * use `new Date()`, `Math.random()`, or any non-deterministic call
  * access properties of a step's output directly (e.g. `result.id`)

  These run at composition time, not execution time, so they either do nothing useful or break replay/rollback.
</Warning>

Anything that looks like normal logic goes into a **step** (for side effects) or a **`transform`** (for shaping data between steps):

```ts title="src/workflows/create-brands.ts" theme={null}
import {
  createWorkflow,
  transform,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

import { createBrandsStep } from "./steps"

export const createBrandsWorkflow = createWorkflow(
  "create-brands",
  function (input: CreateBrandsWorkflowInput) {
    // ✅ shape data with transform, not inline expressions
    const toCreate = transform(input, ({ brands }) =>
      brands.map((b) => ({ ...b, is_active: b.is_active ?? true }))
    )

    const brands = createBrandsStep(toCreate)

    return new WorkflowResponse(brands)
  }
)
```

<Tip>
  Need a conditional or a computed value? Use `transform` to derive data, `when` to run a step conditionally, and put date/random/id generation **inside a step**. Never branch in the composition body itself.
</Tip>

## One mutation per step + compensation

A step is the unit of work and the unit of rollback. The rule: **each step performs a single mutation and defines how to undo it.** `createStep` takes an invoke function and a compensation function; the invoke returns a `StepResponse` whose second argument is the data the compensation needs.

```ts title="src/workflows/steps/create-brands.ts" theme={null}
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import BrandModuleService from "../../modules/brand/service"
import { BRAND_MODULE } from "../../modules/brand"

export const createBrandsStep = createStep(
  "create-brands",
  async (data: CreateBrandInput[], { container }) => {
    const service = container.resolve<BrandModuleService>(BRAND_MODULE)
    const brands = await service.createBrands(data)
    // 2nd arg → passed to the compensation function below
    return new StepResponse(brands, brands.map((b) => b.id))
  },
  async (ids: string[] | undefined, { container }) => {
    if (!ids?.length) {
      return
    }
    const service = container.resolve<BrandModuleService>(BRAND_MODULE)
    await service.deleteBrands(ids)
  }
)
```

If a later step in the workflow throws, Medusa runs the compensation functions of the already-completed steps in reverse — the `createBrands` above is undone by `deleteBrands`. Splitting mutations one-per-step is what makes this reliable: a step that does two writes can only half-compensate.

## Reuse built-in steps

Don't hand-roll what the framework already ships. Medusa's `core-flows` exports composable steps you should reuse instead of writing your own:

| Need                            | Built-in step                                    |
| ------------------------------- | ------------------------------------------------ |
| Create/remove module links      | `createRemoteLinkStep` / `dismissRemoteLinkStep` |
| Emit a domain event             | `emitEventStep`                                  |
| Call another workflow as a step | `otherWorkflow.runAsStep({ input })`             |

```ts title="Composing built-in steps + a sub-workflow" theme={null}
import { emitEventStep } from "@medusajs/medusa/core-flows"

export const createBrandsWorkflow = createWorkflow(
  "create-brands",
  function (input: CreateBrandsWorkflowInput) {
    const brands = createBrandsStep(input.brands)

    // reuse a whole workflow as one step
    notifyOwnersWorkflow.runAsStep({ input: { brands } })

    // reuse the built-in event step
    emitEventStep({ eventName: "brand.created", data: { ids: brands } })

    return new WorkflowResponse(brands)
  }
)
```

<Tip>
  Prefer `runAsStep` over duplicating logic. If two workflows need the same sequence, extract it into its own workflow and call it as a step from both — you get one place to maintain and correct compensation for free.
</Tip>

## Hooks — let others extend your workflow

Expose extension points with `createHook` so consumers can inject behaviour (validation, side effects) without forking the workflow. Add a `validate` hook before the mutation and a `brandsCreated` hook after it:

```ts theme={null}
const validate = createHook("validate", { input })

const brands = createBrandsStep(input.brands)

const brandsCreated = createHook("brandsCreated", {
  brands,
  additional_data: input.additional_data,
})

return new WorkflowResponse(brands, {
  hooks: [validate, brandsCreated],
})
```

Consumers register a handler on the hook to run custom logic at that point. This is the sanctioned way to extend a workflow — see [Extend a workflow](/rc/resources/customization/extend-a-workflow).

## The query engine

Reads inside a step (and anywhere else) go through **Query** — the graph engine that resolves data across modules and links. Resolve it from the container and call `query.graph`:

```ts title="Reading across modules inside a step" theme={null}
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

const query = container.resolve(ContainerRegistrationKeys.QUERY)

const { data: brands } = await query.graph({
  entity: "brand",
  fields: ["id", "name", "products.*"], // follows the product ↔ brand link
  filters: { is_active: true },
})
```

<Warning>
  Query is for **reads**. Never try to mutate through it, and never resolve another module's service inside a step to read its data — go through Query so module isolation and links are respected.
</Warning>

## Checklist for a workflow

* Composition function is a named `function`, with no `async`/`await`, `if`, loops, `try/catch`, `new Date()`, or step-output property access.
* Data shaping between steps uses `transform`; conditional steps use `when`.
* Each step does exactly one mutation and defines a compensation function.
* `StepResponse` passes the compensation the data it needs to undo the work.
* Built-in steps (`createRemoteLinkStep`, `emitEventStep`) and `runAsStep` are reused instead of reimplemented.
* Reads use `query.graph`; no cross-module service calls.
* Extension points are exposed as hooks, not by forking.
