All skills
medusajs avatar

/building-with-medusa

@34c71b3 official
by Medusamedusajs/medusa-agent-skills225 stars
29

Load automatically when planning, researching, or implementing ANY Medusa backend features (custom modules, API routes, workflows, data models, module links, business logic). REQUIRED for all Medusa backend work in ALL modes (planning, implementation, exploration). Contains architectural patterns, best practices, and critical rules that MCP servers don't provide.

Use this Skill: https://skilld.dev/gh/medusajs/medusa-agent-skills/building-with-medusa

This session only. Nothing lands on disk.

referencesubscribers-and-events.md

≈4k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Subscribers and Events

Subscribers are asynchronous functions that execute when specific events are emitted. Use them to perform actions after commerce operations, like sending confirmation emails when an order is placed.

Contents

When to Use Subscribers

Use subscribers when you need to react to events that happen in your application:

  • ✅ Send confirmation emails when orders are placed
  • ✅ Sync data to external systems when products are updated
  • ✅ Trigger webhooks when entities change
  • ✅ Update analytics when customers are created
  • ✅ Perform non-blocking side effects

Don't use subscribers for:

  • ❌ Periodic tasks (use scheduled jobs instead)
  • ❌ Operations that must block the main flow (use workflows instead)
  • ❌ Scheduling future tasks (subscribers execute immediately)

Subscribers vs Scheduled Jobs:

  • Subscriber: Reacts to order.placed event and sends confirmation email (event-driven)
  • Scheduled Job: Finds abandoned carts every 6 hours and sends emails (polling pattern)

Creating a Subscriber

Create a TypeScript file in the src/subscribers/ directory:

// src/subscribers/order-placed.ts
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"

