> ## 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.

# How to Create a Workflow

> Coordinate a business operation across modules as workflow steps, each with automatic rollback, reusing built-in steps and reading through 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**) when 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 and rollback.
</Warning>

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

```ts 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, or id generation **inside a step**. Never branch in the composition body itself.
</Tip>

## One mutation per step and 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 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)
  }
)
```

When 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 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. When 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 such as validation or 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 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.

## Next steps

<CardGroup cols={2}>
  <Card title="Extend a workflow" href="/rc/resources/customization/extend-a-workflow">
    Register handlers on a workflow's hooks to add behaviour without forking.
  </Card>

  <Card title="Best practices overview" href="/rc/resources/best-practices/overview">
    See how workflows fit the wider Mercur architecture.
  </Card>
</CardGroup>
