Templates
Messages are written with Jinja2 syntax (rendered by minijinja): conditions, loops, the built-in filters of minijinja and the filters valerter adds.
templates:
default_alert:
title: "{{ rule_name }} on {{ host }}" # REQUIRED
body: "{{ _msg }}" # REQUIRED
email_body_html: "<p>{{ _msg }}</p>" # REQUIRED for email destinations (text bodies)
accent_color: "#ff0000" # Optional: hex color (Mattermost sidebar, email)
body_format: text # Optional: text (default) or markdown
body_format describes the source of body: text (the default) sends it as
rendered, markdown makes it a Markdown source rendered for each notifier (see
Markdown bodies).
Rule templates and notifier templates
Templates are rendered at two levels:
- Rule templates:
title,bodyandemail_body_htmlof a template, andthrottle.key. They are written once and shared by every destination of the rule. The fields of the event are at the top level:{{ host }},{{ _msg }}. - Notifier templates:
body_template(webhook, Telegram, email) andsubject_template(email). They are written for one channel: the place for markup (JSON for a webhook, HTML for Telegram and email). The fields of the event are underlog:{{ log.host }},{{ log._msg }}.
| Variable | Rule template | Notifier template | Description |
|---|---|---|---|
Event fields (_msg, _time, _stream, host…) |
{{ host }} |
{{ log.host }} |
Every field of the VictoriaLogs line, plus the fields added by the parser |
rule_name |
yes | yes | Name of the rule that triggered |
vl_source |
yes | yes | Name of the VictoriaLogs source the event came from |
title |
yes | The rendered title of the rule template |
|
body |
yes | The rendered body of the rule template; for a Markdown body, its rendering in the notifier format |
|
log_timestamp |
yes | Timestamp of the log line (_time), ISO 8601, for a VictoriaLogs search |
|
log_timestamp_formatted |
yes | The same timestamp, human readable, in the timestamp_timezone (15/01/2026 11:00:00 CET) |
|
accent_color |
email only | The accent_color of the template, none when not set |
|
log |
yes | Every field of the event (see below) |
- Every field of the line VictoriaLogs returns is available, whatever the
parser:
_msg,_time,_streamand the fields of the log. A parser only adds fields (regex captures,json.fields), replacing a field of the same name. rule_nameandvl_sourcewin over an event field of the same name, at both levels.log_timestampandlog_timestamp_formatteddo not exist in a rule template (they render empty there): use the raw{{ _time }}field. The Mattermost footer showslog_timestamp_formattedautomatically.- Mattermost has no notifier template: it sends the rendered
titleandbody.
log holds every field of the event, in the same view as the rule template:
dotted keys are available both flat and expanded ({{ log["k8s.pod"] }} and
{{ log.k8s.pod }}, see below), and a missing field renders empty
({{ log.missing }}, null with tojson). It does not contain rule_name nor
vl_source: use the variables. {{ log | tojson }} renders the whole event as
a JSON object, with dotted keys present twice (flat and expanded). Each value
from log must be escaped for the channel of the notifier template (| tojson
in JSON, | e in Telegram HTML; email body templates escape as HTML
automatically).
log exists only in notifier templates: in a rule template, {{ log }} is the
event field named log if there is one (container output collected by
Fluent Bit, for instance). In a notifier template, a variable that is none of
the above renders empty: valerter logs the warning
Notifier template references unknown variable at startup and in
--validate (see Template validation).
Fields with special characters
VictoriaLogs field names may contain characters that are operators in Jinja,
most commonly / in Kubernetes/OpenShift annotation keys such as
ocp.annotations.authentication.openshift.io/username. Dotted keys are
expanded into nested objects at render time, so reference the last segment
with bracket notation:
{# WRONG: '/' is parsed as a division #}
{{ ocp.annotations.authentication.openshift.io/username }}
{# RIGHT #}
{{ ocp.annotations.authentication.openshift["io/username"] }}
valerter --validate detects this pattern and prints the rewritten expression.
Expansion has two limits, each logged as a warning and never stopping the
alert. A dotted key whose first segment is already a plain field (a and
a.b) is not expanded: {{ a }} keeps the plain value
(skipping dotted-key expansion: top-level scalar already exists). A dotted
key of more than 32 segments is not expanded either: only the flat key exists,
reachable with bracket notation ({{ log["a.b.c…"] }}), so that a malformed
log line cannot build objects thousands of levels deep
(skipping dotted-key expansion: too many segments, the key truncated to 128
bytes in the log).
A top-level field whose name contains / (e.g. io/user-name, with no dot
before it) has no parent object to index, so it cannot be referenced from a
template. Rename it in the rule query with the LogsQL rename pipe, then use
the new name:
query: '_stream:{app="oauth"} | rename "io/user-name" as io_user_name'
# template: {{ io_user_name }}
valerter --validate suggests this rename for {{ io/user-name }}. A /
between two plain identifiers ({{ total/count }}) is read as a division and
accepted: it cannot be told apart from a field named total/count, so a
top-level field like io/username (no ., - or / in its last part) is not
detected and must be renamed the same way.
valerter filters and functions
Besides the built-in filters of minijinja (default, upper, lower,
length, replace, tojson, urlencode, e…), valerter provides the
filters and the function below, available in every template (rule templates,
throttle.key, notifier templates). Filters that minijinja does not provide,
such as truncate, are unknown and refused by the
test render.
| Name | Use | Result |
|---|---|---|
md_escape |
{{ host \| md_escape }} |
The value with a backslash before each character Mattermost’s Markdown engine recognises: \ ` * _ { } [ ] ( ) # + - . ! > \| ~ (web_01 gives web\_01) |
mdv2_escape |
{{ host \| mdv2_escape }} |
The value with a backslash before each character reserved by Telegram parse_mode: MarkdownV2: _ * [ ] ( ) ~ ` > # + - = \| { } . ! and \ |
code |
{{ host \| code }} |
A Markdown code span showing the value literally, whatever backticks it contains; a line break becomes a space |
codeblock(lang) |
{{ _msg \| codeblock('json') }} |
A fenced Markdown code block showing the value literally; lang is optional and keeps only A-Z a-z 0-9 _ + - . # |
md_link(text, url) |
{{ md_link("Logs", vl_url) }} |
A Markdown link whose text is literal (on one line); spaces, control characters, <, >, (, ) and \ of the URL are percent-encoded. A scheme other than http, https or mailto gives text (url), without a link |
urlencode (minijinja) |
{{ ("host:" ~ host) \| urlencode }} |
The value percent-encoded for a URL query (host:web_01 gives host%3Aweb_01) |
Escaping filters. md_escape and mdv2_escape convert their value to a
string (none and a missing value give an empty string) and escape it so it is
displayed literally instead of being read as Markdown.
md_escapeescapes exactly the characters Mattermost’s Markdown engine recognises: other punctuation (:,/,=,?,@…) is left alone, as Mattermost would show the backslash (10\:49).<and&are not neutralised either: harmless in Mattermost, which does not render HTML, but a CommonMark renderer that does would interpret them.mdv2_escapeapplies to MarkdownV2 text outsidepreandcodeentities; inside them, Telegram only requires`and\to be escaped.- For HTML (Telegram
parse_mode: HTML, a custom HTML body), use the built-in| e: it produces the entities Telegram expects (<,>,&). In an email body template, values are escaped as HTML automatically.
Their result is an ordinary string: in an HTML-escaped template (email body,
email_body_html), it is still escaped as HTML afterwards. In the body of a
body_format: markdown template, values are already escaped: md_escape there
is the automatic escaping itself, applied once.
Markdown filters. code, codeblock and md_link build Markdown from
values; they are meant for Markdown bodies, where
their content stays literal wherever they are written (see below). Outside a
Markdown body, they return ordinary strings, escaped like any value (in
email_body_html, {{ x | code }} gives an HTML-escaped `x`). An event
field named md_link would hide the function, as any field hides a global
function of the same name.
Markdown bodies (body_format)
With body_format: markdown, the body of a rule template is written once in
Markdown, and valerter renders it in the format of each notifier: Markdown for
Mattermost, the HTML subset of the Bot API for Telegram, HTML for email, plain
text for a webhook by default (see
Output format per notifier). The
values of the log are shown as text on every channel, unless you insert them
with | safe. As in a text body, the client may still detect bare URLs,
@channel/@here (Mattermost) and mentions in a value: valerter does not
filter them.
templates:
disk_alert:
title: "Disk {{ host }}"
body_format: markdown
body: |
**{{ host }}** is at {{ usage }}% on {{ mount | code }}
{{ md_link("Logs in VictoriaLogs", "https://vl.example.com/select/vmui?query=" ~ ("host:" ~ host) | urlencode) }}
{{ _msg | codeblock }}
With host=web_01, usage=97, mount=/var and _msg=disk <full> & read-only,
Mattermost receives:
**web\_01** is at 97% on `/var`
[Logs in VictoriaLogs](https://vl.example.com/select/vmui?query=host%3Aweb_01)
```
disk <full> & read-only
```
and Telegram (parse_mode: HTML) receives, as body, after the bold title of
its default body_template:
<b>web_01</b> is at 97% on <code>/var</code>
<a href="https://vl.example.com/select/vmui?query=host%3Aweb_01">Logs in VictoriaLogs</a>
<pre>disk <full> & read-only</pre>
title stays plain text (no escaping, no Markdown), and email_body_html, if
present, is still rendered as HTML. A template without body_format, or with
body_format: text, is rendered as written.
Automatic escaping
In a Markdown body, every value inserted with {{ ... }} is escaped: each
ASCII punctuation character gets a backslash, so a value is always read as
literal text (a_b*c stays a_b*c, never italics). The markup written in the
template itself (**...**, [...](...), lists…) is interpreted.
{{ summary | safe }}is the only way to insert a value without escaping: the value is trusted Markdown (bold, links…).{{ value | tojson }}is escaped like any value: the JSON text is shown literally.| eescapes for the current context, which is Markdown here, not HTML (and never twice):{{ v | e }}is the same as{{ v }}.- The line breaks of a value stay line breaks, but a value cannot change the
structure around it: each leading space of a line of the value becomes a
no-break space (U+00A0, four for a tab) and an empty line a lone U+00A0. So
**{{ _msg }}**stays bold over a multi-line message,- {{ _msg }}stays one item, and the indentation of a stack trace stays visible instead of turning into a code block. The renderings,plainincluded, contain these U+00A0. - The characters U+E000 and U+E001 of a value are shown as U+FFFD (valerter
uses them internally for
code,codeblockandmd_link).
Markdown filters in a Markdown body
The content of code, codeblock and md_link is always literal, wherever
they are written: after a list marker, in a quote, on an indented line or in
the middle of a line.
codeblockcuts the paragraph it is written in:Log: {{ _msg | codeblock }}gives the paragraphLog:, then the block. Where a block cannot stand (in**...**, a heading or the text of a link), it becomes a code span; in a code block of the template, its text is written as is.- A Markdown filter must end its expression:
{{ (v | code) ~ '!' }}or{{ v | code | upper }}gives an ordinary string, escaped like any value (backticks shown). md_linkdoes not encode the query of the URL: encode the values you put in it with the built-inurlencodefilter, as in the example above (("host:" ~ host) | urlencodegiveshost%3Aweb_01).
Recognised Markdown
CommonMark plus ~~strikethrough~~, limited to: paragraphs, line breaks,
bold, italics, strikethrough, code spans, code blocks, links, block
quotes, lists, headings and thematic breaks. An image becomes a link to the
image (its alternative text as link text).
- A single line break is kept as a line break: alerts are line oriented.
- Raw HTML in the source (
<b>,<br>,<div>…) is shown as text, never interpreted. - Tables are not recognised: their lines are shown as text.
- Links are only emitted for the
http,httpsandmailtoschemes, in the template as withmd_link;[x](javascript:...)showsx (javascript:...). - Nesting is limited to 32 levels (quotes, lists, emphasis…): a deeper
element is not recognised, its text is kept. A log line inserted with
| safecannot exhaust the stack of the process.
Pitfall: {{ x }} inside a fence or backticks. A value written inside a
code block or a code span of the template is escaped too, and code shows its
content literally, backslashes included. With _msg=disk <full> & read-only:
```
{{ _msg }}
```
shows disk \<full\> \& read\-only, and `{{ mount }}` shows \/var.
Write {{ _msg | codeblock }} and {{ mount | code }} instead.
Template validation
Templates (title, body, email_body_html, throttle.key, and the notifier
subject_template/body_template) are checked for syntax, then test-rendered
twice with placeholder values where every field is defined:
- every condition on a field is true and every loop over a field iterates one
element, so
{% if %}bodies and{% for %}bodies are checked; - every condition on a field is false, every loop is empty and
is definedis false for a field, so{% else %}branches,{% if not x %}and{% if x is not defined %}bodies are checked.
The test render refuses only errors that do not depend on the event’s values, in either pass:
- an unknown filter, test, function or method (
{{ _msg | truncate(50) }},{% if host is nosuchtest %}), reported as<field> render: ..., including after a built-in or valerter filter applied to a field ({{ status | int }}-{{ host | truncat(10) }},{{ host | split('.') | first }} {{ host | nosuch }}), in anelsebranch or in a loop body ({% for k, v in m | items %}{{ v | nosuch }}{% endfor %}); - the
/operator applied to a field path (see Fields with special characters).
The body of a body_format: markdown template is test-rendered with the
Markdown escaping active, as in production; code, codeblock, md_link,
urlencode and tojson are checked like the other filters (an unknown filter
after them is reported).
In a notifier template (body_template, subject_template), a top-level
variable that is not part of its context (see
Rule templates and notifier templates)
is not refused, since it renders empty, but logs a warning when the notifier is
built, at startup and in --validate:
WARN Notifier template references unknown variable notifier=hook field=body_template variable=host
The usual cause is a log field written {{ host }} instead of
{{ log.host }}. Loop and set variables and global functions (range,
namespace, dict) are not reported.
Conversions and arithmetic on fields ({{ status | int }}, {{ (latency |
float) > 1.5 }}, {{ ratio | round }}, {{ count + 1 }}) are accepted: their
outcome depends on the real values. A built-in filter applied to a field
(| int, | float, | round, | split, | upper, | items…) does not
stop the check: the rest of the template is still verified. An error of that
kind at runtime falls back to a generic message (rule templates), to the
<rule>:error key (throttle) or to a failed delivery (notifier templates).
Limits of the test render (these parts are not checked):
- Arithmetic on a raw field (
{{ count + 1 }}) stops the current pass without error: what follows it is not checked in that pass. VictoriaLogs fields are strings, so this expression also fails at runtime: write{{ count | int + 1 }}, which is checked and works. - The body of an
elifis reached by neither pass (the first takes theif, the second theelse). - A loop nested over a loop item (
{% for y in x %}inside{% for x in items %}) does not iterate, and unpacking an item without| items({% for a, b in x %}) stops the pass.
Email body HTML requirement
A template used by a rule with an email destination must define
email_body_html, the HTML body of the email, unless its body is Markdown
(body_format: markdown): the HTML rendering of the body is then the email
body, and email_body_html, when present, still takes priority. Valerter checks
this at startup and in --validate (enabled rules only), and refuses the
configuration otherwise.
email_body_html is rendered with HTML auto-escaping (values are escaped) and
is only used by email notifiers: Mattermost, Telegram and webhooks receive
body. How the email body template inserts it is described in
Email body templates.