export default async function orderPlacedHandler({
  event: { eventName, data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const logger = container.resolve(ContainerRegistrationKeys.LOGGER)

  logger.info(`Order ${data.id} was placed`)

  // Resolve services
  const orderService = container.resolve(Modules.ORDER)
  const notificationService = container.resolve(Modules.NOTIFICATION)

  // Retrieve full order data
  const order = await orderService.retrieveOrder(data.id, {
    relations: ["customer", "items"],
  })

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

  logger.info(`Confirmation email sent for order ${data.id}`)
}

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

Listening to Multiple Events

// src/subscribers/product-changes.ts
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export default async function productChangesHandler({
  event: { eventName, data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const logger = container.resolve(ContainerRegistrationKeys.LOGGER)

  logger.info(`Product event: ${eventName} for product ${data.id}`)

  // Handle different events
  switch (eventName) {
    case "product.created":
      // Handle product creation
      break
    case "product.updated":
      // Handle product update
      break
    case "product.deleted":
      // Handle product deletion
      break
  }
}

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

Common Commerce Events

⚠️ IMPORTANT: Event data typically contains only the ID of the affected entity. You must retrieve the full data if needed.

Order Events

"order.placed" // Order was placed
"order.updated" // Order was updated
"order.canceled" // Order was canceled
"order.completed" // Order was completed
"order.shipment_created" // Shipment was created for order

Product Events

"product.created" // Product was created
"product.updated" // Product was updated
"product.deleted" // Product was deleted

Customer Events

"customer.created" // Customer was created
"customer.updated" // Customer was updated

Cart Events

"cart.created" // Cart was created
"cart.updated" // Cart was updated

Inventory Events

All of the events below were added in v2.17.3 — they don't exist in earlier versions.

"inventory-item.created" // Inventory items were created
"inventory-item.updated" // Inventory items were updated
"inventory-item.deleted" // Inventory items were deleted
"inventory-level.created" // Inventory levels were created
"inventory-level.updated" // Stocked/reserved quantity changed (e.g. during fulfillment)
"inventory-level.deleted" // Inventory levels were deleted

Use these to sync stock to external systems or alert on low levels. inventory-level.updated carries { id, order_id? } — order_id is set when the change came from an order flow.

Auth Events

"auth.password_reset" // Password reset was requested
"auth.verification_requested" // A verification code was generated (v2.15.5+)
"auth.mfa_enabled" // An MFA factor was enabled (v2.15.5+)
"auth.mfa_disabled" // An MFA factor was disabled
"auth.mfa_recovery_codes_generated" // Recovery codes were generated

⚠️ auth.verification_requested payload changed in v2.16.0. It is now:

{
  entity_id      // The identifier being verified, e.g. an email address
  entity_type    // e.g. "email"
  code_provider  // Defaults to "token"
  auth_identity_id
  code           // The verification code — was called `token` before v2.16.0
  expires_at
  metadata
}

token, actor_type, provider, and provider_identity_id are gone. In subscribers, build the verification URL from code, and guard on entity_type (e.g. if (entity_type !== "email") return) instead of actor_type.

Invite Events

"invite.created" // Invite was created (for admin users)

For a complete list of events, ask MedusaDocs for the specific module's events.

Accessing Event Data

Event Data Structure

interface SubscriberArgs<T> {
  event: {
    eventName: string // e.g., "order.placed"
    data: T // Event payload (usually contains { id: string })
  }
  container: MedusaContainer // DI container
}

Retrieving Full Entity Data

⚠️ IMPORTANT: The data object typically only contains the entity ID. Retrieve the full entity data using services or query:

// src/subscribers/order-placed.ts
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"

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

  // data.id contains the order ID
  logger.info(`Handling order.placed event for order: ${data.id}`)

  // Retrieve full order data with relations
  const { data: orders } = await query.graph({
    entity: "order",
    fields: [
      "id",
      "email",
      "total",
      "customer.*",
      "items.*",
      "items.product.*",
    ],
    filters: {
      id: data.id,
    },
  })

  const order = orders[0]

  // Now you have the full order data
  logger.info(`Order total: ${order.total}`)
  logger.info(`Customer email: ${order.customer.email}`)
}

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

Using Module Services

export default async function productUpdatedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const productService = container.resolve("product")

  // Retrieve product using service
  const product = await productService.retrieveProduct(data.id, {
    select: ["id", "title", "status"],
    relations: ["variants"],
  })

  // Process product
}

Triggering Custom Events

Emit custom events from workflows using the emitEventStep:

// src/workflows/create-review.ts
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { emitEventStep } from "@medusajs/medusa/core-flows"

const createReviewWorkflow = createWorkflow(
  "create-review",
  function (input: { product_id: string; rating: number }) {
    // Create review step
    const review = createReviewStep(input)

    // Emit custom event
    emitEventStep({
      eventName: "review.created",
      data: {
        id: review.id,
        product_id: input.product_id,
        rating: input.rating,
      },
    })

    return new WorkflowResponse({ review })
  }
)

export default createReviewWorkflow

Then create a subscriber for the custom event:

// src/subscribers/review-created.ts
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"

export default async function reviewCreatedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string; product_id: string; rating: number }>) {
  const logger = container.resolve("logger")
  const query = container.resolve("query")

  logger.info(`Review ${data.id} created for product ${data.product_id}`)

  // If rating is low, notify support
  if (data.rating <= 2) {
    const notificationService = container.resolve("notification")
    await notificationService.createNotifications({
      to: "support@example.com",
      template: "low-rating-alert",
      channel: "email",
      data: {
        review_id: data.id,
        product_id: data.product_id,
        rating: data.rating,
      },
    })
  }
}

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

Best Practices

1. Always Use Logging

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

  logger.info(`Handling ${eventName} for ${data.id}`)

  try {
    // Subscriber logic
    logger.info(`Successfully handled ${eventName}`)
  } catch (error) {
    logger.error(`Failed to handle ${eventName}: ${error.message}`)
  }
}

2. Handle Errors Gracefully

Subscribers run asynchronously and don't block the main flow. Log errors but don't throw:

// ✅ GOOD: Catches errors and logs
export default async function mySubscriber({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const logger = container.resolve("logger")

  try {
    // Subscriber logic that might fail
    await sendEmail(data.id)
  } catch (error) {
    logger.error(`Failed to send email: ${error.message}`)
    // Don't throw - subscriber completes gracefully
  }
}

3. Keep Subscribers Fast and Non-Blocking

Subscribers should perform quick operations. For long-running tasks, consider:

  • Queuing the task for background processing
  • Using scheduled jobs instead
  • Breaking the work into smaller steps
// ✅ GOOD: Quick operation
export default async function orderPlacedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const notificationService = container.resolve("notification")

  // Quick: Queue email for sending
  await notificationService.createNotifications({
    to: "customer@example.com",
    template: "order-confirmation",
      channel: "email",
    data: { order_id: data.id },
  })
}

