This document is split into three parts:
dbtemplates (builder storage + admin fields)systemandexternaltemplates (code authoring)- final block catalog used by all template types
In builder mode, blocks are persisted in a universal DB model (channel-agnostic):
- block identity and tree:
type,parent_id,position - block payload:
metadata - template language:
localeon template record
Builder endpoints:
GET /api/admin/mpn/templates/:id/blocks- load DB block treePOST /api/admin/mpn/templates/:id/blocks- save DB block tree
Block field definitions are configured in service block schemas:
src/modules/mpn-builder/services-local/email-template-service.tssrc/modules/mpn-builder/services-local/slack-template-service.ts
Those schemas define:
- block type (
heading,row,repeater, etc.) - field keys (
value,label,arrayPath, etc.) - field input types in Admin (
text,textarea, ...)
Admin form fields map directly to metadata keys in DB.
For db templates:
- email flow transforms DB
metadatainto final email blockprops - slack flow transforms DB
metadatainto final Slack Block Kit structure - after transformation, rendering uses the same final block contract as
system/external
This means:
dbdiffers by source/storage- final rendering behavior is shared
For db blocks, prefer:
{{data.*}}placeholders- explicit user text in
metadata
{{translations.*}} is mainly for system/external templates where translation dictionaries are defined in code.
For system and external, you author final blocks directly in code (blocks + translations).
Important distinction:
sectionis a valid final block in code templates (system/external)sectionis not exposed as a DB builder block type- in DB builder, use
groupas container; it is later transformed to final channel structure
Example locations:
- system templates: plugin template config files
- external templates: your package/file imported via
options.extend.services
export const templateBlocks = [
{
type: "section",
props: {
blocks: [
{
type: "heading",
props: {
value: "{{translations.headerTitle}}"
}
},
{
type: "row",
props: {
label: "{{translations.labels.orderNumber}}",
value: "{{data.order.transformed.order_number}}"
}
}
]
}
},
{
type: "repeater",
props: {
arrayPath: "order.transformed.items",
itemBlocks: [
{
type: "product-item",
props: {
label: "{{translations.labels.product}}",
thumbnail: "{{data.order.transformed.items.thumbnail}}",
value: "{{data.order.transformed.items.title}} - {{data.order.transformed.items.quantity}}x"
}
}
]
}
}
]export const templateBlocks = [
{
type: "header",
text: {
type: "plain_text",
text: "{{translations.headerTitle}}"
}
},
{
type: "section",
text: {
type: "mrkdwn",
text: "{{translations.headerDescription}}"
}
},
{
type: "section",
fieldsPath: "inventory_level.stock_locations",
fieldTemplate: {
type: "plain_text",
text: "{{data.inventory_level.stock_locations.name}}: {{data.inventory_level.stock_locations.quantity}}"
}
},
{
type: "divider"
}
]This is the final block model used during rendering, regardless of source (db, system, external).
section(code templates)- note: used directly in
system/externalconfigs - in
dbflow, container behavior usually comes fromgrouptransformed toprops.blocks
- note: used directly in
section- purpose: container
- required data:
props.blocks
heading- purpose: title
- required data:
props.value
text- purpose: paragraph/content
- required data:
props.value
row- purpose: label/value pair
- required data:
props.label,props.value
separator- purpose: visual divider
- required data: none
group- purpose: nested grouping
- required data:
props.blocks
repeater- purpose: repeat nested blocks for array
- required data:
props.arrayPath,props.itemBlocks
product-item(email)- purpose: product row
- required data:
props.label,props.thumbnail,props.value
header- purpose: header text
- required data:
text.type,text.text
sectionwithtext- purpose: body text
- required data:
text.type,text.text
sectionwithfields- purpose: compact list/key-value display
- required data:
fields[]
sectionwithfieldsPath+fieldTemplate(plugin helper)- purpose: dynamically generate
fieldsfrom array data - required data:
fieldsPath,fieldTemplate
- purpose: dynamically generate
actions- purpose: buttons/actions
- required data:
elements[]
divider- purpose: visual separation
- required data: none
- Use
dbwhen template should be editable in Admin. - Use
system/externalwhen template should be managed in code with translation dictionaries. - Keep block payload minimal and block-specific.
- In
db, configure admin-editable keys via service block field definitions. - In
db, prefergroupfor nesting; insystem/external, usesectiondirectly where needed. - Design final blocks first, then map DB metadata to those final fields.
- Templates Documentation - Template types and workflows
- Translations Documentation - Interpolation and locale behavior
- Creating Custom Templates - Contributor guide
- Slack Block Kit Documentation - Slack format reference