Skip to main content

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:

  1. The template chosen on the matching rule (the Message Template dropdown in Settings → Notifications → Rules, or template_id via the API).
  2. The org-level default template for the channel's type (the template marked Default for smtp, slack, or teams).
  3. 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.
  4. A minimal hard-coded fallback (CertAutoPilot Notification: {{.event_type}}).
note

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 smtp template 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 registeredupper, date, printf pipelines with custom helpers, sprig, etc. are not available. Variables arrive pre-formatted as strings (e.g. expires_at is RFC 3339, expiration_date is YYYY-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 typeSubjectBody
smtptext templateHTML template (values are HTML-escaped); delivered as an HTML email
slacktext template → header blocktext template, then HTML converted to plain text (<b>*bold*, <p>/<br> → newlines, other tags stripped); delivered as Slack blocks (mrkdwn)
teamstext template → bold TextBlocksame 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:

EventVariables
cert.issuedprimary_domain, domains, issuer (ACME path); issuer_type (msca path); validation_method (manual-DNS path)
cert.renewedprimary_domain, domains; issuer_type (MSCA)
cert.renewal_failedprimary_domain, error; issuer_type (MSCA)
cert.expiring_soonprimary_domain, expires_at
cert.revokedprimary_domain, domains, cert_name, triggered_by, reason
cert.revocation_detectedprimary_domain, cert_name, revocation_source; revoked_at, revocation_reason when the CA reports them
distribution.successprimary_domain, module_type, distribution_id, target_count, succeeded_count, failed_count, targets (names, capped with +N more)
distribution.failedmodule_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_successdistribution_id, primary_domain, rollback_status, auto_rollback, failure_source, error
distribution.rollback_faileddistribution_id, primary_domain, rollback_status, auto_rollback, failure_source, error
domain.expiring_soondomain_name, expiration_date, days_remaining, registrar
domain.expireddomain_name, expiration_date, registrar
domain.dangling_dnsdomain_name, dangling_reason
domain.dnssec_missingdomain_name
domain.no_dmarcdomain_name
approval.requestedoperation_type, primary_domain, requested_by
approval.approvedoperation_type, primary_domain, approved_by
approval.rejectedoperation_type, primary_domain, rejected_by, reject_reason
approval.executedoperation_type, primary_domain
approval.expiredoperation_type, requested_by (no primary_domain)
warning

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

  1. Go to Settings → Notifications → TemplatesNew Template.
  2. Set Name, Channel Type (smtp, slack, or teams), Subject Template, Body Template.
  3. Optionally toggle Default — the template becomes the org-wide default for that channel type (fallback step 2 above).
  4. 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}

See also