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

# Events

> Learn about Medusa's event system, including the event bus, subscribers, emitting events, and event-driven architecture patterns.

# 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

<Note>
  Medusa provides two Event Bus implementations: `event-bus-local` for development and `event-bus-redis` for production with distributed workers.
</Note>

## Event Bus Service

The Event Bus service manages event distribution:

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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/`:

```typescript theme={null}
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:

```typescript theme={null}
import type { SubscriberConfig } from "@medusajs/framework"

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

### Multiple Events

Subscribe to multiple events:

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
{
  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:

```typescript theme={null}
{
  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:

```typescript theme={null}
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)
  }
)
```

<Tip>
  Event grouping is useful for workflows where you want to emit multiple events only after all steps succeed.
</Tip>

## Delayed Events

Emit events with a delay:

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
import { Modules } from "@medusajs/framework/utils"

export default defineConfig({
  modules: [
    {
      resolve: "@medusajs/event-bus-redis",
      options: {
        redisUrl: process.env.REDIS_URL,
      },
    },
  ],
})
```

<Warning>
  Always use Redis Event Bus in production environments with multiple workers to ensure events are distributed correctly.
</Warning>

## Best Practices

<Steps>
  <Step title="Keep Subscribers Idempotent">
    Subscribers may be called multiple times for the same event. Design them to handle duplicate calls gracefully.
  </Step>

  <Step title="Handle Failures">
    Wrap subscriber logic in try-catch blocks to prevent one subscriber from blocking others.
  </Step>

  <Step title="Avoid Long-Running Operations">
    Subscribers should be fast. Offload heavy work to background jobs or async workflows.
  </Step>

  <Step title="Use Typed Event Data">
    Define TypeScript types for event payloads to catch errors at compile time.
  </Step>
</Steps>

<Note>
  Subscribers run asynchronously and don't block the main execution flow. They're perfect for side effects like notifications, logging, and syncing.
</Note>

## Next Steps

<CardGroup cols={2}>
  <Card title="Workflows" href="/concepts/workflows">
    Emit events from workflow hooks
  </Card>

  <Card title="Services" href="/concepts/services">
    Emit events automatically with @EmitEvents
  </Card>
</CardGroup>
