Jinja Basics
LAST UPDATED: MAR 13, 2025
Jinja is a Python-syntax-inspired templating language used within D3 playbooks for the dynamic retrieval, manipulation, and presentation of data.
Data and Expressions
Data Types
A data type is a classification that defines a value's structure, behavior, manipulation, and valid operations. Jinja in D3 playbooks supports nine distinct types.
PRIMITIVES
These are basic types that store single values.
String (
str
) – Example:"D3 Security"
Integer (
int
) – Example:2025
Float (
float
) – Example:3.14159263
Boolean (
bool
) – Example:True
orFalse
(case insensitive)None (
NoneType
) – Example:None
(case insensitive)
COLLECTIONS
These store multiple values.
List (
list
) – Ordered collection of values.
EXAMPLECODE[ 1, 2, 3, 4 ]
Tuple (
tuple
) – Ordered, immutable collection of values.
EXAMPLECODE( 1, 2, 3 )
READER NOTE
In the playbook task output, D3 automatically converts tuples to lists.
Dictionary (
dict
) – Key-value pairs.
EXAMPLECODE{ "company": "D3 Security", "employees": 150 }
SPECIAL
Undefined (
Undefined
) – Represents a variable has not been assigned a valueCODE{{ unknown_variable }}
READER NOTE
Will not directly cause a task error.
Will not generate return data in the Playbook Task Details popover after execution.
Assigning Variables
The {% set %}
statement is used to define variables and assign them specific values of a particular data type. These variables can then be referenced and displayed in the task output (Playbook Task Details popover) using the syntax {{ variable_name }}
.
EXAMPLE 1: Intra-Task Variable Assignment and Referencing
{% set user = {
"username": "Alice",
"department": "Security Operations Center",
"role": "Incident Responder",
"access_level": "?",
"last_login": "2024-02-07T10:30:00Z"
} %}
{{ user }}

EXAMPLE 2: Inter-Task Data Referencing and Explicit Override
{% set user = PlaybookData | jsonpath('$.["Defining and Assigning Variables"].returnData') %}
{% set updated_user = {
"username": user.username,
"department": user.department,
"role": user.role,
"access_level": "Tier-2",
"last_login": user.last_login
} %}
{{ updated_user }}

Expressions
An expression is anything that evaluates to a single value. This includes:
Literal Values
(Constant Expressions)Variable References
Arithmetic Expressions
Logical Expressions
Comparisons
Function Calls (Macros)
Tests
LITERAL VALUES: Static values assigned directly.
{{ "D3 Security" }}
{{ 2025 }}
{{ true }}
{{ none }}
-20250314-004138.png?inst-v=37c408fd-ec2a-43d8-a264-c9dd08b68b30)
VARIABLE REFERENCES: Retrieving stored values.
{% set threat_level = "Critical" %}
Threat Level: {{ threat_level }}

ARITHMETIC EXPRESSIONS: Performing calculations.
{% set failed_attempts = 5 %}
Lockout Threshold: {{ failed_attempts * 2 }}

LOGICAL EXPRESSIONS: Combining conditions with and
, or
, not
.
{% set is_malicious = True %}
{% set is_whitelisted = False %}
Suspicious: {{ is_malicious and not is_whitelisted }}

COMPARISONS: Evaluates relationships between values via comparison operators, which always return True
or False
.
Operator | Description | Example |
---|---|---|
== | Equal to |
CODE
|
!= | Not equal to |
CODE
|
> | Greater than |
CODE
|
< | Less than |
CODE
|
<= | Greater than or equal to |
CODE
|
>= | Less than or equal to |
CODE
|
{% set x = 10 %}
{% set y = 5 %}
x greater than y: {{ x > y }}
-20250207-173343.png?inst-v=37c408fd-ec2a-43d8-a264-c9dd08b68b30)
FUNCTION CALLS: Custom (user-defined) Jinja functions allow reusable, parameterized logic to process and format data dynamically.
{% macro greet_user(username) %}
Hello {{ username }}!
{% endmacro %}
{{ greet_user("Alice") }}
{{ greet_user("Emily") }}

