Email Action Templates

An Automated Threat Hunting (ATH) playbook monitors your environment on a schedule, runs a query against your data, and takes action when it finds results that match a condition you define. One of the actions a playbook can take is sending an email notification to alert your team that something requires attention.

This topic focuses on writing templates for the Subject and Email Body fields of an email action. An email action also includes recipients, attachments, and other configuration options, but those settings are described in the Automated Threat Hunting playbook documentation. Here you'll learn how template syntax works and how to create effective email notifications.

Overview

When you configure an email action, you can write templates that control the content of the email—one template for the subject and another for the body. The Subject field and Email Body field can each contain plain text, template expressions, or a combination of both. If you use templates in both fields, you can use the same or different template languages and Stellar Cyber will detect the language for each. The Email Body field can also be left empty if you do not want to include a message body.

Templates use placeholder variables that Stellar Cyber fills in automatically with real data from the query results at the time the playbook runs. This means you write the template once, and Stellar Cyber generates a customized email each time the playbook triggers.

Choosing a Sending Mode

Before looking at template syntax, decide how you want the email action to send notifications. The email action supports two sending modes:

  • Run-for-all mode (the default)Stellar Cyber sends one email that summarizes all matching records, and every recipient receives the same email. For example: "Here are the 12 failed login attempts detected in the last hour." This mode is active when the Run for each record option is not selected in the email action settings.

  • Run-for-each modeStellar Cyber sends a separate email for each matching record, and each email contains only the data from this record. For example: "A failed login from IP address 10.0.0.1 was detected on the account for amy." This mode is active when you select Run for each record in Actions: Email.

The advantages that each mode offers can help you choose between them:

  • Use run-for-all mode when a fixed set of recipients, such as a SOC team, needs a single overview of everything the playbook found. Run-for-all mode also supports attachments and works with playbooks that use calculations.

  • Use run-for-each mode when you want each record to produce its own notification. Because the Recipients field supports template variables in this mode, each email can be addressed using data from its own record; for example, each tenant's administrators can receive only the notifications for that tenant. Even with a fixed recipient list, run-for-each mode is useful when you want each record to become a separate, trackable message in a shared inbox or ticketing system.

Each mode provides a different set of data to the template, so the template you write depends on the mode you are using. A template written in the run-for-all style also works in run-for-each mode (see Using Run-for-All Style Templates in Run-for-Each Mode), but a template written in the run-for-each flat style does not work in run-for-all mode. This topic explains both modes, shows you the data available in each one, and provides example templates you can adapt for your own playbooks.

Template Syntax

