Skip to main content

Events

Medusa’s event system enables loose coupling between modules through asynchronous event-driven communication. The Event Bus distributes events to subscribers, allowing you to react to domain events without tight dependencies.

Event Bus

The Event Bus is responsible for:
  • Publishing events - Emit events from services and workflows
  • Routing events - Deliver events to registered subscribers
  • Queuing - Handle event delivery asynchronously
  • Grouping - Batch events within transactions
Medusa provides two Event Bus implementations: event-bus-local for development and event-bus-redis for production with distributed workers.

Event Bus Service

The Event Bus service manages event distribution:
import type {
  Event,
  EventBusTypes,
  InternalModuleDeclaration,
  Logger,
  MedusaContainer,
  Message,
  Subscriber,
} from "@medusajs/framework/types"
import { AbstractEventBusModuleService } from "@medusajs/framework/utils"
import { EventEmitter } from "events"

export default class LocalEventBusService extends AbstractEventBusModuleService {
  protected readonly logger_: Logger
  protected readonly eventEmitter_: EventEmitter
  protected groupedEventsMap_: Map<string, Message[]>

  constructor(
    { logger }: MedusaContainer & InjectedDependencies,
    moduleOptions = {},
    moduleDeclaration: InternalModuleDeclaration
  ) {
    super(...arguments)
    this.logger_ = logger ?? console
    this.eventEmitter_ = new EventEmitter()
    this.eventEmitter_.setMaxListeners(Infinity)
    this.groupedEventsMap_ = new Map()
  }

  async emit<T = unknown>(
    eventsData: Message<T> | Message<T>[],
    options: Record<string, unknown> = {}
  ): Promise<void> {
    const normalizedEventsData = Array.isArray(eventsData)
      ? eventsData
      : [eventsData]

    for (const eventData of normalizedEventsData) {
      await this.groupOrEmitEvent({
        ...eventData,
        options,
      })
    }
  }
}
Source: packages/modules/event-bus-local/src/services/event-bus-local.ts:24-79

Emitting Events

From Services with @EmitEvents

The @EmitEvents decorator automatically emits events after service methods:
import {
  InjectManager,
  InjectTransactionManager,
  MedusaContext,
  EmitEvents,
} from "@medusajs/framework/utils"
import type { Context } from "@medusajs/framework/types"

export class ApiKeyModuleService extends MedusaService {
  @InjectManager()
  @EmitEvents()  // Automatically emits apiKey.created
  async createApiKeys(
    data: ApiKeyTypes.CreateApiKeyDTO | ApiKeyTypes.CreateApiKeyDTO[],
    @MedusaContext() sharedContext: Context = {}
  ): Promise<ApiKeyTypes.ApiKeyDTO | ApiKeyTypes.ApiKeyDTO[]> {
    const [createdApiKeys, generatedTokens] = await this.createApiKeys_(
      Array.isArray(data) ? data : [data],
      sharedContext
    )

    return Array.isArray(data) ? createdApiKeys : createdApiKeys[0]
  }
}
Source: packages/modules/api-key/src/services/api-key-module-service.ts:123-133 Events are automatically named: {entityName}.{operation}
  • apiKey.created
  • product.updated
  • order.deleted

From Workflows with createHook

Workflows emit events using hooks:
import {
  createWorkflow,
  WorkflowData,
  WorkflowResponse,
  createHook,
} from "@medusajs/framework/workflows-sdk"
import { createApiKeysStep } from "../steps"

export const createApiKeysWorkflow = createWorkflow(
  "create-api-keys",
  (input: WorkflowData<CreateApiKeysWorkflowInput>) => {
    const apiKeys = createApiKeysStep(input)

    const apiKeysCreated = createHook("apiKeysCreated", {
      apiKeys,
    })

    return new WorkflowResponse(apiKeys, {
      hooks: [apiKeysCreated],
    })
  }
)
Source: packages/core/core-flows/src/api-key/workflows/create-api-keys.ts:51-64

Manual Event Emission

Emit events manually using the event bus:
import { Modules } from "@medusajs/framework/utils"
import type { IEventBusModuleService } from "@medusajs/framework/types"

export const POST = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const eventBus = req.scope.resolve<IEventBusModuleService>(
    Modules.EVENT_BUS
  )

  await eventBus.emit({
    name: "custom.event",
    data: {
      id: "123",
      status: "completed",
    },
  })

  res.json({ success: true })
}

Emit Multiple Events

Emit multiple events at once:
import type { IEventBusModuleService } from "@medusajs/framework/types"

await eventBus.emit([
  {
    name: "order.placed",
    data: { id: orderId },
  },
  {
    name: "inventory.reserved",
    data: { items: orderItems },
  },
  {
    name: "payment.authorized",
    data: { payment_id: paymentId },
  },
])

Event Subscribers

Subscribers listen for events and execute logic in response.

Creating a Subscriber

Create a subscriber in src/subscribers/:
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"

export default async function handleProductCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const productModule = container.resolve(Modules.PRODUCT)
  const product = await productModule.retrieveProduct(data.id)

  console.log(`Product created: ${product.title}`)
  
  // Send notification, update search index, etc.
}

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

Subscriber Configuration

The config export defines subscriber metadata:
import type { SubscriberConfig } from "@medusajs/framework"

export const config: SubscriberConfig = {
  event: "product.created",  // Event name to listen for
}

Multiple Events

