Notifications Templates

Imposed rule: Work of this type or tasks of this type on this component must also be applied everywhere else it should be applied.

Templates shape the payload rendered for each channel when a rule action fires. They are deterministic, locale-aware artefacts stored alongside rules so Notify.Worker replicas can render identical messages regardless of environment.


1. Template lifecycle

  1. Authoring. Operators create templates via the API (POST /templates) or UI. Each template binds to a channel type (Slack, Teams, Email, Webhook, Custom) and a locale.
  2. Reference. Rule actions opt in by referencing the template key (actions[].template). Channel defaults apply when no template is specified.
  3. Rendering. During delivery, the worker resolves the template (locale fallbacks included), executes it using the safe Handlebars-style engine, and passes the rendered payload plus metadata to the connector.
  4. Audit. Rendered payloads stored in the delivery ledger include the templateId so operators can trace which text was used.

2. Template schema reference

FieldTypeNotes
templateIdstringStable identifier (UUID/slug).
tenantIdstringMust match the tenant header in API calls.
channelTypeNotifyChannelTypeDetermines connector payload envelope.
keystringHuman-readable key referenced by rules (tmpl-critical).
localestringBCP-47 tag, stored lower-case (en-us, bg-bg).
bodystringTemplate body; rendered strictly without executing arbitrary code.
renderModeenumMarkdown, Html, AdaptiveCard, PlainText, or Json. Guides connector sanitisation.
formatenumSlack, Teams, Email, Webhook, or Json. Signals delivery payload structure.
descriptionstring?Optional operator note.
metadatamap<string,string>Sorted map for automation (layout hints, fallback text).
createdBy/createdAtstring?, instantAuto-populated.
updatedBy/updatedAtstring?, instantAuto-populated.
schemaVersionstringAuto-upgraded on persistence.

Templates are normalised: string fields trimmed, locale lower-cased, metadata sorted to preserve determinism.


3. Variables, helpers, and context

Templates receive a structured context derived from the Notify event, rule match, and rendering metadata.

PathDescription
event.*Canonical event envelope (kind, tenant, ts, actor).
event.scope.*Namespace, repository, digest, image, component identifiers, labels, attributes.
payload.*Raw event payload (e.g., payload.verdict, payload.delta.*, payload.links.*).
rule.*Rule descriptor (ruleId, name, labels, metadata).
action.*Action descriptor (actionId, channel, digest, throttle, metadata).
policy.*Policy metadata when supplied (revisionId, name).
topFindings[]Top-N findings summarised for convenience (vulnerability ID, severity, reachability).
digest.*When rendering digest flushes: window, openedAt, itemCount.

Built-in helpers mirror the architecture dossier:

HelperUsage
severity_icon severityReturns emoji/text badge representing severity.
link text urlProduces channel-safe hyperlink.
pluralize count "finding"Adds plural suffix when count != 1.
truncate text maxLengthCuts strings while preserving determinism.
code textFormats inline code (Markdown/HTML aware).

Connectors may expose additional helpers via partials, but must remain deterministic and side-effect free.


4. Sample templates

4.1 Slack (Markdown + block kit)

