Notifications
Register email, in-app, SMS, Slack and push notification templates and re-brandable email layouts, seeded into the notifications service on provision.
Your manifest ships the notification templates
An app that sends email — a ticket receipt, an SLA-breach alert, a credit-low warning — does not build its own mail templates or store them in its own database. It declares them in app.manifest.yaml, and the platform materializes them into the shared egav-notifications service. Two arrays do this: spec.notification_templates (the messages) and spec.notification_layouts (the branded chrome the messages render into). Each entry becomes a row egav-notifications can look up by code and render at send time.
The manifest defines the shape of a message — its code, channel, subject, body, variables, and the layout it lives in. How that message is actually delivered — which SMTP relay, which sender address, which channels an org has switched on — is runtime Settings & config, not manifest content. Keep the two straight: the manifest is the template; Settings is the pipeline.
notification_templates — the messages your app sends
Each entry registers one template. The four required keys are code, channel, name, and scope; everything else is optional.
| Key | Required | Purpose |
|---|---|---|
code | yes | Unique lookup key sent to egav-notifications. Dotted snake-case, hyphens allowed (^[a-z][a-z0-9_-]*(\.…)*$), ≤100 chars — e.g. support_ticket_created, egav-ai.credit_pool.threshold_crossed. |
channel | yes | Delivery channel: email, in_app, sms, slack, or push. |
name | yes | Human label (1–255 chars) shown in the operator template list. |
scope | yes | Whether an operator-authored per-organization override may apply: system, tenant_overridable, or tenant_only (see Scope below). |
category | — | Free-form grouping, default transactional. transactional is exempt from unsubscribe / preference suppression — it always delivers. Any other value is subject to the recipient's notification preferences. |
description | — | Free-text note for operators; no runtime effect. |
is_active | — | Whether the template is enabled. Default true. |
subject | — | Subject line. Interpolates {{variable}} tokens. |
body_html | — | Inline HTML body. |
body_text | — | Inline plain-text body (fallback for non-HTML clients). |
body_html_ref | — | Relative path to an HTML file in the submission bundle, inlined into body_html at publish (see below). |
body_text_ref | — | Relative path to a text file, inlined into body_text at publish. |
from_name | — | Override the sender display name for this template. |
from_email | — | Override the sender address. Must be a verified sender identity. |
layout_code | — | code of the layout this template renders into. Omit to use the default layout. |
variables_schema | — | Per-variable metadata — type, required, sample per token. See below. |
recipient_config | — | Free-form routing hints passed to the delivery worker. |
notification_templates:
- code: support_ticket_created
channel: email
name: Support Ticket Created
category: transactional # always delivers — exempt from unsubscribe
scope: tenant_overridable # ships a default; an operator may author an override for one org
is_active: true
subject: "Ticket {{ticket_number}} received — {{subject}}"
body_html: "<div style=\"padding:32px\">…full HTML body kept short here…</div>"
body_text: "Hi {{requester_name}}, ticket {{ticket_number}} created: {{ticket_url}}"
layout_code: default # render into the 'default' layout's chrome
variables_schema:
ticket_number: { type: string, required: true, sample: T-00042 }
subject: { type: string, required: true, sample: Cannot access billing }
requester_name: { type: string, required: true, sample: Alice }
ticket_url: { type: string, required: false, sample: https://portal.example.com/t/abc }Inline the body, or reference a file
A template body can be inlined (body_html / body_text) or pulled from a file shipped alongside the manifest (body_html_ref / body_text_ref). The *_ref form takes a relative path into the submission bundle; the platform reads the file at publish time and inlines its contents into body_html / body_text before storing the template. The stored row is identical either way — refs just keep long HTML out of the YAML.
notification_templates:
- code: welcome_email
channel: email
name: Welcome
scope: tenant_overridable
subject: "Welcome to {{org_name}}"
body_html_ref: templates/welcome.html # inlined into body_html at publish
body_text_ref: templates/welcome.txt # inlined into body_text at publish
layout_code: defaultvariables_schema — the template's contract
variables_schema documents every {{token}} the subject and body expect. It is keyed by variable name; each value carries the variable's type, whether it is required, and a sample used for previews and validation. Declaring a variable here lets the Control Plane render a live preview and lets the caller be checked against the template before a send.
variables_schema:
threshold_pct: { type: integer, required: true, sample: 80 }
used: { type: integer, required: true, sample: 4000 }
remaining: { type: integer, required: true, sample: 1000 }
org_guid: { type: string, required: true, sample: 1111-…-1111 }Scope — who owns the copy
scope is the same three-value enum on both templates and layouts. It decides whether an operator-authored override for one organization may apply on top of the manifest copy. Customers have no template or layout editor: only platform operators author overrides, in the Control Plane.
| scope | Who owns it | Behaviour |
|---|---|---|
system | Platform | The copy is locked. No per-organization override applies; the manifest is the only source. Use for compliance-sensitive or legally-worded messages. |
tenant_overridable | Platform ships, an operator may override for one org | The manifest default renders until an operator authors an override for an organization; the override then applies for that organization only. The common choice for both templates and layouts. |
tenant_only | Operator, per organization | No platform default. The copy is authored per organization by an operator; nothing renders for an organization until it is. |
Organizations customise their messages through branding settings (name, logo, colours, footer notices), not by editing copy. The plan featuresnotifications.custom_templates.*andnotifications.custom_identities.enabledwere retired; do not declare or gate on them.
notification_layouts — the chrome around every email
A layout is the reusable HTML frame — header, footer, wrapper document, inline CSS, and theme colours — that a template renders into. Templates supply the message; the layout supplies the branding. Layouts are almost always tenant_overridable; an organization's logo, brand colours and footer notices come from its branding settings, and an operator can override the chrome for one organization. The four required keys match templates: code, channel, name, scope.
| Key | Required | Purpose |
|---|---|---|
code | yes | Unique layout key (same pattern as template code). Templates point at it via layout_code. |
channel | yes | Channel the layout applies to — email in practice. |
name | yes | Human label (1–255 chars). |
scope | yes | Who may edit the chrome. Typically tenant_overridable. |
description | — | Free-text note. |
is_active | — | Whether the layout is available. Default true. |
is_default | — | Mark the fallback layout used when a template names no layout_code. Default false. |
wrapper_html | — | The outer HTML document. Must contain the single {{content}} slot — the rendered header + body + footer are concatenated and injected there. May also use the theme-colour tokens and {{ unsubscribe_url }}. |
header_html | — | Branded header block, injected as {{ header }}. |
footer_html | — | Footer block (unsubscribe link, legal), injected as {{ footer }}. |
css_inline | — | CSS inlined into the rendered message. |
theme_colors | — | Token→hex map (e.g. primary_color, header_bg_color, link_color) the chrome references. |
notification_layouts:
- code: default
channel: email
name: Default Email Layout
scope: tenant_overridable # an operator may override the chrome for one org
is_active: true
is_default: true # used when a template omits layout_code
wrapper_html: "<!DOCTYPE html><body>…{{ content }}…</body></html>" # single slot
header_html: "<table style=\"background:{{ header_bg_color }};color:{{ header_text_color }}\">…logo…</table>"
footer_html: "<table>…<a href=\"{{ unsubscribe_url }}\">Unsubscribe</a>…</table>"
css_inline: "a { color: #2563eb; } p { margin: 0 0 12px 0; }"
theme_colors:
primary_color: '#2563eb'
header_bg_color: '#1f2937'
link_color: '#2563eb'How a template and layout compose
At send time egav-notifications renders the pieces in a fixed order, then nests them. First the template body renders with its own {{variable}} tokens (from variables_schema). Then the layout's header_html and footer_html each render, with the theme-colour tokens and {{ unsubscribe_url }} available. The three are concatenated — header + body + footer — and injected into the layout's wrapper_html through its single {{content}} slot. So the wrapper needs exactly one placeholder, {{content}}, and the tokens the chrome may reference are the theme colours ({{ primary_color }}, {{ header_bg_color }}, {{ header_text_color }}, {{ footer_bg_color }}, {{ footer_text_color }}, {{ link_color }}) plus {{ unsubscribe_url }}. A template with no layout_code renders into whichever layout is marked is_default: true.
When it reaches egav-notifications
These arrays live in your app's app.manifest.yaml and are published to the Control Plane with the rest of the manifest. The rows are materialized into the egav-notifications service when your tenant instance is provisioned or upgraded — the on_provision / on_upgrade lifecycle hooks. Re-publishing an updated manifest re-seeds them; scope decides whether that overwrites an operator-authored override for an organization (system) or leaves it in place (tenant_overridable / tenant_only).
Declare the message shape here; declare the delivery pipeline — SMTP relay, sender identity, per-channel toggles, retention — in Settings & config. See the manifest overview for where these arrays sit in the full spec.