Skip to main content
Event subscribers allow you to react to events in your Medusa application, such as order creation, product updates, or custom events from your workflows. They enable asynchronous, decoupled event-driven architectures.

What is an Event Subscriber?

An event subscriber:
  • Listens to one or more events from the event bus
  • Executes asynchronously when events are triggered
  • Has access to the dependency injection container
  • Can trigger workflows, send notifications, or perform side effects
  • Exports a configuration specifying which events to listen to

Creating a Basic Subscriber

1

Create the Subscriber File

Create a file in src/subscribers/ with a default export function and configuration:
src/subscribers/brand-created.ts
import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"

export default async function brandCreatedHandler({
  event,
  container,
}: SubscriberArgs<{ id: string }>) {
  const logger = container.resolve("logger")

  logger.info(`Brand created with ID: ${event.data.id}`)
}

export const config: SubscriberConfig = {
  event: "brand.created",
}
2

Access Services and Modules

Resolve dependencies from the container:
import { Modules } from "@medusajs/framework/utils"
import { INotificationModuleService } from "@medusajs/framework/types"

export default async function brandCreatedHandler({
  event,
  container,
}: SubscriberArgs<{ id: string }>) {
  const notificationService = container.resolve<INotificationModuleService>(
    Modules.NOTIFICATION
  )

  await notificationService.createNotifications({
    to: "[email protected]",
    channel: "email",
    template: "brand-created",
    data: {
      brand_id: event.data.id,
    },
  })
}

export const config: SubscriberConfig = {
  event: "brand.created",
}
3

Execute Workflows

Trigger workflows in response to events:
import { Modules } from "@medusajs/framework/utils"
import { IWorkflowEngineService } from "@medusajs/framework/types"

export default async function orderCreatedHandler({
  event,
  container,
}: SubscriberArgs<{ id: string }>) {
  const workflowEngine = container.resolve<IWorkflowEngineService>(
    Modules.WORKFLOW_ENGINE
  )

  await workflowEngine.run("send-order-confirmation", {
    input: {
      order_id: event.data.id,
    },
  })
}

export const config: SubscriberConfig = {
  event: "order.created",
}

Subscriber Configuration

Single Event

export const config: SubscriberConfig = {
  event: "brand.created",
}

Multiple Events

export const config: SubscriberConfig = {
  event: ["brand.created", "brand.updated", "brand.deleted"],
}

With Context

Add metadata to identify the subscriber:
export const config: SubscriberConfig = {
  event: "order.created",
  context: {
    subscriberId: "order-notification-handler",
  },
}

Real-World Examples

Payment Webhook Handler

This example shows processing payment webhooks and triggering workflows:
src/subscribers/payment-webhook.ts
import { processPaymentWorkflowId } from "@medusajs/core-flows"
import {
  IPaymentModuleService,
  ProviderWebhookPayload,
} from "@medusajs/framework/types"
import {
  Modules,
  PaymentActions,
  PaymentWebhookEvents,
} from "@medusajs/framework/utils"
import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"

export default async function paymentWebhookHandler({
  event,
  container,
}: SubscriberArgs<ProviderWebhookPayload>) {
  const paymentService = container.resolve<IPaymentModuleService>(
    Modules.PAYMENT
  )

  const processedEvent = await paymentService.getWebhookActionAndData(
    event.data
  )

  if (!processedEvent.data) {
    return
  }

  // Skip unsupported actions
  if (
    processedEvent?.action === PaymentActions.NOT_SUPPORTED ||
    processedEvent?.action === PaymentActions.CANCELED ||
    processedEvent?.action === PaymentActions.FAILED
  ) {
    return
  }

  const workflowEngine = container.resolve(Modules.WORKFLOW_ENGINE)
  await workflowEngine.run(processPaymentWorkflowId, {
    input: processedEvent,
  })
}

export const config: SubscriberConfig = {
  event: PaymentWebhookEvents.WebhookReceived,
  context: {
    subscriberId: "payment-webhook-handler",
  },
}

Send Notifications

Send notifications based on configurable events:
src/subscribers/order-notifications.ts
import { INotificationModuleService } from "@medusajs/framework/types"
import {
  ContainerRegistrationKeys,
  Modules,
  pickValueFromObject,
} from "@medusajs/framework/utils"
import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"

type NotificationConfig = {
  event: string
  template: string
  channel: string
  to: string
  resource_id: string
  data: Record<string, string>
}

const handlerConfig: NotificationConfig[] = [
  {
    event: "order.created",
    template: "order-created-template",
    channel: "email",
    to: "order.email",
    resource_id: "order.id",
    data: {
      order_id: "order.id",
    },
  },
]

