This plugin supports three template sources in v1.0.0:
system- built-in templates shipped with the plugindb- templates created and edited in the builder (stored in database)external- templates registered from your own packages/files via plugin options
The primary rendering path is workflow-based and uses template_id.
systemdbexternal
emailslack
Type/service combinations are available through Admin API:
GET /api/admin/mpn/templates/typesGET /api/admin/mpn/templates/types/:type/servicesGET /api/admin/mpn/templates/types/:type/services/:service/templates
Rendering workflows read template_id and resolve source automatically:
template_idstarting withsystem_->systemtemplate registry- any other
template_id-> DB lookup (dbtemplate) externaltemplates are available in service registry when registered in config
Main workflows:
emailServiceWorkflow(mpn-builder-email-service)slackServiceWorkflow(mpn-builder-slack-service)
v1.0.0 uses two different mechanisms depending on template source:
systemandexternaltemplates: can use{{translations.*}}, because they provide translation dictionaries in template configdbtemplates: language is selected by templatelocalestored in DB (channel+localeare set during template creation)
In practice for db templates:
- prefer
{{data.*}}and explicit text in blockmetadata - do not rely on
{{translations.*}}in DB block payload - create separate DB templates per language when needed (e.g.
pl,en)
Use this when template should be part of plugin codebase and versioned with release.
System templates are registered inside local services:
src/modules/mpn-builder/services-local/email-template-service.tssrc/modules/mpn-builder/services-local/slack-template-service.ts
Each template provides:
blockstranslations
Example IDs:
- email:
system_order-placed,system_contact-form - slack:
system_inventory-level,system_product
System templates are read-only from builder DB perspective.
Use this when template should be editable from Admin UI.
Endpoint:
POST /api/admin/mpn/templates
Payload shape:
{
items: [
{
name: "my-order-template",
label: "My order template",
description: "DB editable template",
channel: "email", // or "slack"
locale: "pl", // language assigned to this DB template
subject: "Order {{data.order.id}}",
is_active: true
}
]
}Endpoint:
POST /api/admin/mpn/templates/:id/blocks
Payload shape:
{
template_id: "tmpl_123",
blocks: [
{
type: "heading",
position: 0,
parent_id: null,
metadata: {
value: "Podsumowanie zamowienia"
}
},
{
type: "row",
position: 1,
parent_id: null,
metadata: {
label: "Numer zamowienia",
value: "{{data.order.transformed.order_number}}"
}
}
]
}DB model stores blocks as:
typepositionparent_idmetadata(block config)
Use this when templates live outside plugin repo (e.g. dedicated package).
Register via options.extend.services in medusa-config.ts:
import path from "path"
module.exports = defineConfig({
plugins: [
{
resolve: "@codee-sh/medusa-plugin-notification-emails",
options: {
extend: {
services: [
{
id: "email",
templates: [
{
name: "external_order-summary",
path: path.resolve(
require.resolve("@your-scope/templates/email/order-summary")
)
}
]
}
]
}
}
}
]
})After registration, template becomes available as type external for that service.
import { emailServiceWorkflow } from "@codee-sh/medusa-plugin-notification-emails/workflows/mpn-builder-services/email-service"
const {
result: { html, text, subject },
} = await emailServiceWorkflow(container).run({
input: {
template_id: "system_order-placed", // or db template id
data: templateData,
options: {
locale: "en",
translations: {
headerTitle: "Custom title"
}
}
}
})import { slackServiceWorkflow } from "@codee-sh/medusa-plugin-notification-emails/workflows/mpn-builder-services/slack-service"
const {
result: { blocks },
} = await slackServiceWorkflow(container).run({
input: {
template_id: "system_inventory-level",
data: templateData,
options: {
locale: "en"
}
}
})Useful API endpoints:
GET /api/admin/mpn/templates- DB templatesGET /api/admin/mpn/templates/types- available template typesGET /api/admin/mpn/templates/types/:type/services- services by typeGET /api/admin/mpn/templates/types/:type/services/:service/templates- templates by type+serviceGET /api/admin/mpn/available-templates- template services and builder metadata
In custom subscriber logic, call rendering workflow first, then pass rendered content to notification module.
import { Modules } from "@medusajs/framework/utils"
import { emailServiceWorkflow } from "@codee-sh/medusa-plugin-notification-emails/workflows/mpn-builder-services/email-service"
export default async function customEventHandler({ event: { data }, container }: any) {
const notificationModuleService = container.resolve(Modules.NOTIFICATION)
const {
result: { html, text, subject },
} = await emailServiceWorkflow(container).run({
input: {
template_id: "system_contact-form",
data: {
name: "System Notification",
email: "system@example.com",
message: data.message,
},
options: { locale: "en" },
},
})
await notificationModuleService.createNotifications({
to: "admin@example.com",
channel: "email",
content: { subject, html, text },
})
}- Prefer workflow-based rendering (
emailServiceWorkflow,slackServiceWorkflow) - Use
systemfor release-owned templates,dbfor admin-editable templates,externalfor package-owned templates - Keep template IDs stable and explicit (
system_*for built-ins) - For
dbtemplates keep user-facing text directly in blockmetadataand control language through templatelocale - Validate template data shape before rendering
- Test rendering for all locales and for each template source (
system/db/external)
- Blocks Documentation - Block types, behavior, and DB structure
- Translations Documentation - Interpolation and translation overrides
- Configuration Documentation - Plugin options and external template registration
- Creating Custom Templates - Contributor-focused guide