# How to Integrate External Services with Medusa Workflows: A Complete Guide

> Integrate external services with Medusa workflows by building custom steps with createStep and composing them into robust workflows. Learn to implement rollback safety with this complete guide.

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

---

**Integrate external services with Medusa workflows by creating custom steps using `createStep` that wrap HTTP calls or SDKs, optionally defining compensation functions for automatic rollback safety, then composing them into workflows with `createWorkflow`.**

Medusa's workflow engine provides a type-safe, composable framework for orchestrating business logic across your commerce stack. To integrate external services like payment gateways, shipping carriers, or CRM systems into your Medusa application, you encapsulate third-party calls within workflow steps that benefit from built-in transaction management and retry semantics.

## Understanding Medusa's Workflow Architecture

Medusa workflows are declarative TypeScript modules that export a `createWorkflow` call. When the server starts, `WorkflowLoader` automatically registers these modules with the `MedusaWorkflow` runtime, making them available for execution across your application.

The core primitive for external integration is the **step**, defined using `createStep(name, mainFn, compensationFn?)` in files like [`packages/core/framework/src/workflows/__fixtures__/workflows/product-updater.ts`](https://github.com/medusajs/medusa/blob/main/packages/core/framework/src/workflows/__fixtures__/workflows/product-updater.ts). Each step receives serializable input and returns a `StepResponse`, ensuring deterministic execution and safe retries. This architecture isolates external service calls into pure functions that the engine can track, rollback, or retry as needed.

## Creating Custom Steps for External Services

### Step Structure and the createStep Function

A step that communicates with an external service consists of a main function containing your business logic and an optional compensation function that undoes the operation if the workflow fails later.

```typescript
// src/workflows/steps/create-payment-intent.ts
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import Stripe from "stripe"

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2023-10-16",
})

export const createPaymentIntentStep = createStep(
  "create-payment-intent",
  async ({ amount, currency }: { amount: number; currency: string }) => {
    const intent = await stripe.paymentIntents.create({
      amount,
      currency,
    })
    return new StepResponse({ intentId: intent.id })
  },
  async ({ intentId }: { intentId: string }) => {
    if (intentId) {
      await stripe.paymentIntents.cancel(intentId)
    }
  }
)

```

The first function performs the external HTTP request using `fetch`, `axios`, or the provider's SDK, while the second function serves as the **compensation** mechanism.

### Implementing Compensation for Safety

Compensation functions ensure atomicity across distributed operations. If any subsequent step in your workflow throws an error, Medusa automatically invokes the compensation functions of previously completed steps in reverse order.

When integrating external services, always store request identifiers in the step's output. This enables idempotent retries and safe rollback operations. For example, storing the Stripe `intentId` allows the compensation function to void the payment if inventory validation fails later in the workflow.

## Composing Workflows with External Steps

### Using useQueryGraphStep for Data Enrichment

Before calling external services, you often need to fetch and validate Medusa entities. The `useQueryGraphStep` (referenced in [`packages/core/core-flows/src/order/workflows/update-order.ts`](https://github.com/medusajs/medusa/blob/main/packages/core/core-flows/src/order/workflows/update-order.ts)) queries Medusa's data layer without leaving the workflow context.

```typescript
// src/workflows/create-order-payment.ts
import { createWorkflow, WorkflowData, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { createPaymentIntentStep } from "./steps/create-payment-intent"
import { useQueryGraphStep } from "@medusajs/framework/workflows-sdk"

export const createOrderPaymentWorkflow = createWorkflow(
  "create-order-payment",
  (input: WorkflowData<{ orderId: string }>) => {
    const order = useQueryGraphStep({
      entity: "order",
      fields: ["id", "total"],
      filters: { id: input.orderId },
      options: { throwIfKeyNotFound: true },
    })

    const paymentResult = createPaymentIntentStep({
      amount: order.data[0].total,
      currency: "usd",
    })

    return new WorkflowResponse(paymentResult)
  }
)

```

This pattern validates that the order exists and retrieves the total amount before initiating the external payment call.

### Complete Workflow Example

The workflow above demonstrates the Saga pattern implementation in Medusa: query steps gather local data, custom steps handle external mutations, and compensation functions guarantee that partial failures don't leave your system inconsistent.

## Advanced Patterns: Hooks and Context Injection

Hooks provide extension points that allow other modules to inject context before or after step execution. In [`packages/core/core-flows/src/order/workflows/fetch-shipping-option.ts`](https://github.com/medusajs/medusa/blob/main/packages/core/core-flows/src/order/workflows/fetch-shipping-option.ts), the `fetchShippingOptionForOrderWorkflow` exposes hooks for pricing customization:

```typescript
export const fetchShippingOptionForOrderWorkflow = createWorkflow(
  "fetch-shipping-option",
  (input) => {
    fetchShippingOptionForOrderWorkflow.hooks.setPricingContext(
      (ctx) => ({
        ...ctx,
        customDiscount: true,
      })
    )
    // External shipping API call follows
  }
)

```

Use hooks to inject authentication headers, feature flags, or promotional context from external CRM systems without hardcoding provider-specific logic into your core workflow.

## Real-World Implementation: Loyalty Plugin Example

The loyalty plugin in [`packages/plugins/loyalty/src/workflows/store-credit/steps/create-store-credit-accounts.ts`](https://github.com/medusajs/medusa/blob/main/packages/plugins/loyalty/src/workflows/store-credit/steps/create-store-credit-accounts.ts) demonstrates production-grade external service integration:

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

export const createStoreCreditAccountsStep = createStep(
  "create-store-credit-accounts",
  async (input: { customerId: string; amount: number }) => {
    const account = await StoreCreditService.create({
      customer_id: input.customerId,
      balance: input.amount,
    })
    return new StepResponse({ accountId: account.id })
  },
  async ({ accountId }: { accountId: string }) => {
    if (accountId) {
      await StoreCreditService.delete(accountId)
    }
  }
)

```

This pattern applies to any micro-service communication, whether updating inventory in an ERP or syncing customer data to a marketing platform.

## Consuming Workflows in API Routes

Once defined, workflows execute within your API routes using the Medusa container scope:

```typescript
// src/api/admin/orders/[id]/pay.ts
import { MedusaResponse, AuthenticatedMedusaRequest } from "@medusajs/framework/http"
import { createOrderPaymentWorkflow } from "@medusajs/core-flows"

export const POST = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const { id } = req.params
  const { result } = await createOrderPaymentWorkflow(req.scope).run({
    input: { orderId: id },
  })
  res.status(200).json({ payment_intent_id: result.intentId })
}

```

This separation ensures all external service logic remains isolated in the workflow layer while your HTTP layer handles transport concerns.

## Best Practices for External Service Integration

- **Isolate external calls** – Dedicate one step per external service to ensure clear compensation boundaries and simplify debugging.

- **Persist request identifiers** – Store transaction IDs, intent IDs, or external references in `StepResponse` to enable idempotent retries and safe rollbacks.

- **Avoid global mutable state** – Steps must be pure functions; side effects and shared state cause non-deterministic behavior during parallel execution or retries.

- **Leverage hooks for configuration** – Use workflow hooks to inject API keys, base URLs, or feature flags from environment-specific modules rather than hardcoding credentials.

- **Test steps independently** – Since steps are pure functions receiving serializable input, mock external HTTP clients and unit test with Jest or Vitest without loading the full Medusa server.

## Summary

- **Medusa workflows** integrate external services through the `createStep` API in `packages/core/framework/src/workflows`, which wraps third-party HTTP calls or SDK invocations in type-safe, composable units.

- **Compensation functions** provide automatic rollback capabilities, ensuring that failed workflows don't leave external systems in inconsistent states.

- **Hooks and query steps** (`useQueryGraphStep`) allow you to enrich external requests with Medusa data and inject context from other modules without coupling concerns.

- **Real-world implementations** in the loyalty plugin (`packages/plugins/loyalty`) and order workflows (`packages/core/core-flows/src/order`) demonstrate production patterns for payment processors, shipping carriers, and micro-service communication.

## Frequently Asked Questions

### How do I handle authentication for external APIs in Medusa workflows?

Store API keys and secrets in environment variables, then access them within your step functions or inject them through workflow hooks. According to the Medusa source code in `packages/core/core-flows`, steps execute within the standard Node.js process and can access `process.env` directly. For multi-tenant scenarios, use hooks to inject credentials based on the request context.

### What happens if an external service call fails during a workflow execution?

Medusa captures exceptions thrown within step functions and initiates a rollback sequence. The engine executes compensation functions for all previously completed steps in reverse chronological order. For transient failures, configure retry policies on the step level or implement idempotency keys within your external service calls to ensure safe re-execution.

### Can I use any npm package inside a Medusa workflow step?

Yes, workflow steps are standard TypeScript functions that can import any npm package compatible with your Node.js version. The `createStoreCreditAccountsStep` in `packages/plugins/loyalty` demonstrates importing service classes, while `packages/modules/providers/auth-google` shows wrapping external OAuth SDKs. Ensure packages don't rely on browser-specific APIs or global mutable state.

### How do I test workflow steps that call external services?

Mock the external client at the module boundary, then call the step function directly with test inputs. Since steps receive serializable data and return `StepResponse` objects, you can unit test them without initializing the Medusa framework. For integration testing, use the `workflow-testing` utilities to run complete workflows against a test database while stubbing HTTP requests with libraries like `nock` or `msw`.