Notification templates
Every notification is rendered from a subject template and a body template using Go template syntax. You can rely on the built-in per-event defaults, or define custom templates under Settings → Notifications → Templates.
How a template is chosen
For each event, the sender resolves the template in this order:
- The template chosen on the matching rule (the Message Template dropdown in Settings → Notifications → Rules, or
template_idvia the API). - The org-level default template for the channel's type (the template marked Default for
smtp,slack, orteams). - The built-in default for the event type (see variables by event — every built-in subject/body uses only that event's variables). The exact built-in message per event is viewable in the UI via Templates → View built-in defaults.
- A minimal hard-coded fallback (
CertAutoPilot Notification: {{.event_type}}).
Custom templates are org-wide and bound to a channel type, not to an event type. A custom template therefore renders for every event that reaches that channel — write it using only the always-available variables, or guard event-specific variables with {{if .var}}…{{end}}.
- A template's channel type must match the rule's channel — assigning, say, an
smtptemplate to a Slack rule is rejected (its HTML would not render correctly on Slack). The Rules form only lists templates that match the selected channel. - One template can be marked Default per channel type. Marking a new template as default automatically clears the previous default for that channel type.
Different templates for different events
A template itself is not tied to an event — the rule is what binds a template to specific events. To send different messages for different events, create a separate rule per event (or per group of events that share a message):
- Rule A: events =
cert.expired→ channel Ops Email → template Expiry Alert - Rule B: events =
distribution.failed→ channel Ops Email → template Distribution Failure
A rule may list several events; that single template then renders for all of them, so use {{if .var}}…{{end}} to vary event-specific details (or split into separate rules). A rule can list exactly one event, so fully per-event templating is possible. See Routing & rules.
Template syntax
- Standard Go template syntax:
{{.variable}},{{if .var}}…{{else}}…{{end}}. - No template functions are registered —
upper,date,printfpipelines with custom helpers, sprig, etc. are not available. Variables arrive pre-formatted as strings (e.g.expires_atis RFC 3339,expiration_dateisYYYY-MM-DD). - Missing variables render as empty strings (
missingkey=zero) — referencing a variable an event doesn't provide never errors, it just prints nothing. - Syntax is validated when you save a template; invalid syntax is rejected with
400.
Example (works for any event):
Subject: [CertAutoPilot] {{.event_type}}{{if .primary_domain}} — {{.primary_domain}}{{end}}
Body: <p>Event <b>{{.event_type}}</b>{{if .error}}: {{.error}}{{end}}</p>
<p>Certificate: {{.certificate_id}} / Job: {{.job_id}}</p>
Per-channel rendering
| Channel type | Subject | Body |
|---|---|---|
smtp | text template | HTML template (values are HTML-escaped); delivered as an HTML email |
slack | text template → header block | text template, then HTML converted to plain text (<b> → *bold*, <p>/<br> → newlines, other tags stripped); delivered as Slack blocks (mrkdwn) |
teams | text template → bold TextBlock | same HTML-to-plain conversion; delivered as an Adaptive Card |
Write bodies as simple HTML (<p>, <b>, <br>, <code>) — email renders it directly and Slack/Teams degrade it to readable plain text.
Variables by event
Three variables are injected into every event: {{.event_type}}, {{.certificate_id}}, {{.job_id}}. (For scheduler sweep events, job_id carries the dedup bucket key — e.g. a date — rather than a job UUID; for domain events, certificate_id carries the domain tracking ID.)
Event-specific variables:
| Event | Variables |
|---|---|
cert.issued | primary_domain, domains, issuer (ACME path); issuer_type (msca path); validation_method (manual-DNS path) |
cert.renewed | primary_domain, domains; issuer_type (MSCA) |
cert.renewal_failed | primary_domain, error; issuer_type (MSCA) |
cert.expiring_soon | primary_domain, expires_at |
cert.revoked | primary_domain, domains, cert_name, triggered_by, reason |
cert.revocation_detected | primary_domain, cert_name, revocation_source; revoked_at, revocation_reason when the CA reports them |
distribution.success | primary_domain, module_type, distribution_id, target_count, succeeded_count, failed_count, targets (names, capped with +N more) |
distribution.failed | module_type, error, distribution_id, target_count, succeeded_count, failed_count, targets, failed_targets (name + host + error code, capped); primary_domain (may be empty on some failure paths) |
distribution.rollback_success | distribution_id, primary_domain, rollback_status, auto_rollback, failure_source, error |
distribution.rollback_failed | distribution_id, primary_domain, rollback_status, auto_rollback, failure_source, error |
domain.expiring_soon | domain_name, expiration_date, days_remaining, registrar |
domain.expired | domain_name, expiration_date, registrar |
domain.dangling_dns | domain_name, dangling_reason |
domain.dnssec_missing | domain_name |
domain.no_dmarc | domain_name |
approval.requested | operation_type, primary_domain, requested_by |
approval.approved | operation_type, primary_domain, approved_by |
approval.rejected | operation_type, primary_domain, rejected_by, reject_reason |
approval.executed | operation_type, primary_domain |
approval.expired | operation_type, requested_by (no primary_domain) |
cert.expired and job.failed are subscribable and have built-in templates, but the current release contains no emitter for them — templates for these events never fire. Use cert.expiring_soon and job-level monitoring instead.
Creating a custom template
- Go to Settings → Notifications → Templates → New Template.
- Set Name, Channel Type (
smtp,slack, orteams), Subject Template, Body Template. - Optionally toggle Default — the template becomes the org-wide default for that channel type (fallback step 2 above).
- Save. Syntax errors are rejected at save time.
To bind a template to a specific rule instead of the whole channel type, set template_id on the rule via the API (the rule form UI does not expose it yet).
Testing
There is no template preview/render endpoint. To validate end-to-end formatting, use the channel's Send test action (POST /api/v1/projects/{projectId}/notification-channels/{id}/test) — note it sends a fixed test message, not your template — then trigger a real low-stakes event (e.g. issue a staging certificate) to see the rendered output.
Resetting to default
Templates fall through the resolution chain, so resetting is just deletion:
- Delete a rule-bound template (or clear the rule's
template_id) → the org default for the channel type applies. - Delete (or un-default) the org default → the built-in per-event defaults apply.
API
Org-scoped; listing requires authentication, mutations require Admin:
GET /api/v1/notification-templates
POST /api/v1/notification-templates
PATCH /api/v1/notification-templates/{id}
DELETE /api/v1/notification-templates/{id}