Both modes support two template languages:

  • Mustache: A simple, logic-less template language. Variables are enclosed in double curly braces: {{variable_name}}. Mustache also supports iteration (looping over a list of items) using section tags: {{#list}} ... {{/list}}.

  • Jinja2: A more powerful template language that supports conditionals, loops, and filters. Variables use {{ variable_name }} and logic uses {% ... %} blocks.

You can use either language in your templates. If a template can be interpreted successfully by either language, Stellar Cyber processes it as Jinja2.

How the Email Body Is Processed When You Save

When you save an email action, Stellar Cyber processes the body as HTML before storing it. This processing is the same in both run-for-all mode and run-for-each mode, because it happens when you save—independent of how the template renders when the playbook triggers. As a result, the body you see after saving can differ from the text you entered. Two behaviors account for most of the differences you might notice.

Line Breaks

To preserve the layout you type, Stellar Cyber converts each line break in the body into an HTML break tag (<br>) when you save. In an email that renders as HTML, a plain line break does not produce a visible break—the mail client collapses it into a space—so this conversion keeps the structure of your message intact in the delivered email.

How this behavior affects you depends on how you author the body:

  • Composing plain, readable text: Press Enter wherever you want a line break. Each line break becomes a <br> in the delivered email, so the layout matches what you typed. You do not need to know HTML.

  • Pasting hand-authored HTML or a template that trims whitespace: The same conversion can work against you. Every line break in your source becomes a visible <br>, including the line breaks you added only for readability. A <br> inserted between structural tags such as <table> and <tr> can distort the layout. A <br> inserted next to a Jinja2 whitespace-trimming tag ({%- -%}) can still appear in the rendered email, because the conversion adds it as a literal tag that the trim markers cannot remove.

Authoring a raw-HTML body currently requires that you keep the markup on a single line and control the breaks yourself with <br> or <p> tags, so that no source line breaks remain for Stellar Cyber to convert. The following examples show what happens to each style when you save, and how the delivered email looks as a result.

Multi-line source – The markup is easy to read, but you have already added your own break tags, and Stellar Cyber adds another one at every source line break—so each intended break is doubled:

Copy
Source:

                <b>Stellar Cyber Alert Notification</b><br />
                <br />
                <b>Alert:</b> Scanner Reputation Anomaly<br />
                <b>Source IP:</b> 195.178.110.137<br />
                <b>Severity:</b> High

                After processing — a <br /> is added at every source line break, on top of the tags you wrote:

            <b>Stellar Cyber Alert Notification</b><br /><br /><br /><br /><b>Alert:</b> Scanner Reputation Anomaly<br /><br /><b>Source IP:</b> 195.178.110.137<br /><br /><b>Severity:</b> High

The following email is what recipients see. Notice how the doubled break tags leave a large gap under the heading and space every line apart:

Stellar Cyber Alert Notification



Alert: Scanner Reputation Anomaly

Source IP: 195.178.110.137

Severity: High

Single-line source – The markup is harder to read, but it contains no line breaks for Stellar Cyber to convert, so it renders exactly as you intend:

Copy
Source:

                <b>Stellar Cyber Alert Notification</b><br /><br /><b>Alert:</b> Scanner Reputation Anomaly<br /><b>Source IP:</b> 195.178.110.137<br /><b>Severity:</b> High

            After processing — unchanged, because there are no source line breaks to convert.

The following email is what recipients see. Notice that the heading, the single blank line, and the three rows all appear exactly as authored:

Stellar Cyber Alert Notification

Alert: Scanner Reputation Anomaly
Source IP: 195.178.110.137
Severity: High

Special Characters and Template Operators

When you save, Stellar Cyber also encodes special characters in the literal text of the body—for example, it converts &, <, >, and " into their HTML-encoded equivalents. It also sanitizes the HTML by removing unsupported tags and attributes and preserving only a safe subset of HTML elements and attributes. As a result, HTML copied from another source, such as a designed email template, can lose images, embedded styles, CSS classes, and other unsupported formatting. This HTML sanitization protects the delivered email against malformed or unsafe markup while preserving supported formatting.

Template tokens are preserved. Stellar Cyber does not encode the contents of {{ }}, {% %}, {{{ }}}, or {# #} tokens, so the comparison operators in your template logic remain intact. For example, {% if event_score >= 75 %} is preserved with the >= operator unchanged, rather than encoded as &gt;=.

Because of this processing, the stored body can look different from the text you entered. Tags might be normalized to a consistent form (for example, <br> becomes <br />), and whitespace might be adjusted. This is expected. To confirm what Stellar Cyber stored and how it renders, use the Run Now option on the playbook and review … | Last Status (see Troubleshooting).

Run-for-All Mode (Default)

Run-for-all mode is the default behavior. It is active whenever the Run for each record option is not selected in the email action configuration.

How It Works

When the playbook triggers, Stellar Cyber collects all matching records from the query results and passes them to your template as a single data structure. Your template then has access to the entire set of results at once. Because all records are available together, you must write iteration logic in your template to loop through the records and display each one.

Stellar Cyber sends one email containing the rendered output of your template. All recipients receive the same email.

Data Available to Templates (Template Context)

In run-for-all mode, the subject template and the email body template have access to the full Interflow record. The following are the key parts of this record that you can reference in your templates:

Variable Path Description
ctx.payload.filtered A list (array) of all matching records. Each item in this list represents one event record. This is the primary data you iterate over in your template.
ctx.payload.filtered_total The total number of matching records. Useful for displaying a count in the subject line or body.
ctx.payload.hits.total.value The total number of hits from the underlying query (before filtering).
ctx.trigger.triggered_time The timestamp when the playbook triggered.
ctx.trigger.scheduled_time The timestamp when the playbook was scheduled to run.
ctx.payload.aggregations If the playbook includes calculations, this object contains the aggregation results.

Each item inside ctx.payload.filtered is a record object with the following structure:

  • _index: The index where the record is stored.

  • _type: The document type.

  • _source: The actual event data fields (for example, user, srcip, event_type, event_score). When referencing a field inside a record, you access it as _source.fieldname.

Template Examples

Mustache Example

These templates produce a single email with a subject identifying the total number of detected alert events and the email body listing all detected events in bulleted format.

Subject

Copy
Alert: {{ctx.payload.filtered_total}} events detected

Email Body

Copy
The following {{ctx.payload.filtered_total}} events were detected at {{ctx.trigger.triggered_time}}:

{{#ctx.payload.filtered}}
- User: {{_source.user}}, Event: {{_source.event_type}}, Source IP: {{_source.srcip}}
{{/ctx.payload.filtered}}

How this works:

  • {{ctx.payload.filtered_total}} inserts the count of matching records.

  • {{#ctx.payload.filtered}} begins a loop over all matching records.

  • Inside the loop, {{_source.user}}, {{_source.event_type}}, and {{_source.srcip}} pull field values from each record.

  • {{/ctx.payload.filtered}} ends the loop.

Jinja2 Example

These templates use Jinja2 to add conditional logic—in this case, labeling events by severity based on their score—to identify the total number of alert events requiring review in the subject and present them in the email body.

Subject

Copy
Security Alert: {{ ctx.payload.filtered_total }} events require review

Email Body

Copy
Playbook triggered at {{ ctx.trigger.triggered_time }}.

{% for hit in ctx.payload.filtered %}
{% if hit._source.event_score >= 75 %}[CRITICAL]
{% elif hit._source.event_score >= 50 %}[MAJOR]
{% elif hit._source.event_score >= 25 %}[MINOR]
{% else %}[NOTICE]
{% endif %} User: {{ hit._source.user }}, Score: {{ hit._source.event_score }}, IP: {{ hit._source.srcip }}
{% endfor %}

How this works:

  • {% for hit in ctx.payload.filtered %} loops through each record, assigning it to the variable hit.

  • The {% if %} / {% elif %} / {% else %} block checks the event score and inserts a severity label accordingly.

  • Field values are accessed as hit._source.fieldname.

Features Available in Run-for-All Mode

  • Attachments: You can attach Interflow data (JSON), CSV calculation results, and JSON calculation results to the email. These options are configured in the email action settings.

  • Recipients: You specify recipients using static email addresses or saved recipient references. Dynamic per-record recipients are not available in this mode.

  • No email throttle: Because only one email is sent per execution, there is no per-execution email limit.

Run-for-Each Mode

Run-for-each mode is activated by selecting the Run for each record option in the email action configuration.

The Run for each record option is available only for playbooks that do not use calculations.

How It Works

Instead of passing all records to a single template rendering, Stellar Cyber processes each matching record individually. For every record that the query returns, Stellar Cyber renders the template using the data in that record and sends a separate email. This means that if the query returns 10 matching records, Stellar Cyber sends 10 emails—each one customized with the data from its corresponding record.

Stellar Cyber works through the matching records one at a time. During each rendering, the record being processed is called the current record—the record whose email is being generated at that moment. For example, if a query matches 20 records, the playbook performs 20 separate renderings, one for each record.

Because each template rendering receives only one record, you do not need to write any iteration logic. You simply reference the fields you want directly.

Data Available to the Template (Template Context)

In run-for-each mode, the template context is built from a single record, and two template styles are supported:

  • Flat style (recommended for new templates) – Reference fields directly by name, such as {{user}}. This style is described in this section.

  • Run-for-all style (supported for compatibility) – Templates written for run-for-all mode, which loop over ctx.payload.filtered, also render correctly in run-for-each mode. See Using Run-for-All Style Templates in Run-for-Each Mode.

For the flat style, Stellar Cyber takes _source fields in the record and "flattens" them to the root level of the context, meaning you can access them directly by field name without any prefix. For convenience, the fields are also accessible under _source.fieldname.

For example, given a record like this—

Copy
{
  "_id": "record_123",
  "_index": "aella-ser-2025.01.01-",
  "_source": {
    "user": "john",
    "email": "john@example.com",
    "event_type": "login_failure",
    "severity": "high",
    "srcip": "10.0.0.1"
  }
}

—the template context becomes:

Access Pattern Example Result
Direct (recommended) {{user}} john
Via _source prefix {{_source.user}} john
Record metadata {{_id}} record_123

Both access patterns produce the same result. The direct pattern (without _source.) is recommended for readability.

Using Run-for-All Style Templates in Run-for-Each Mode

Run-for-each mode also renders templates that were written for run-for-all mode—templates that reference the ctx object and loop over ctx.payload.filtered. This compatibility lets you enable Run for each record on an existing email action and keep the template that the action already uses. You do not need to rewrite templates that were originally written for run-for-all mode.

The key to understanding how the same template behaves in the two modes is what ctx.payload.filtered contains in each mode:

  • In run-for-all mode, ctx.payload.filtered contains all matching records. A loop over the list produces one line for each record, and the single email summarizes all of them.

  • In run-for-each mode, Stellar Cyber renders the template separately for each record, and during each rendering, ctx.payload.filtered contains only that one record. The loop still runs, but it runs only once for each email, so each email shows only the data from its own record.

Example: One Template, Two Modes

Suppose a playbook query matches the following three records:

Record user srcip
Record 1 amy 10.0.0.1
Record 2 bob 10.0.0.2
Record 3 carl 10.0.0.3

The Email Body field contains this run-for-all style template:

Copy
{{#ctx.payload.filtered}}
User: {{_source.user}}, IP: {{_source.srcip}}
{{/ctx.payload.filtered}}

With Run for each record not selected (run-for-all mode), the playbook sends one email, and the loop produces one line for each of the three records:

Copy
User: amy, IP: 10.0.0.1
User: bob, IP: 10.0.0.2
User: carl, IP: 10.0.0.3

With Run for each record selected (run-for-each mode), the playbook sends three emails. During each rendering, ctx.payload.filtered contains only the record for that email, so the loop runs once and each email contains a single line:

Copy
Email 1:  User: amy, IP: 10.0.0.1
Email 2:  User: bob, IP: 10.0.0.2
Email 3:  User: carl, IP: 10.0.0.3

In run-for-each mode, the following flat-style template produces the same three emails without a loop:

Copy
User: {{user}}, IP: {{srcip}}

Choosing Between the Two Styles

  • When you write a new template for run-for-each mode, use the flat style. It is simpler to read and does not require a loop.

  • When you switch an existing email action from run-for-all mode to run-for-each mode, keep the existing template. It renders correctly without changes, and each email shows only the data from its own record.

Template Examples

Basic Notification

Subject

Copy
Security Alert for {{user}}

Body Email

Copy
Hello {{user}},

A {{severity}}-severity event was detected on your account:

  Event type: {{event_type}}
  Source IP:  {{srcip}}

Please review this activity. If you do not recognize it, contact your security team immediately.

How this works:

  • There is no loop. The template references fields directly because it renders against a single record.

  • {{user}}, {{severity}}, {{event_type}}, and {{srcip}} are all fields from _source in the record, made available at the root level.

Using Jinja2 Filters

Subject

Copy
[{{ event_score | to_severity(uppercase=true) }}] Alert for {{ user }}

Body Email

Copy
Event detected at {{ timestamp | ts_format(hour_shift=-5, format="%Y-%m-%d %H:%M:%S") }} (EST):

  User:     {{ user }}
  Type:     {{ event_type }}
  Severity: {{ event_score | to_severity(capitalize=true) }}
  Source:   {{ srcip }}

How this works:

  • | to_severity converts a numeric event score to a human-readable severity label (see Custom Jinja2 Filters below).

  • | ts_format formats a millisecond timestamp into a readable date/time string with an optional timezone offset.

Dynamic Recipients

One of the most powerful features of run-for-each mode is the ability to send each email to a different recipient based on data in the record itself. In the Recipients field of the email action configuration, you can use template variables in addition to static email addresses.

How Recipient Variables Resolve

When Stellar Cyber processes a record in run-for-each mode, it renders the Recipients field against the same record that it uses to render the subject and the email body. The content of each email and the recipient address for that email therefore always come from the same record. There is no separate routing step—the process works like a mail merge, in which each row of a spreadsheet produces one letter and the address on the envelope comes from the same row as the content of the letter.

A recipient variable is a substitution, not a lookup. It resolves to whatever value is in the referenced field of the current record—the record whose email is being generated. Stellar Cyber does not look up recipients in a user directory and does not decide which recipient a record belongs to. For each email to reach a meaningful recipient, the address must either be present in the record or be constructed by the template from a field in the record.

Where the Recipient Address Comes From

The following patterns show where the recipient address can come from:

  • The record contains an email address – Records from identity-centric sources, such as email security events or cloud sign-in logs, often include an email address or user principal name. For example, if a failed sign-in record contains "email": "amy@example.com", the variable {{_source.email}} resolves to amy@example.com, and the email for that record is sent to that address.

  • The template constructs an address from a username – If the record contains a username but not a full email address, and usernames in your organization map predictably to email addresses, you can construct the address in the template. For example, {{user}}@example.com resolves to amy@example.com for a record that contains "user": "amy". Stellar Cyber does not verify that the constructed address exists—the mapping between usernames and addresses is an assumption that you make when you write the template.

  • The template constructs an address from a tenant name – In multi-tenant deployments, each record identifies the tenant that it belongs to. If your notification addresses follow a naming convention, you can construct a per-tenant address. For example, soc-{{_source.tenant_name}}@example.com resolves to soc-acme@example.com for records from the tenant acme and to soc-globex@example.com for records from the tenant globex. As a result, each tenant's administrators receive only the notifications for that tenant. This happens not because Stellar Cyber matches records to recipients, but because the template builds each address from the tenant name in the record, and your mail system defines those addresses as distribution lists.

  • The address is static – A static address, such as soc-team@example.com, always receives the email regardless of record content. Use static addresses when every notification should go to the same recipients. Each record still produces its own email, which is useful when each record should become a separate, trackable message in a shared inbox or a ticketing system.

If your records contain neither an email address nor a field from which an address can be constructed—for example, records that contain only IP addresses and ports—dynamic recipients are not usable for that playbook. In that case, use static recipients.

You can combine dynamic and static recipients. For example, the entry {{_source.email}}, soc-team@example.com sends each email both to the recipient resolved from its record and to the SOC team. If a dynamic recipient fails to resolve for a particular record (for example, the email field is empty), Stellar Cyber skips that recipient for that record and logs the miss.

Features Available in Run-for-Each Mode

  • No iteration needed: Each record gets its own email automatically.

  • Template compatibility: Templates written in the run-for-all style render correctly, with each email showing only the data from its own record.

  • Dynamic recipients: Recipient addresses can be resolved from record fields using template variables.

  • Email throttle: A maximum of 100 emails are sent per playbook execution. If the query returns more than 100 records, only the first 100 generate emails.

  • No attachments: File attachments (interflow JSON, CSV, calculation JSON) are not supported in this mode.

Comparison of the Two Modes

The following table summarizes the key differences between run-for-all mode and run-for-each mode to help you choose the right one for your use case:

Feature Run-for-All Mode Run-for-Each Mode
Emails sent per execution 1 (summarizes all records) 1 per matching record (up to 100)
Template context Full interflow object (ctx.*) A single record. Flat-style fields are available at the root, and run-for-all style templates also render correctly (ctx.payload.filtered contains only the record whose email is being generated)
How to access a field {{_source.user}} inside a loop over ctx.payload.filtered {{user}} or {{_source.user}} directly; run-for-all style syntax also works
Iteration required in template Yes, you must loop through records No, each record is handled automatically. (A loop in a run-for-all style template runs once for each email.)
Dynamic per-record recipients Not supported Supported (via template variables)
Attachments Supported (interflow JSON, CSV, calculation JSON) Not supported
Calculations Compatible Not compatible (option is hidden when calculations are present)
Email throttle None 100 emails per execution

Custom Jinja2 Filters

The following custom Jinja2 filters are available in both run-for-all mode and run-for-each mode. They provide convenient formatting for common data types found in security event records.

ts_format

This filter formats a millisecond-precision timestamp into a human-readable date and time string. You can optionally shift the time by a number of hours (to adjust for timezone) and specify a custom format string.

Copy
{{ timestamp | ts_format }}
  -> "2025-01-15 08:30:00 UTC"

{{ timestamp | ts_format(hour_shift=-5) }}
  -> "2025-01-15 03:30:00 UTC"

{{ timestamp | ts_format(hour_shift=0, format="%Y-%m-%d %H:%M:%S") }}
  -> "2025-01-15 08:30:00"

For a complete list of format codes, refer to strftime.org.

to_severity

This filter converts a numeric event score (0–100) into a severity label. This is useful for making email notifications immediately understandable without requiring the reader to interpret a numeric score.

Score Range Severity Label
75–100 critical
50–74 major
25–49 minor
0–24 notice

Options:

Copy
{{ event_score | to_severity }}                      -> "major"
{{ event_score | to_severity(capitalize=true) }}     -> "Major"
{{ event_score | to_severity(uppercase=true) }}      -> "MAJOR"

Troubleshooting

  • Email not sent for a record (run-for-each mode): If a variable in the email body fails to resolve for a particular record (because the referenced field is missing or empty in that record), Stellar Cyber skips sending an email for that record and logs the skip under AUTOMATION | Action History | Email Actions.

  • Recipient resolution failures (run-for-each mode): If a dynamic recipient template resolves to an empty string or an invalid email format, Stellar Cyber skips that recipient and logs the reason. Check the AUTOMATION | Action History for details on resolution misses.

  • Attachment too large: In run-for-all mode, if an Interflow attachment exceeds the size limit of the email server, Stellar Cyber sends the email without the attachment and logs the following message: Attachment too large (Email server returned 500, message: None), sent without Interflow attachment.

  • Verifying template output: Use the Run Now option on the playbook and then check … | Last Status to see whether the email action executed successfully and review any error details.

Common Mistakes

The most frequent source of confusion is a mismatch between the sending mode and the template. Here are the symptoms and their likely causes:

Symptom Likely Cause
In run-for-each mode, each email shows only one record, even though the template loops over ctx.payload.filtered. This is the expected behavior. In run-for-each mode, ctx.payload.filtered contains only the record for the current email, so the loop runs once. To send one email that lists all matching records, clear the Run for each record option.
Variables like {{user}} or {{srcip}} render as blank in the email. You are using a flat-style template in run-for-all mode (without Run for each record enabled). In run-for-all mode, fields are nested inside records in ctx.payload.filtered. You must iterate and use {{_source.user}} inside the loop.
The email body shows the iteration tags literally (for example, you see {{#ctx.payload.filtered}} as text in the email). A syntax error in the Mustache template is preventing it from rendering. Check that opening and closing section tags match exactly and that variable paths are spelled correctly.