const configAsMap = handlerConfig.reduce(
  (acc: Record<string, NotificationConfig[]>, h) => {
    if (!acc[h.event]) {
      acc[h.event] = []
    }
    acc[h.event].push(h)
    return acc
  },
  {}
)

export default async function orderNotifications({
  event,
  container,
}: SubscriberArgs<any>) {
  const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
  const notificationService = container.resolve<INotificationModuleService>(
    Modules.NOTIFICATION
  )

  const handlers = configAsMap[event.name] ?? []
  const payload = event.data

  for (const handler of handlers) {
    try {
      await notificationService.createNotifications({
        template: handler.template,
        channel: handler.channel,
        to: pickValueFromObject(handler.to, payload),
        trigger_type: handler.event,
        resource_id: pickValueFromObject(handler.resource_id, payload),
        data: Object.entries(handler.data).reduce((acc, [key, value]) => {
          acc[key] = pickValueFromObject(value, payload)
          return acc
        }, {}),
      })
    } catch (err) {
      logger.error(`Failed to send notification for ${event.name}`, err.message)
    }
  }
}

export const config: SubscriberConfig = {
  event: handlerConfig.map((h) => h.event),
  context: {
    subscriberId: "order-notifications-handler",
  },
}

Sync Data to External System

src/subscribers/brand-sync.ts
import { BRAND_MODULE } from "../modules/brand"
import { IBrandModuleService } from "../modules/brand/types"
import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export default async function syncBrandToExternal({
  event,
  container,
}: SubscriberArgs<{ id: string }>) {
  const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
  const brandService = container.resolve<IBrandModuleService>(BRAND_MODULE)

  try {
    const brand = await brandService.retrieveBrand(event.data.id)

    // Sync to external system
    await fetch("https://external-api.com/brands", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(brand),
    })

    logger.info(`Synced brand ${brand.id} to external system`)
  } catch (error) {
    logger.error(`Failed to sync brand ${event.data.id}`, error)
  }
}

export const config: SubscriberConfig = {
  event: ["brand.created", "brand.updated"],
  context: {
    subscriberId: "brand-external-sync",
  },
}

Event Data Types

Type your event data for type safety:
interface BrandCreatedEventData {
  id: string
  name: string
}

export default async function brandCreatedHandler({
  event,
  container,
}: SubscriberArgs<BrandCreatedEventData>) {
  // event.data is now typed
  const brandId = event.data.id
  const brandName = event.data.name
}

Built-in Events

Medusa emits events for core entities:
  • order.created, order.updated, order.canceled
  • product.created, product.updated, product.deleted
  • customer.created, customer.updated
  • payment.created, payment.captured
  • fulfillment.created, fulfillment.shipped
And many more. See the Events concept for more information about the event system.

Custom Events from Workflows

Emit custom events using workflow hooks:
src/workflows/brand/create-brand.ts
import { createHook, createWorkflow } from "@medusajs/framework/workflows-sdk"

export const createBrandWorkflow = createWorkflow(
  "create-brand",
  (input) => {
    const brand = createBrandStep(input)

    const brandCreated = createHook("brandCreated", {
      id: brand.id,
      name: brand.name,
    })

    return new WorkflowResponse(brand, {
      hooks: [brandCreated],
    })
  }
)
Subscribe to the hook:
src/subscribers/brand-created.ts
export const config: SubscriberConfig = {
  event: "brandCreated",
}

Error Handling

Handle errors gracefully to prevent subscriber failures from affecting the main flow:
export default async function brandHandler({
  event,
  container,
}: SubscriberArgs<{ id: string }>) {
  const logger = container.resolve("logger")

  try {
    // Your logic here
  } catch (error) {
    logger.error(
      `Failed to process brand event for ${event.data.id}`,
      error.message
    )
    // Don't throw - let the subscriber complete
  }
}

Best Practices

  • Keep subscribers focused on a single responsibility
  • Use workflows for complex business logic instead of putting it in subscribers
  • Handle errors gracefully - don’t let subscriber failures break the main flow
  • Use typed event data for type safety
  • Add context with subscriberId for easier debugging
  • Use the logger for debugging and error tracking
  • Consider idempotency when handling events (events may be delivered multiple times)
  • Don’t perform long-running operations synchronously - use workflows or background jobs

Next Steps

Create Workflows

Build workflows triggered by subscribers

Scheduled Jobs

Create time-based background tasks

Build docs developers (and LLMs) love