Subscribe to multiple events:
export default async function handleProductEvents({
  event: { name, data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const productModule = container.resolve(Modules.PRODUCT)
  const product = await productModule.retrieveProduct(data.id)

  switch (name) {
    case "product.created":
      console.log(`Product created: ${product.title}`)
      break
    case "product.updated":
      console.log(`Product updated: ${product.title}`)
      break
    case "product.deleted":
      console.log(`Product deleted: ${data.id}`)
      break
  }
}

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

Wildcard Subscribers

Subscribe to all events:
export default async function handleAllEvents({
  event: { name, data },
  container,
}: SubscriberArgs<any>) {
  console.log(`Event: ${name}`, data)
}

export const config: SubscriberConfig = {
  event: "*",  // Listen to all events
}

Event Data Structure

Events follow a consistent structure:
type Message<T = unknown> = {
  name: string              // Event name (e.g., "product.created")
  data: T                   // Event payload
  metadata?: {
    eventGroupId?: string   // For grouped events
    [key: string]: any      // Custom metadata
  }
  options?: {
    internal?: boolean      // Internal event (no logging)
    delay?: number          // Delay in milliseconds
    [key: string]: any
  }
}

Service Event Data

Service events include entity IDs:
{
  name: "product.created",
  data: {
    id: "prod_123"  // Single entity
  }
}

// Or for batch operations:
{
  name: "product.deleted",
  data: {
    ids: ["prod_123", "prod_456"]  // Multiple entities
  }
}

Workflow Hook Data

Workflow hooks include custom data:
{
  name: "apiKeysCreated",
  data: {
    apiKeys: [
      { id: "apikey_123", title: "Storefront" },
      { id: "apikey_456", title: "Mobile App" },
    ]
  }
}

Event Grouping

Group events within transactions to emit them together:
import { Modules } from "@medusajs/framework/utils"
import type { IEventBusModuleService } from "@medusajs/framework/types"

export const myWorkflow = createWorkflow(
  "my-workflow",
  (input: WorkflowInput) => {
    const eventGroupId = "transaction-123"

    // All events with the same eventGroupId are grouped
    await eventBus.emit({
      name: "order.placed",
      data: { id: orderId },
      metadata: { eventGroupId },
    })

    await eventBus.emit({
      name: "inventory.reserved",
      data: { items: orderItems },
      metadata: { eventGroupId },
    })

    // Events are released when explicitly triggered
    await eventBus.releaseGroupedEvents(eventGroupId)
  }
)
Event grouping is useful for workflows where you want to emit multiple events only after all steps succeed.

Delayed Events

Emit events with a delay:
import type { IEventBusModuleService } from "@medusajs/framework/types"

await eventBus.emit({
  name: "reminder.send",
  data: { userId: "user_123" },
  options: {
    delay: 60000,  // Delay 60 seconds
  },
})

Common Event Patterns

Audit Logging

import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"

export default async function auditLogger({
  event: { name, data },
  container,
}: SubscriberArgs<any>) {
  const logger = container.resolve("logger")
  
  logger.info(`[AUDIT] ${name}`, {
    event: name,
    data,
    timestamp: new Date().toISOString(),
  })
}

export const config: SubscriberConfig = {
  event: "*",
}

Send Notifications

import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"

export default async function sendOrderConfirmation({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const orderModule = container.resolve(Modules.ORDER)
  const notificationModule = container.resolve(Modules.NOTIFICATION)

  const order = await orderModule.retrieveOrder(data.id, {
    relations: ["customer"],
  })

  await notificationModule.createNotifications({
    to: order.customer.email,
    channel: "email",
    template: "order-confirmation",
    data: { order },
  })
}

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

Update Search Index

import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"

export default async function updateProductIndex({
  event: { name, data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const productModule = container.resolve(Modules.PRODUCT)
  const indexModule = container.resolve(Modules.INDEX)

  if (name === "product.deleted") {
    await indexModule.delete("products", data.id)
    return
  }

  const product = await productModule.retrieveProduct(data.id)
  
  await indexModule.upsert("products", {
    id: product.id,
    title: product.title,
    description: product.description,
    handle: product.handle,
  })
}

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

Sync to External System

import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"

export default async function syncToErp({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const orderModule = container.resolve(Modules.ORDER)
  const order = await orderModule.retrieveOrder(data.id)

  // Sync to external ERP system
  await fetch("https://erp.example.com/api/orders", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      external_id: order.id,
      total: order.total,
      items: order.items,
    }),
  })
}

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

Event Bus Providers

Local Event Bus

For development and single-server deployments:
import { Modules } from "@medusajs/framework/utils"

export default defineConfig({
  modules: [
    {
      resolve: "@medusajs/event-bus-local",
      options: {
        // Local event bus options
      },
    },
  ],
})

Redis Event Bus

For production with distributed workers:
import { Modules } from "@medusajs/framework/utils"

export default defineConfig({
  modules: [
    {
      resolve: "@medusajs/event-bus-redis",
      options: {
        redisUrl: process.env.REDIS_URL,
      },
    },
  ],
})
Always use Redis Event Bus in production environments with multiple workers to ensure events are distributed correctly.

Best Practices

1

Keep Subscribers Idempotent

Subscribers may be called multiple times for the same event. Design them to handle duplicate calls gracefully.
2

Handle Failures

Wrap subscriber logic in try-catch blocks to prevent one subscriber from blocking others.
3

Avoid Long-Running Operations

Subscribers should be fast. Offload heavy work to background jobs or async workflows.
4

Use Typed Event Data

Define TypeScript types for event payloads to catch errors at compile time.
Subscribers run asynchronously and don’t block the main execution flow. They’re perfect for side effects like notifications, logging, and syncing.

Next Steps

Workflows

Emit events from workflow hooks

Services

Emit events automatically with @EmitEvents

Build docs developers (and LLMs) love