Skip to main content

Documentation

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.

KeyRequiredPurpose
codeyesUnique 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.
channelyesDelivery channel: email, in_app, sms, slack, or push.
nameyesHuman label (1–255 chars) shown in the operator template list.
scopeyesWhether 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.
app.manifest.yamlyaml
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.

app.manifest.yamlyaml
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: default

variables_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.

app.manifest.yamlyaml
  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.

scopeWho owns itBehaviour
systemPlatformThe copy is locked. No per-organization override applies; the manifest is the only source. Use for compliance-sensitive or legally-worded messages.
tenant_overridablePlatform ships, an operator may override for one orgThe 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_onlyOperator, per organizationNo 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 features notifications.custom_templates.* and notifications.custom_identities.enabled were 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.

KeyRequiredPurpose
codeyesUnique layout key (same pattern as template code). Templates point at it via layout_code.
channelyesChannel the layout applies to — email in practice.
nameyesHuman label (1–255 chars).
scopeyesWho 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.
app.manifest.yamlyaml
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.