ObjectStackObjectStack

Email Template — System Protocol reference

Declarative template definition consumed by IEmailService.sendTemplate() to render outbound mail.

Email Template Metadata Protocol

Declarative template definition consumed by IEmailService.sendTemplate() to render outbound mail. Read from the metadata registry by (name, locale) — the environment's item over each code package's declaration, the built-in auth templates included — so administrators can author/edit/translate templates in Studio without shipping code, and a Studio save overrides a built-in default at environment level (allowEnvironmentOverlay: true in the metadata registry). No sys_email_template row is read or written on the way.

Aligned with Salesforce EmailTemplate and ServiceNow sysevent_email_action conventions: a single named template is resolved by (name, locale); subject/body strings carry simple {{path.to.value}} placeholders rendered against a per-send data payload.

Source: packages/spec/src/system/email-template.zod.ts

TypeScript Usage

import { EmailTemplateDefinitionSchema, EmailTemplateDefinitionVariableSchema } from '@objectstack/spec/system';
import type { EmailTemplateDefinition, EmailTemplateDefinitionVariable } from '@objectstack/spec/system';

// Validate data
const result = EmailTemplateDefinitionSchema.parse(data);

EmailTemplateDefinition

Properties

PropertyTypeRequiredDescription
namestring✅Template identifier (dotted snake_case)
labelstring✅Display label
categoryneveroptional[REMOVED] email_template.category was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove) — nothing read it: the Studio listing never offered the filter facet it promised, and delivery never depended on it. Delete the key. A template is found by its name, so group templates with a dotted namespace prefix instead (auth.password_reset, crm.welcome). Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
localestringoptional (default: "en-US")BCP-47 locale (e.g. en-US, zh-CN) — the bundle key the resolver matches EXACTLY. A call that NAMES a locale gets exactly one retry rung, the literal en-US, with no language-subtag folding: en and en-US are different ROWS of one bundle and neither reaches the other, so a bundle with no en-US row has no fallback floor for those calls and every recipient locale it does not carry a row for raises TEMPLATE_NOT_FOUND, which is permanent — the delivery dead-letters with no retry. A call naming NO locale is the other case and does not dead-letter: it drops to the bundle's lowest locale tag and renders that silently. Your stack's own i18n.defaultLocale is the wrong tag here unless it is spelled en-US.
subjectstring✅Subject template
bodyHtmlstring✅HTML body template
bodyTextstringoptionalPlain-text body template (auto-derived from HTML when omitted)
variables{ name: string; type?: Enum<'string' | 'number' | 'boolean' | 'date' | 'url' | 'user' | 'record'>; required?: boolean; description?: string }[]optional (default: [])
fromOverride{ name?: string; address: string }optional
replyTostringoptional
activebooleanoptional (default: true)
isSystemneveroptional[REMOVED] email_template.isSystem was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove) — nothing enforced it: no door refused deleting a template that set it. Delete the key. A built-in template is a code package's declaration: a Studio save overrides it for the environment, and discarding that save restores the package's copy. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
descriptionstringoptional
protection{ lock: Enum<'none' | 'no-overlay' | 'no-delete' | 'full'>; reason: string; docsUrl?: string }optionalPackage author protection block — lock policy for this email template.
_lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>optionalItem-level lock — controls overlay & delete (ADR-0010).
_lockReasonstringoptionalHuman-readable reason shown when a write is refused by _lock.
_lockSourceEnum<'artifact' | 'package' | 'env-forced'>optionalLayer that set _lock (artifact | package | env-forced).
_provenanceEnum<'package' | 'org' | 'env-forced'>optionalOrigin of the item (package | org | env-forced).
_packageIdstringoptionalOwning package machine id.
_packageVersionstringoptionalOwning package version.
_lockDocsUrlstringoptionalOptional documentation link surfaced next to _lockReason.

Nested Shape: EmailTemplateDefinition.variables[number]

PropertyTypeRequiredDescription
namestring✅Variable name as referenced in placeholders (snake_case or dotted path)
typeEnum<'string' | 'number' | 'boolean' | 'date' | 'url' | 'user' | 'record'>optional (default: "string")
requiredbooleanoptional (default: false)
descriptionstringoptionalAuthor hint shown in Studio

Nested Shape: EmailTemplateDefinition.protection

PropertyTypeRequiredDescription
lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>✅Lock policy — none | no-overlay | no-delete | full.
reasonstring✅User-visible reason shown when the lock blocks an action.
docsUrlstringoptionalOptional URL the Studio banner links to for more context.

EmailTemplateDefinitionVariable

Properties

PropertyTypeRequiredDescription
namestring✅Variable name as referenced in placeholders (snake_case or dotted path)
typeEnum<'string' | 'number' | 'boolean' | 'date' | 'url' | 'user' | 'record'>optional (default: "string")
requiredbooleanoptional (default: false)
descriptionstringoptionalAuthor hint shown in Studio

On this page