4. Use Workflows for Mutations

If your subscriber needs to perform mutations, use workflows:

// ✅ GOOD: Uses workflow for mutations
import { syncProductWorkflow } from "../workflows/sync-product"

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

  // Execute workflow to sync to external system
  try {
    await syncProductWorkflow(container).run({
      input: { product_id: data.id },
    })
    logger.info(`Product ${data.id} synced successfully`)
  } catch (error) {
    logger.error(`Failed to sync product ${data.id}: ${error.message}`)
  }
}

5. Avoid Infinite Event Loops

Be careful when subscribing to events that trigger more events:

// ❌ BAD: Can cause infinite loop
export default async function productUpdatedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const productService = container.resolve("product")

  // This triggers another product.updated event!
  await productService.updateProducts({
    id: data.id,
    metadata: { last_updated: new Date() },
  })
}

// ✅ GOOD: Add guard condition
export default async function productUpdatedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const logger = container.resolve("logger")
  const query = container.resolve("query")

  // Retrieve product to check if we should update
  const { data: products } = await query.graph({
    entity: "product",
    fields: ["id", "metadata"],
    filters: { id: data.id },
  })

  const product = products[0]

  // Guard: Only update if not already processed
  if (!product.metadata?.processed) {
    const productService = container.resolve("product")
    await productService.updateProducts({
      id: data.id,
      metadata: { processed: true },
    })
  }
}

6. Make Subscribers Idempotent

Subscribers might be called multiple times for the same event. Design them to handle this:

export default async function orderPlacedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const logger = container.resolve("logger")
  const myService = container.resolve("my-service")

  // Check if we've already processed this order
  const processed = await myService.isOrderProcessed(data.id)

  if (processed) {
    logger.info(`Order ${data.id} already processed, skipping`)
    return
  }

  // Process order
  await myService.processOrder(data.id)

  // Mark as processed
  await myService.markOrderAsProcessed(data.id)
}

Complete Example: Order Confirmation Email

// src/subscribers/order-placed.ts
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"

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

  logger.info(`Sending order confirmation for order: ${data.id}`)

  try {
    const query = container.resolve("query")

    // Retrieve full order data
    const { data: orders } = await query.graph({
      entity: "order",
      fields: [
        "id",
        "display_id",
        "email",
        "total",
        "currency_code",
        "customer.first_name",
        "customer.last_name",
        "items.*",
        "items.product.title",
        "shipping_address.*",
      ],
      filters: {
        id: data.id,
      },
    })

    if (!orders || orders.length === 0) {
      logger.error(`Order ${data.id} not found`)
      return
    }

    const order = orders[0]

    // Send confirmation email
    const notificationService = container.resolve("notification")
    await notificationService.createNotifications({
      to: order.email,
      template: "order-confirmation",
      channel: "email",
      data: {
        order_id: order.display_id,
        customer_name: `${order.customer.first_name} ${order.customer.last_name}`,
        items: order.items,
        total: order.total,
        currency: order.currency_code,
        shipping_address: order.shipping_address,
      },
    })

    logger.info(`Order confirmation email sent to ${order.email}`)
  } catch (error) {
    logger.error(
      `Failed to send order confirmation for ${data.id}: ${error.message}`
    )
  }
}

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

Source: SKILL.md on GitHub

1 warning16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    This skill is a comprehensive development guide for building MedusaJS backend applications. It provides detailed architectural patterns, best practices, and implementation checklists for modules, workflows, API routes, and data models. No malicious patterns or security risks were identified.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer7mo

    9/14 files flagged

  • ZeroLeaks5mo

    2 findings · Score: 80/100

Signed by skilld at 34c71b3. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 days ago.

Activeupdated 2 months ago

README badge

README badge for medusajs/medusa-agent-skills/building-with-medusa