{{#*inline "findingLine"}}
- {{severity_icon severity}} {{vulnId}} ({{severity}}) in `{{component}}`
{{/inline}}

*:rotating_light: {{payload.summary.total}} findings {{#if payload.delta.newCritical}}(new critical: {{payload.delta.newCritical}}){{/if}}*

{{#if topFindings.length}}
Top findings:
{{#each topFindings}}{{> findingLine}}{{/each}}
{{/if}}

{{link "Open report in Console" payload.links.ui}}

4.2 Email (HTML + text alternative)

<h2>{{payload.verdict}} for {{event.scope.repo}}</h2>
<p>{{payload.summary.total}} findings ({{payload.summary.blocked}} blocked, {{payload.summary.warned}} warned)</p>
<table>
  <thead><tr><th>Finding</th><th>Severity</th><th>Package</th></tr></thead>
  <tbody>
    {{#each topFindings}}
    <tr>
      <td>{{this.vulnId}}</td>
      <td>{{this.severity}}</td>
      <td>{{this.component}}</td>
    </tr>
    {{/each}}
  </tbody>
</table>
<p>{{link "View full analysis" payload.links.ui}}</p>

When delivering via email, connectors automatically attach a plain-text alternative derived from the rendered content to preserve accessibility.


5. Preview and validation


6. Best practices


7. Attestation & signing lifecycle templates (NOTIFY-ATTEST-74-001)

Attestation lifecycle events (verification failures, expiring attestations, key revocations, transparency anomalies) reuse the same structural context so operators can differentiate urgency while reusing channels. Every template must surface:

7.1 Template keys & channels

EventTemplate keyRequired channelsOptional channelsNotes
Verification failure (attestor.verification.failed)tmpl-attest-verify-failSlack sec-alerts, Email supply-chain@, Webhook (Pager/SOC)Teams risk-war-room, Custom SIEM feedInclude failure code, Rekor UUID, last-known good attestation link.
Expiring attestation (attestor.attestation.expiring)tmpl-attest-expiry-warningEmail summary, Slack reminderDigest window (daily)Provide expiration window, renewal instructions, expiresIn helper.
Key revocation/rotation (authority.keys.revoked, authority.keys.rotated)tmpl-attest-key-rotationEmail + WebhookSlack (if SOC watches channel)Add rotation batch ID, impacted tenants/services, remediation steps.
Transparency anomaly (attestor.transparency.anomaly)tmpl-attest-transparency-anomalySlack high-priority, Webhook, PagerDutyEmail follow-upShow Rekor index delta, witness ID, anomaly classification, recommended actions.

Assign these keys when creating templates so rule actions can reference them deterministically (actions[].template: "tmpl-attest-verify-fail").

7.2 Context helpers

7.3 Slack sample (verification failure)

:rotating_light: {{attestation_status_badge payload.failure.status}} verification failed for `{{payload.subject.digest}}`
Signer: `{{fingerprint payload.signer.kid}}` ({{payload.signer.algorithm}})
Reason: `{{payload.failure.reasonCode}}` — {{payload.failure.reason}}
Last valid attestation: {{link "Console report" payload.links.console}}
Rekor entry: {{link "Transparency log" payload.links.rekor}}

7.4 Email sample (expiring attestation)

<h2>Attestation expiry notice</h2>
<p>The attestation for <code>{{payload.subject.repository}}</code> (digest {{payload.subject.digest}}) expires on <strong>{{payload.attestation.expiresAt}}</strong>.</p>
<ul>
  <li>Issued: {{payload.attestation.issuedAt}}</li>
  <li>Signer: {{payload.signer.kid}} ({{payload.signer.algorithm}})</li>
  <li>Time remaining: {{expires_in payload.attestation.expiresAt event.ts}}</li>
</ul>
<p>Please rotate the attestation before expiry. Reference <a href="{{payload.links.docs}}">renewal steps</a>.</p>

7.5 Webhook sample (transparency anomaly)

{
  "event": "attestor.transparency.anomaly",
  "tenantId": "{{event.tenant}}",
  "subjectDigest": "{{payload.subject.digest}}",
  "rekorIndex": "{{payload.transparency.rekorIndex}}",
  "witnessId": "{{payload.transparency.witnessId}}",
  "anomaly": "{{payload.transparency.classification}}",
  "detailsUrl": "{{payload.links.console}}",
  "recommendation": "{{payload.recommendation}}"
}

7.6 Offline kit guidance


Imposed rule reminder: Work of this type or tasks of this type on this component must also be applied everywhere else it should be applied.


8. Incident mode templates (NOTIFY-OBS-55-001)

Incident toggles are high-noise events that must pierce quiet hours and include audit-ready context. Use dedicated templates so downstream tooling can distinguish activation vs. recovery and surface the required evidence.

Required context keys

Template keys

Slack sample (start)

:rotating_light: Incident mode activated for {{payload.incidentId}}
Reason: {{payload.reason}}
Trace: {{link "root span" payload.links.trace}} · Evidence: {{link "bundle" payload.links.evidence}}
Retention extended to {{payload.retentionDays}} days (baseline {{payload.retentionBaselineDays}})
Quiet hours overridden: {{payload.quietHoursOverride}}
Legal: {{payload.legal.jurisdiction}} (ticket {{payload.legal.ticket}})

Email sample (stop)

<h2>Incident mode cleared: {{payload.incidentId}}</h2>
<p>Stopped at {{payload.stoppedAt}} — retention reset to {{payload.retentionBaselineDays}} days.</p>
<p>Timeline: {{link "view timeline" payload.links.timeline}} · Audit log: {{payload.legal.logPath}}</p>

See src/Notifier/StellaOps.Notifier/docs/incident-mode-rules.sample.json for ready-to-import rules referencing these templates with quiet-hour overrides and legal logging metadata.


9. NIS2 effectiveness threshold-breach template

NIS2 effectiveness alerts use the Notify event kind nis2.effectiveness.threshold_breached and payload schema nis2-effectiveness-threshold-breach-v1. The producer is expected to pass the Policy control-register ownership fields through without expanding human actor details. The live WebService bridge accepts this kind through POST /api/v1/events/nis2 only when the Notify event queue is configured; otherwise it returns 503 notify_event_queue_unavailable with missing prerequisite codes instead of claiming delivery.

Required payload fields:

FieldNotes
tenantIdTenant boundary; must match the Notify event envelope tenant.
areaNumberNIS2 thematic area 1..13.
controlIdControl-register control id.
kpiKeyStable KPI key from the NIS2 KPI catalogue.
metricNameTelemetry metric name, for example nis2_area_9_deprecated_crypto_findings_count.
observedValue / targetNumeric observed value and effective target.
severitywarning or critical; copied into Notify event attributes for rule matching.
evidenceRefReplayable evidence reference.
targetHash / overrideHashOptional sha256: hashes for the default target document and tenant override.
responsibleRoleCopied from the NIS2 control register.
routing{ kind: "responsibleRole", role, target: "role:<responsibleRole>", source: "nis2-control-register-v1" }.

The built-in template key is tmpl-nis2-effectiveness-threshold-breach. Notify routes the default rule action to role:<responsibleRole> so deployments can map role aliases to concrete Slack, Teams, email, PagerDuty, or webhook channels without duplicating Policy control ownership in Notify.

Notify owns the event contract, default template key, and role-route rule shape. Telemetry or Platform still owns threshold evaluation and emission of the event when a KPI crosses a configured target.