FILTERS:
{% set log_entry = "User Lex Fridman logged in from 192.168.1.100 at 7:30 AM." %}
Sanitized Log: {{ log_entry
| replace("Lex Fridman", "[REDACTED_USER]")
| replace("192.168.1.100", "[REDACTED_IP]") }}

TESTS: Built-in expressions used to evaluate properties of a value, returning True
or False
.
{% set user_role = "admin" %}
Admin Role Defined: {{ user_role is defined }}
Guest Role Defined: {{ guest_role is defined }}

Test | Description | Example |
is defined | Checks whether a variable exists. |
CODE
|
is not defined | Checks whether a variable is not defined. |
CODE
|
is none | Checks whether a variable is None. |
CODE
|
is number | Checks whether a value is a number (integer or float). |
CODE
|
is string | Checks whether a value is a string. |
CODE
|
is sequence | Checks whether a value is a sequence (list, tuple, or string). |
CODE
|
is even | Checks whether a number is even. |
CODE
|
is odd | Checks whether a number is odd. |
CODE
|
is greaterthan | Checks whether a value is greater than another. |
CODE
|
is lessthan | Checks whether a value is less than another. |
CODE
|
is in | Checks whether an item exists in a list or string. |
CODE
|
Control Structures
Filters
Modify variables using chainable filters: {{ variable | filter1(potential_arguments) | filter2 }}
.
READER NOTE
See Jinja2’s list of built-in filters.
EXAMPLE 1: Filter Without Argument
{{ "D3 Security" | length }}

EXAMPLE 2: Filter With Argument
{% set alert = {"description": "Malware detected on endpoint."} %}
{{ alert.description | replace("Malware", "SUSPICIOUS ACTIVITY") }}

EXAMPLE 3: Filter Chaining
{% set alert = {
"timestamps": [
"2024-02-06T14:15:00",
"2024-02-06T10:00:00",
"2024-02-06T12:30:00",
"2024-02-06T09:45:00",
"2024-02-06T16:00:00"
]
} %}
{% set first_time = alert.timestamps | sort | first | replace("T", " ") %}
{% set last_time = alert.timestamps | sort | last | replace("T", " ") %}
First Seen: {{ first_time }}
Last Seen: {{ last_time }}
sort
: Arranges the list in ascending order (earliest to latest timestamps).first
: Retrieves the first element (earliest timestamp) from the sorted list.last
: Retrieves the last element (latest timestamp) from the sorted list.

Conditionals and Loops
Conditional (if) statements and loops are fundamental for controlling logic and automating repetitive tasks. Their Jinja syntax are as follows:
If Statements:
{% if <condition> %} ... {% elif condition %} ... {% else %} ... {% endif %}
For Loops:
{% for variable in iterable %} code {% endfor %}
Common Iterables: List, Range, Dictionary, String
EXAMPLE 1: Conditional Branch Selection — elif
Condition Satisfied
{% set severity = "Medium" %}
{% if severity == "High" %}
Alert requires immediate attention.
{% elif severity == "Medium" %}
Alert should be reviewed soon.
{% else %}
No immediate action required.
{% endif %}

EXAMPLE 2: Nesting of Conditionals
{% set threat = {
"severity": "Critical",
"anomaly_detected": True,
"attack_vector": "Ransomware"
} %}
{% if threat.severity == "Critical" %}
Immediate escalation is required.
{% if threat.anomaly_detected %}
Anomaly confirmed. Engage the incident response team.
{% else %}
Anomaly not detected. Reassess threat data.
{% endif %}
{% else %}
No immediate action is required.
{% endif %}

EXAMPLE 3: Logical Negation (not
) Operator
{% set firewall_enabled = False %}
{% if not firewall_enabled %}
SOC Alert: Firewall is not enabled.
{% endif %}

