What Are Templates?
Templates in Home Assistant let you use dynamic values instead of fixed text or numbers. They use a language called Jinja2 to "fill in" details based on the current state of your smart home.
In plain English: a template lets you make things like automations, notifications, or sensors smarter – so instead of "the door is open," you could have "The back door has been open for {{ states('sensor.door_open_time') }} minutes!"
Why Use Templates?
- Make notifications more useful: "{{ states('sensor.temperature_living_room') }}°C in the living room" instead of a fixed number.
- Combine info: "{{ states('sensor.temp') }}°C and {{ states('sensor.humidity') }}% humidity."
- Automate smarter: Only trigger if the sun is down and someone's home.
- Math & logic: Add, subtract, or compare values.
Where Can You Use Templates?
- Automations & scripts – trigger, condition, or action fields.
- Template sensors – create a sensor that combines or transforms values.
- Notifications – make messages dynamic.
- Dashboards – show calculations or friendly names.
Template Syntax Basics
Jinja2 templates use special tags to insert values, run logic, and control the flow of your templates. Here's what you need to know:
{{ ... }}– Expression/output tag
Use to output a value or result of an expression directly into your template.
Example:
{{ states('sensor.temperature') }}– Logic/control tag
Use to run code blocks, such asifstatements, loops, and assignments.
Example:{% if states('light.kitchen') == 'on' %} The kitchen light is on! {% endif %}{# ... #}– Comment tag
Use to add comments that will not appear in the final output.
Example:{# This is a comment #}
Common Jinja2 Logic Blocks
- If statement: Runs code conditionally.
{% if is_state('binary_sensor.door', 'on') %} Door is open! {% else %} Door is closed. {% endif %} - For loop: Iterate over lists or results.
{% for light in states.light %} {{ light.name }}: {{ light.state }} {% endfor %} - Set variable: Create or assign a value to a variable.
{% set count = 5 %} There are {{ count }} items. - With block: Limit the scope of variables (less common, advanced).
{% with temp = states('sensor.temperature') %} The temperature is {{ temp }}°C. {% endwith %} - Macros (advanced): Define reusable template code (like a function).
{% macro greet(name) -%} Hello, {{ name }}! {%- endmacro %} {{ greet('Ada') }}
Note: Every {% if %} or {% for %} block must be closed with {% endif %} or {% endfor %}. Similarly, macros and other block tags must be closed appropriately.
Tip: Most Home Assistant templates use only {{ ... }} and basic {% if %} or {% for %} blocks, so start simple and experiment!
Whitespace Control
Jinja2 gives you fine-grained control over whitespace to help make your output look tidy and professional. This is especially useful for generating messages or cleaning up output that would otherwise have unwanted blank lines or spaces.
{{- value }}– Removes any whitespace (including newlines) before the expression.{{ value -}}– Removes any whitespace immediately after the expression.{%- logic %}– Removes whitespace before a logic tag (likeif,for, etc).{% logic -%}– Removes whitespace after a logic tag.- You can combine both:
{%- ... -%}trims both sides.
Example:
{%- if is_state('sensor.rain', 'yes') -%} It's raining! {%- else -%} Dry weather today. {%- endif -%} Tip: Leading/trailing whitespace can cause unexpected blank lines, especially when looping or using if/else blocks. Trimming with - helps avoid these issues.
Special Note for Home Assistant Users
- Output destination matters:
- If you use a template for an entity state or attribute (such as a sensor value), all line breaks and extra spaces are stripped. The result will be a single line, no matter how much whitespace is in your template.
- If you use a template for notifications, messages, or in the Markdown card, line breaks are usually kept. However, make sure to use the right type of line ending for your platform:
\n(Unix-style) works for most Home Assistant uses, but Windows systems sometimes expect\r\n.
- For multi-line text, use triple-quotes (
""") in YAML, or be explicit about newlines in your template.
Always test your template in the Developer Tools to see how the output looks in your chosen destination!
Common Jinja2 Operators
Operators in Jinja2 let you compare values, perform math, build logic, and more. Most are similar to those in Python, but with a few unique to Jinja2. Here's a summary of the main operator categories:
- Comparison Operators
==Equal to{{ 2 == 2 }}→True!=Not equal to{{ 3 != 2 }}→True>Greater than{{ 3 > 2 }}<Less than{{ 1 < 2 }}>=Greater than or equal to{{ 2 >= 2 }}<=Less than or equal to{{ 1 <= 2 }}inValue is in list/string{{ 'kitchen' in 'light.kitchen' }}not inValue is not in list/string{{ 'bedroom' not in 'light.kitchen' }}
- Logical Operators
andBoth must be true{{ True and False }}→FalseorAt least one must be true{{ True or False }}→TruenotNegate a value{{ not True }}→False
Tip:
and,or, andnotmust always be lowercase. - Math Operators
+Addition{{ 3 + 2 }}→5-Subtraction{{ 5 - 2 }}→3*Multiplication{{ 2 * 3 }}→6/Division{{ 7 / 2 }}→3.5//Integer (floor) division{{ 7 // 2 }}→3%Modulo (remainder){{ 7 % 2 }}→1**Exponent (power){{ 2 ** 3 }}→8~String concatenation{{ 'Hello, ' ~ 'world!' }}→Hello, world!
- Special/Other Operators
isTest an expression (with a test){{ value is number }}is notNegated test{{ value is not string }}?:Ternary/Inline if (iif()is more common in HA)
{{ 'yes' if value > 0 else 'no' }}
Example combining operators:
{% if states('sensor.temperature') | float > 20 and is_state('binary_sensor.door', 'off') %} Warm and door is closed! {% endif %} Tip: Always be careful with types. For example, states('...') always returns a string, so use | int or | float to compare as numbers.
Filters
Filters in Jinja2 transform or process values and lists. You "pipe" data into a filter with |. Home Assistant adds extra filters on top of standard Jinja2. Here's a comprehensive list, grouped for clarity:
- String & Text Filters
string- Convert anything to a string.{{ 42 | string }}lower- Convert to lowercase.{{ 'ABC' | lower }}→abcupper- Convert to UPPERCASE.{{ 'abc' | upper }}→ABCtitle- Capitalize the first letter of each word.{{ 'my title' | title }}capitalize- Capitalize only the first character.{{ 'word' | capitalize }}→Wordtrim- Remove leading/trailing spaces.{{ ' test ' | trim }}replace('old', 'new')- Replace text.{{ 'abcabc' | replace('a','x') }}indent(n)- Indent lines by n spaces.wordcount- Count words in a string.striptags- Remove HTML tags.{{ '<b>test</b>' | striptags }}wordwrap(n)- Wrap text to fit n characters wide.urlencode- URL-encode the string.{{ 'hello world' | urlencode }}→hello+worldescape- HTML-escape the string.forceescape- Force HTML-escaping even if already safe.truncate(n)- Shorten text to n characters.
- Numbers & Math Filters
float- Convert to a floating-point number.{{ '5.7' | float }}int- Convert to integer.{{ '42' | int }}abs- Absolute value.{{ -5 | abs }}→5round([n])- Round to n decimal places.{{ 3.14159 | round(2) }}max- Largest value in a list.{{ [1,2,3] | max }}min- Smallest value.{{ [1,2,3] | min }}sum- Add all numbers.{{ [1,2,3] | sum }}average- Mean of list (HA only).{{ [1,2,3] | average }}median- Median value (HA only).statistical_mode- Most common value (HA only).log([base])- Logarithm (HA only, advanced).
- Lists, Sequences, & Dictionaries
length- Number of items in list/string.{{ [1,2,3] | length }}list- Convert to a list.sort- Sort a list.{{ [3,1,2] | sort }}reverse- Reverse a list.{{ [1,2,3] | reverse }}unique- Remove duplicates.{{ [1,2,2] | unique }}first- First item.{{ [1,2,3] | first }}last- Last item.{{ [1,2,3] | last }}groupby('attr')- Group items by attribute.random- Pick a random item.join(', ')- List to string.{{ [1,2,3] | join(', ') }}→1, 2, 3map('attr')- Pull out an attribute from each item.select('test')- Filter list by test, e.g.odd,even.reject('test')- Exclude items by test.selectattr('attr','eq','value')- Filter by attribute value.rejectattr('attr','eq','value')- Exclude by attribute value.dictsort- Sort dict by keys/values.attr('attr')- Get attribute from each item.items- Dict items as (key, value) pairs.batch(n)- Split a list into lists of n items each.slice(n)- Return every n-th item from a list.zip- Combine multiple lists together, element-wise.
- Date & Time Filters (Home Assistant specific)
as_datetime- Convert to datetime object (HA).as_local- Convert datetime to local time (HA).as_timestamp- Seconds since 1970 (HA).timestamp_custom('%H:%M')- Format timestamp (HA).timestamp_local- Format timestamp for local time (HA).relative_time- Seconds from now to a time/entity.
- Home Assistant Entity & Device Filters
device_id- Get device ID from entity (HA).device_attr('attr')- Get device attribute (HA).area_id- Get area ID from entity (HA).area_name- Get area name from ID (HA).is_hidden_entity- Check if entity is hidden (HA).has_value- True if state is notunknown/unavailable(HA).device_class- Get device class (HA).
- Other & Advanced
default('value')- Use fallback if value is missing/blank.{{ states('sensor.x') | default('0') }}pprint- Pretty-print value for debugging.tojson- Convert value to JSON string.from_json- Parse JSON string to object/list.safe- Mark as safe HTML (bypass escaping, use with caution).
For a full reference and examples of all filters, see the Jinja2 documentation and Home Assistant templating docs.
Useful Functions
Home Assistant adds many built-in functions to Jinja2, making it easy to interact with your devices, get the time, do math, and more. Here are some of the most useful:
states('entity_id'): Gets the state of an entity as a string.
{{ states('sensor.outdoor_temp') }}→18.5state_attr('entity_id', 'attribute'): Gets the value of an attribute.
{{ state_attr('light.living_room', 'brightness') }}is_state('entity_id', 'state'): Checks if an entity matches a state (returnsTrueorFalse).
{{ is_state('binary_sensor.door', 'on') }}now(): Returns the current date and time as a Pythondatetimeobject.
{{ now().strftime('%H:%M') }}utcnow(): Returns the current UTC date and time.as_timestamp(value): Converts a datetime or state to a UNIX timestamp (seconds since 1970).as_datetime(value): Converts a timestamp or string to adatetimeobject.relative_time(value): Returns time since a timestamp or entity's last change (in seconds).timedelta(days=0, seconds=0, ...): Lets you create or add durations to times.iif(condition, true_val, false_val): "Inline if" function for simple value switching.
{{ iif(is_state('sun.sun', 'above_horizon'), '🌞', '🌙') }}expand('group.name'): Expands a group entity into a list of entities.distance(lat1, lon1, lat2, lon2): Gets the distance (in km) between coordinates.log(value, base): Logarithm function (advanced math).device_attr(), area_id(), area_name(), device_id(), ...: Many more helpers for devices, areas, and integrations.
Example:
The current time is {{ now().strftime('%H:%M:%S') }}
Lists, Dicts, and Tuples in Home Assistant Templates
When working with templates, you'll often need to store, process, or loop over collections of values. Jinja2 supports three main data structures: lists, dicts, and tuples. Home Assistant imposes some unique limitations, so this guide focuses on the safe and practical approaches you'll actually use.
Lists
A list is an ordered collection of items - think of it as a numbered row of boxes, where each box holds a value. Lists are great for things like storing sensor names, results from filters, or any sequence of items. Use square brackets [ ] and separate values with commas.
{% set my_list = ['Conor', 'Mary', 'Jane'] %}
This is the first value: {{ my_list[0] }}
This is the second value: {{ my_list[1] }}
There are {{ my_list | length }} items in the list.
- Indexing: List indexes start at 0, so
my_list[0]is the first item. - Use cases: Looping through sensors, notifications, combining multiple states.
Adding Items to a List
You cannot use append() or extend() in Home Assistant. Instead, combine lists using + to create a new one:
{% set my_list = my_list + ['Alice'] %}
Each operation creates a new list with the extra items.
Replacing or Removing Items
To replace a value, build a new list using slices:
{% set modified_list = my_list[:2] + ['Bob'] + my_list[3:] %}
Now Bob is at position 3: {{ modified_list }}
To remove an item, use filters like reject:
{% set filtered_list = modified_list | reject('equalto', 'Jane') | list %}
Filtered list: {{ filtered_list }} - Use
select,reject,unique, and similar filters for most list tasks. - No
remove(),insert(), orsort()methods - always create a new list instead.
Looping Through a List
{% for name in my_list %}
{{ name }}
{% endfor %} Tuples
A tuple is similar to a list, but immutable - once created, you can't change its contents. Define a tuple with parentheses:
{% set my_tuple = ('Conor', 'Alice', 'Jo') %}
Third value: {{ my_tuple[2] }} In practice, most Home Assistant users stick with lists.
Dictionaries (dicts)
A dict (dictionary) is a collection of key:value pairs, great for structured info - like room names, device details, or lookups. In Home Assistant templates, always use the dict() function to create them (curly braces { } are blocked as unsafe!).
{% set my_dict = dict(name="Conor", website="homeassistantguide.info") %}
Name: {{ my_dict.name }}
URL: {{ my_dict.url }} - Access with
my_dict.nameormy_dict['name']. - To add or replace a key, make a new dict:
{% set new_dict = dict(my_dict, role='teacher') %} - Most Python dict methods (like
update(),setdefault(),items()) are blocked for safety. - Always test dict tricks in Developer Tools before relying on them!
Why So Many Limitations?
For security, Home Assistant restricts any Python methods that could change or leak data. Always use Jinja2's filters (like select, reject, unique) and avoid Python's list/dict methods in templates.
More Examples: Safe List and Dict Tricks
- Filter all "on" lights:
{% set on_lights = states.light | selectattr('state', 'equalto', 'on') | list %} - Get a dict value only if it exists:
{{ my_dict['name'] if 'name' in my_dict else 'unknown' }}
Multiline Filters ({% filter ... %} blocks)
You can apply a filter (like upper, lower, replace, etc.) to a whole block of text using a filter block:
{% filter upper %}
This whole block will be uppercase!
So will this line.
{% endfilter %} You can nest logic inside a filter block and apply other filters as well.
{% filter replace('!', '…') %}
Hello, friend!
Your balance is {{ balance }}!
{% endfilter %} Note: Most filters work here, but some Home Assistant–specific ones may not. Always test in Developer Tools.
Helper Functions: states('...') vs. Attribute Access
Always use Home Assistant's built-in helper functions to access entity states and attributes. Don't use direct object access like states.sensor.kitchen.state in templates! Here's why:
-
states('sensor.kitchen')is safe and always returns a string - even if the entity doesn't exist. -
states.sensor.kitchen.statecan throw an error if the entity is missing or unavailable. -
state_attr('sensor.kitchen', 'unit_of_measurement')returns the attribute orNoneif not present. - Most template errors in Home Assistant come from trying to access missing entity/object attributes directly.
{{ states('sensor.kitchen_temp') }}
{{ state_attr('sensor.kitchen_temp', 'unit_of_measurement') }}
Summary Table:
| Use | Returns | Safe? |
|---|---|---|
states('sensor.x') | String (always) | ✔️ |
states.sensor.x.state | State object or error | ❌ |
state_attr('sensor.x', 'y') | Attribute or None | ✔️ |
Best practice: Always use helpers for entity/attribute access, and build/modify lists or dicts using only the safe Jinja2 tools described above.
Common Pitfalls & Best Practices
Watch out for these common mistakes when writing Jinja2 templates in Home Assistant:
- Incorrect syntax: Forgetting braces, brackets, or using the wrong delimiters (
{{vs{%). - Incorrect whitespace control: Extra spaces, blank lines, or failing to trim whitespace may cause unexpected formatting or errors.
- Comparing different data types: For example, comparing a string directly to a number will always fail:
{% if states('sensor.temp') > 20 %} ... {% endif %}
Here,states('sensor.temp')returns a string, so you should convert it:{% if states('sensor.temp') | float > 20 %} - Variable scope within loops: Variables created or changed inside a
forloop will not persist outside the loop. This is a quirk of Jinja2 and Home Assistant. - Note: This scope limitation does not apply to
ifblocks, but does to others such asforloops.
Example: Variable Scope in Loops (and how to do it right)
❌ The wrong way - will not work:
{% set total = 0 %}
{% for x in [1,2,3] %}
{% set total = total + x %}
{% endfor %}
Total: {{ total }} What you'll see:
Total: 0 (variable never updated outside the loop)
✔️ The correct way - using namespace:
{% set ns = namespace(total=0) %}
{% for x in [1,2,3] %}
{% set ns.total = ns.total + x %}
{% endfor %}
Total: {{ ns.total }} What you'll see:
Total: 6
- Best practice: When you need to accumulate or modify a value inside a loop and use it outside,
(e.g., ns = namespace(x=0)). - Debugging tip: Test your template logic in the Developer Tools - if something isn't working, check your data types and variable scopes!
Debugging Templates
Use the Home Assistant Template Developer Tool to test your templates quickly and safely. This helps identify issues before applying them to automations or scripts.
Wrapping Up
Jinja2 templates significantly enhance the flexibility and functionality of Home Assistant. Don't hesitate to experiment, and always validate your templates using the developer tools provided by Home Assistant.