EXAMPLE 1: Looping Over a List
{% set ip_addresses = [
"192.168.1.10",
"10.0.0.5",
"172.16.0.3",
"203.0.113.25",
"198.51.100.42",
"192.168.2.15"
] %}
{# The allowed IP ranges are private networks: 192.168.x.x, 10.x.x.x, and 172.16.x.x. #}
{% for ip in ip_addresses %}
{% if not (ip.startswith("192.168") or ip.startswith("10.") or ip.startswith("172.16")) %}
External IP flagged: {{ ip }}
{% endif %}
{% endfor %}
READER NOTE
Texts between {#
and #}
are comments, and will not be rendered in the task output.

EXAMPLE 2: Looping Over Nested Lists
{% set file_hash = "a3f1c4b92e7d6f8098b2a1d5c3e4f7g601a2b3c4d5e6f7h8091a2b3c4d5e6f7a" %}
{% set segments = file_hash | batch(8, '') | list %}
{% for index in range(8) %}
Segment {{ index + 1 }}: {{ segments[index] | join('') }}
{% endfor %}
LOGIC BREAKDOWN
Line 2:
The
batch(8, '')
filter breaksfile_hash
(a long string) into chunks of 8 characters.| list
converts the generator produced bybatch()
into a list, enabling indexing and iteration.CODE[ ["a", "3", "f", "1", "c", "4", "b", "9"], ["2", "e", "7", "d", "6", "f", "8", "0"], ["9", "8", "b", "2", "a", "1", "d", "5"], ["c", "3", "e", "4", "f", "7", "g", "6"], ["0", "1", "a", "2", "b", "3", "c", "4"], ["d", "5", "e", "6", "f", "7", "h", "8"], ["0", "9", "1", "a", "2", "b", "3", "c"], ["4", "d", "5", "e", "6", "f", "7", "a"] ]
Line 4:
The
range(8)
generates the indices from0
to7
.The loop iterates through the indices, allowing access to each segment.
Line 5:
index + 1
ensures human-friendly numbering (starting from 1 instead of 0).segments[index]
retrieves the corresponding sub-list fromsegments
.| join('')
converts the list of characters into a single string. That is, fromCODESegment 1: ['a', '3', 'f', '1', 'c', '4', 'b', '9'] Segment 2: ['2', 'e', '7', 'd', '6', 'f', '8', '0'] Segment 3: ['9', '8', 'b', '2', 'a', '1', 'd', '5'] Segment 4: ['c', '3', 'e', '4', 'f', '7', 'g', '6'] Segment 5: ['0', '1', 'a', '2', 'b', '3', 'c', '4'] Segment 6: ['d', '5', 'e', '6', 'f', '7', 'h', '8'] Segment 7: ['0', '9', '1', 'a', '2', 'b', '3', 'c'] Segment 8: ['4', 'd', '5', 'e', '6', 'f', '7', 'a']
into
CODESegment 1: a3f1c4b9 Segment 2: 2e7d6f80 Segment 3: 98b2a1d5 Segment 4: c3e4f7g6 Segment 5: 01a2b3c4 Segment 6: d5e6f7h8 Segment 7: 091a2b3c Segment 8: 4d5e6f7a

EXAMPLE 3: Looping Over a Dynamic Range
{% set ip_address = "192.168.1.100" %}
{% set octets = ip_address.split('.') %}
{% for index in range(octets | length) %}
Octet {{ index + 1 }}: {{ octets[index] }}
{% endfor %}
LOGIC BREAKDOWN
Line 2: The
.split('.')
function splits theip_address
string at each dot (.
). The result is a list of four octets:["192", "168", "1", "100"]
.Line 4:
octets | length
gets the number of elements in the list (4 in this case).range(octets | length)
creates a sequence from0
to3
.The loop iterates through these indices, allowing access to each octet in the list.
Line 5:
index + 1
converts zero-based index into a human-friendly numbering system (starting from 1).octets[index]
retrieves the corresponding octet from the list.

EXAMPLE 4: Looping Over a Dictionary
{% set security_events = {
"Unauthorized Access": "Critical",
"Malware Infection": "High",
"Suspicious Login": "Medium"
} %}
{% for event, severity in security_events.items() %}
Event "{{ event }}" has a severity level of: {{ severity }}
{% endfor %}

EXAMPLE 5: Looping Over a String
{% set filename = "malware$payload.exe" %}
{% set suspicious_chars = "!@#$%^&*" %}
{% for char in filename %}
{% if char in suspicious_chars %}
Alert: Suspicious character "{{ char }}" found in filename.
{% endif %}
{% endfor %}

EXAMPLE 6ADVANCED: Recursive Looping | Generating Nested Lists
{% set incident_list = [
{"Incident": "Incident 1", "related_events": [
{"Incident": "Incident 1.1"},
{"Incident": "Incident 1.2", "related_events": [
{"Incident": "Incident 1.2.1"},
{"Incident": "Incident 1.2.2"}
]}
]},
{"Incident": "Incident 2", "related_events": [
{"Incident": "Incident 2.1"},
{"Incident": "Incident 2.2"}
]}
] %}
{{ '[' }}
{% for object in incident_list recursive %}
"{{ object.Incident }}"
{% if object.related_events %}, [
{{ loop(object.related_events) }}
]{% endif %}
{% if not loop.last %}, {% endif %}
{% endfor %}
{{ ']' }}
READER NOTE
Jinja does not process raw brackets (
[
) outside of a variable expression, so using {{'['
}} explicitly outputs the character.Jinja is case-sensitive when accessing dictionary keys.
LOGIC BREAKDOWN
Line 16:
Begins a loop over
incident_list
, iterating through each incident.The
recursive
keyword in Jinja is a loop modifier that allows a{% for %}
loop to call itself on a sub-list within the current iteration, via theloop()
function.
Line 17:
"{{ object.Incident }}"
Outputs the incident name as a JSON-compatible string.
The double quotes ensure valid JSON formatting.
Lines 18/19: If an
Incident
contains arelated_events
sub-list, the loop calls itself vialoop(object.related_events)
, mimicking the behavior of writing{% for object in related_events recursive %}
without creating a separate loop.
EXAMPLE"Incident 1"
is processed → Findsrelated_events
→ Callsloop(object.related_events)
."Incident 1.2"
has morerelated_events
, so recursion repeats.
Line 21: Without
{% if not loop.last %}, {% endif %}
, the formatting changes significantly because Jinja introduces unintended whitespace and line breaks when rendering recursive loops.If the current item is not the last one,
{% if not loop.last %}, {% endif %}
adds a comma.
Object Name | Is | Comma Added? |
"Incident 1" | ✅ True | ✅ Yes |
"Incident 1.1" | ✅ True | ✅ Yes |
"Incident 1.2" | ✅ True | ✅ Yes |
"Incident 1.2.1" | ✅ True | ✅ Yes |
"Incident 1.2.2" | ❌ False | ❌ No (last item in this level) |
"Incident 2" | ✅ True | ✅ Yes |
"Incident 2.1" | ✅ True | ✅ Yes |
"Incident 2.2" | ❌ False | ❌ No (last item in this level) |
-20250207-195046.png?inst-v=37c408fd-ec2a-43d8-a264-c9dd08b68b30)
Lists
Lists in Jinja store multiple values within square brackets ([]
) and support operations like indexing, slicing, iteration, filtering, and transformation.
EXAMPLE 1: Removing Duplicates from a List
{% set alerts = [
"Failed Login from 192.168.1.10",
"Malware Detected on Endpoint",
"Failed Login from 192.168.1.10",
"Unauthorized Access Attempt",
"Malware Detected on Endpoint"
] %}
{% set unique_alerts = alerts | unique %}
Unique Security Alerts:
{% for alert in unique_alerts %}
- {{ alert }}
{% endfor %}

EXAMPLE 2: Combining Lists
{% set list1 = ["malicious.com", "badactor.net"] %}
{% set list2 = ["phishingsite.org", "hacker.xyz"] %}
{% set combined = list1 + list2 %}
All Malicious Domains: {{ combined }}

EXAMPLE 3: Frequency Mapping and Conditional Membership
{% set security_events = [
{"username": "alice", "event": "Phishing Attempt"},
{"username": "bob", "event": "Brute Force Attack"},
{"username": "alice", "event": "Credential Stuffing"},
{"username": "dave", "event": "DLP Violation"},
{"username": "alice", "event": "Unauthorized Access Attempt"},
{"username": "bob", "event": "Failed MFA Authentication"},
{"username": "eve", "event": "Brute Force Attack"}
] %}
{# Step 1: Extract usernames from the list of events #}
{% set targeted_users = security_events | map('username') | list %}
{# Step 2: Count occurrences using a dictionary #}
{% set user_counts = {} %}
{% for user in targeted_users %}
{% if user_counts[user] is not defined %}
{% set _ = user_counts.update({user: 1}) %}
{% else %}
{% set _ = user_counts.update({user: user_counts[user] + 1}) %}
{% endif %}
{% endfor %}
{# Step 3: Filter users who appear more than once #}
{% set repeated_targets = [] %}
{% for user, count in user_counts.items() %}
{% if count > 1 %}
{% set _ = repeated_targets.append(user) %}
{% endif %}
{% endfor %}
{# Step 4: Output Users Who Were Targeted Multiple Times #}
Repeatedly Targeted Users:
{% for user in repeated_targets %}
- {{ user }}
{% endfor %}
READER NOTE
The throwaway variable _
is used as a convention in Jinja (and Python) when a statement needs to be executed, but its result does not need to be stored or referenced later.
LOGIC BREAKDOWN
Step 1 generates the targeted_users list, including repeats:
['alice', 'bob', 'alice', 'dave', 'alice', 'bob', 'eve']
Step 2 creates the user_counts dictionary:
{'alice': 3, 'bob': 2, 'dave': 1, 'eve': 1}
Step 3 generates the repeated_targets list:
['alice', 'bob']
Step 4 outputs each item in the list produced in step 3, with a dash in front.

EXAMPLE 4: Filtering Lists (Using the selectattr
Filter)
{% set security_events = [
{"event": "Event1", "severity": "Critical"},
{"event": "Event2", "severity": "Low"},
{"event": "Event3", "severity": "Critical"},
{"event": "Event4", "severity": "Informational"},
{"event": "Event5", "severity": "High"},
{"event": "Event6", "severity": "Medium"},
{"event": "Event7", "severity": "High"},
{"event": "Event8", "severity": "Critical"}
] %}
{% set high_critical_events = security_events | selectattr("severity", "in", ["High", "Critical"]) | list %}
High-Priority Security Events:
{% for event in high_critical_events %}
- {{ event.event }} ({{ event.severity }})
{% endfor %}

EXAMPLE 5: Slicing Notation
{% set ipv6_address = "2001:0db8:85a3:0000:0000:8a2e:0370:7334" %}
{% set segments = ipv6_address.split(':') %}
First 4 Segments: {{ segments[:4] | join(':') }}
Last 4 Segments: {{ segments[-4:] | join(':') }}

READER NOTE
Jinja slicing follows Python slicing syntax.
Slicing | Explanation | Example (events = ["A", "B", "C", "D", "E"]) | Output |
list[start:end] | Extracts elements from start to end-1 | events[1:4] | ['B', 'C', 'D'] |
list[:end] | Extracts from the beginning to end-1 | events[:3] | ['A', 'B', 'C'] |
list[start:] | Extracts from start to the end | events[2:] | ['C', 'D', 'E'] |
list[-n:] | Extracts the last n elements | events[-3:] | ['C', 'D', 'E'] |
list[:-n] | Removes the last n elements | events[:-2] | ['A', 'B', 'C'] |
list[start:end:step] | Extracts elements from start to end-1, skipping by step | events[0:5:2] | ['A', 'C', 'E'] |
list[::step] | Extracts all elements, skipping by step | events[::2] | ['A', 'C', 'E'] |
list[::-1] | Reverses the list | events[::-1] | ['E', 'D', 'C', 'B', 'A'] |
Macros (Functions)
Macros are comparable with functions in regular programming languages.
EXAMPLE 1: Convert Hexadecimal to Binary
{% macro hex_to_binary(hex_value) %}
{%- set binary_str = [] -%}
{%- for char in hex_value -%}
{%- set binary_digit = "{:04b}".format(char | int(base=16)) -%}
{%- set _ = binary_str.append(binary_digit) -%}
{%- endfor -%}
{{ binary_str | join(" ") }}
{% endmacro %}
Binary Representation: {{ hex_to_binary("ABCD") }}
READER NOTE
In Jinja, {%-
and -%}
are used to trim whitespace and remove unnecessary new lines that would otherwise be included in the output.
{% ... %}
: Standard Jinja syntax, which preserves surrounding whitespace and new lines.{%- ... %}
: Trims whitespace before the Jinja tag.{% ... -%}
: Trims whitespace after the Jinja tag.{%- ... -%}
: Trims whitespace both before and after the Jinja tag.

EXAMPLE 2: Detecting SQL Injection
{% macro detect_sqli(request) %}
{% set sqli_signatures = ["SELECT ", "UNION ", "DROP ", "--", "' OR '1'='1", "INSERT ", "UPDATE ", "DELETE "] %}
{% set detected_signatures = [] %}
{%- for sig in sqli_signatures -%}
{%- if sig in request -%}
{%- set _ = detected_signatures.append(sig) -%}
{%- endif -%}
{%- endfor -%}
{%- if detected_signatures -%}
{
"alert": "SQL Injection Attempt Detected",
"request": "{{ request }}",
"matched_signatures": {{ detected_signatures | join(", ") }},
"severity": "High"
}
{%- endif -%}
{% endmacro %}
SQLi Detection Output: {{ detect_sqli("GET /search?q=' OR '1'='1' UNION SELECT password FROM users") }}

EXAMPLE 3: Detecting Cross-Site Scripting (XSS)
{% macro detect_xss(input_data) %}
{% set xss_signatures = ["<script", "javascript:", "onerror=", "onload=", "alert(", "document.cookie", "eval("] %}
{% set detected_signatures = [] %}
{%- for sig in xss_signatures -%}
{%- if sig in input_data -%}
{%- set _ = detected_signatures.append(sig) -%}
{%- endif -%}
{%- endfor -%}
{%- if detected_signatures -%}
{
"alert": "Cross-Site Scripting (XSS) Attempt Detected",
"input": "{{ input_data }}",
"matched_signatures": {{ detected_signatures | join(", ") }},
"severity": "High"
}
{%- endif -%}
{% endmacro %}
XSS Detection Output: {{ detect_xss('<script>alert("XSS")</script>') }}

Troubleshooting
EXAMPLE
{% set num = 25 %}
{{ "The number is " + num }}
FIX Convert the integer variable into a string.
{% set num = 25 %}
{{ "The number is " + num | string }}

EXAMPLE
{% set users = [{"name": "Elon"}, {"name": "Alice"}, {"name": "Emily"}] %}
{{ users | map("attribute", "name") | list }}
FIX Iterate through the collection (users list) and append the desired attributes to a new collection.
{% set users = [{"name": "Elon"}, {"name": "Alice"}, {"name": "Emily"}] %}
{% set result = [] %}
{% for user in users %}
{% set _ = result.append(user.name) %}
{% endfor %}
{{ result | list }}

EXAMPLE
{{ "C:\Users\name\Downloads" }}
FIX Use Double Backslashes (\\)
{{ "C:\\Users\\name\\Downloads" }}

EXAMPLE
{% set data = {"users": [{"name": "Alice"}, {"name": "Emily"}]} %}
First User: {{ data.users | first.name }}
READER NOTE
Direct attribute access (.name) immediately after a filter (first) is not supported.
FIX Apply the filter, store it in a variable, then access the attribute via the variable.
{% set data = {"users": [{"name": "Alice"}, {"name": "Emily"}]} %}
{% set first_user = data.users | first %}
First User: {{ first_user.name }}

EXAMPLE
{% set results = [] %}
{% set results = {
Region: "demo_region",
Business Unit: "demo_business_unit",
Application ID: "demo_app_ID",
} %}
{{ results | tojson }}
FIX Enclose dictionary keys in double quotes.
{% set results = [] %}
{% set results = {
"Region": "demo_region",
"Business Unit": "demo_business_unit",
"Application ID": "demo_app_ID",
} %}
{{ results | tojson }}
