
This article may contain affiliate links. If you buy through these links, the price is the same for you and the store pays me a small commission that helps keep Tecnoyfoto running.
Until now, all our values have been fixed. An action always controlled the same light, and a notification always displayed the same message. A home is not static, however: temperature changes, someone may or may not be home, and different entities may start different automation runs.
Home Assistant uses templates to calculate values from the current situation. Its template engine is based on Jinja. A template may be written inside a YAML value, but Jinja and YAML remain separate layers.
Understanding that separation is the central goal of this chapter. We will identify where YAML structure ends and a dynamic expression begins. Then we will read states and attributes, test results in the template editor, and create a message that describes the virtual home.
The problem that a fixed value cannot solve
This action always creates the same message:
action: persistent_notification.create
data:
title: "YAML Course - Living-room state"
message: "The room temperature is 21 degrees."
YAML structures an action with a data map. Home Assistant interprets persistent_notification.create and receives two strings. Nothing tells it to query the sensor, so the message keeps saying 21 even when the temperature changes.
We can insert the current state with a template:
action: persistent_notification.create
data:
title: "YAML Course - Living-room state"
message: >-
The living-room temperature is
{{ states('sensor.course_living_room_temperature') }} °C.
message is still a YAML key, and >- is still YAML syntax for opening a folded multiline string. Inside that string, {{ ... }} delimits a Jinja expression that Home Assistant evaluates before running the action.
We can describe the responsibilities:
YAML: `message` contains a text block.
Jinja: calculates the text to insert between the delimiters.
Home Assistant: provides the `states()` function and runs the action.
The three delimiters you need to recognize
Jinja uses several delimiters. For this level we need three.
{{ ... }} calculates and displays a result
{{ states('sensor.course_living_room_temperature') }}
Home Assistant reads the state and places the result where the expression appears. If the sensor’s state is 22.5, the output is:
22.5
Think of the double braces as “calculate this and insert it here.”
{% ... %} controls logic
{% if is_state('binary_sensor.course_someone_home', 'on') %}
Someone is home.
{% else %}
Nobody is home.
{% endif %}
The {% ... %} blocks do not print the condition. They control which part of the text is generated. if opens the decision, else provides the alternate path, and endif closes it.
{# ... #} adds a Jinja comment
{# This comment does not appear in the output #}
Do not confuse it with a YAML comment:
# This comment belongs to YAML
Neither appears in the final output, but each is processed by a different layer and uses different delimiters.
Template editor: test before automating
Open Settings → Tools → Template. This editor evaluates a template immediately and displays either the result or an error.
Start with:
The temperature is {{ states('sensor.course_living_room_temperature') }} °C.
Change the practice environment’s temperature control and watch the output update.
Then try:
{{ is_state('binary_sensor.course_someone_home', 'on') }}
The result is True or False. Toggle the presence control to verify both results.
The editor allows you to separate two questions:
- Does the template generate the expected value?
- Does the action correctly use that value?
If a message fails inside an automation, first copy its template into the editor. If it works there, inspect the context and YAML structure where the template was placed.
Some variables only exist within an automation or script, such as trigger or fields received by a script. The editor cannot automatically invent that context. We will learn to recognize it when we use those variables.
Read a state with states()
The recommended way to read the state of a particular entity is:
{{ states('sensor.course_living_room_temperature') }}
The entity_id is passed as a string inside the parentheses. Its single quotes belong to the Jinja expression. When the entire template is inside a double-quoted YAML string, the combination remains easy to read:
message: "Temperature: {{ states('sensor.course_living_room_temperature') }} °C"
We could reverse the quote styles, but then the inner quotes would need to be escaped or rearranged. Double quotes outside and single quotes inside avoid unnecessary escaping.
states() returns the state as a string. Even if it displays 22.5, do not assume that the value is already numeric. The distinction matters when we compare values or perform arithmetic.
Avoid fragile direct access
In older examples, you may find:
{{ states.sensor.course_living_room_temperature.state }}
Direct access can fail if the entity is unavailable when the template is evaluated. The states('entity_id') function returns a controlled result, so that is the form we will use throughout the course.
Check a state with is_state()
To find out if an entity is in a specific state:
{{ is_state('binary_sensor.course_someone_home', 'on') }}
The function returns a boolean. Read it as a question:
Is the state of
binary_sensor.course_someone_homeon?
We can use it in an if block:
{% if is_state('binary_sensor.course_someone_home', 'on') %}
Someone is home.
{% else %}
The house is empty.
{% endif %}
We don’t need to write:
{% if is_state('binary_sensor.course_someone_home', 'on') == true %}
is_state() already returns a boolean. Comparing it with true again only adds noise.
Read an attribute with state_attr()
In Chapter 3, we saw that the temperature unit is an attribute. We can read it with:
{{ state_attr('sensor.course_living_room_temperature', 'unit_of_measurement') }}
The first argument is the entity and the second, the name of the attribute.
This lets us avoid hard-coding the unit:
The temperature is
{{ states('sensor.course_living_room_temperature') }}
{{ state_attr('sensor.course_living_room_temperature', 'unit_of_measurement') }}.
If the attribute does not exist, state_attr() returns none. Before using an attribute from an online example, check in States that your entity actually provides it.
Multiline text and block scalars
A short template fits in a string:
message: "Temperature: {{ states('sensor.course_living_room_temperature') }} °C"
Once a template contains decisions, a block scalar is easier to read:
message: >-
{% if is_state('binary_sensor.course_someone_home', 'on') %}
Someone is home.
{% else %}
The house is empty.
{% endif %}
YAML controls the outer indentation. All content must belong to message. Jinja controls the inner decision.
If {% else %} moves outside the YAML block, the problem is not that else is misspelled. The outer structure no longer treats that line as part of the message value.
In long templates, aligning if, else, and endif makes the logic easier to see, even though Jinja does not use indentation in the same way YAML does.
Convert text to number
To perform arithmetic, convert the state first:
{{ states('sensor.course_living_room_temperature') | float(0) }}
The | symbol applies a filter. float(0) attempts to produce a floating-point number and falls back to 0 if conversion fails.
We can add two degrees:
{{ states('sensor.course_living_room_temperature') | float(0) + 2 }}
The parentheses can make a complex expression clearer:
{{ (states('sensor.course_living_room_temperature') | float(0)) + 2 }}
The fallback value 0 prevents a technical error, but it may produce the wrong household decision. If the sensor is unavailable, should we treat that as zero degrees or report that no measurement is available? In the next chapter, we will separate safe conversion from correct meaning.
A complete dynamic message
Let’s build a template that combines presence and temperature:
{% if is_state('binary_sensor.course_someone_home', 'on') %}
There's someone at home. The living-room temperature is
{{ states('sensor.course_living_room_temperature') }}
{{ state_attr('sensor.course_living_room_temperature', 'unit_of_measurement') }}.
{% else %}
Nobody is home.
{% endif %}
Test it in the editor with presence on and off. Change the temperature as well.
Then insert it into an action:
action: persistent_notification.create
data:
title: "YAML Course - State of the House"
message: >-
{% if is_state('binary_sensor.course_someone_home', 'on') %}
There's someone at home. The living-room temperature is
{{ states('sensor.course_living_room_temperature') }}
{{ state_attr('sensor.course_living_room_temperature', 'unit_of_measurement') }}.
{% else %}
Nobody is home.
{% endif %}
Run the action from Tools → Actions. Home Assistant first renders the template to produce a string, then passes that string to the persistent-notification action.
Back to script fields
In the previous chapter, we introduced fields without using them. Now we can see how a received value becomes part of an action.
course_create_message:
alias: "YAML Course - Create Message"
fields:
message_text:
name: "Message"
description: "Text the notification will display."
required: true
selector:
text:
sequence:
- action: persistent_notification.create
data:
title: "YAML Course"
message: "{{ message_text }}"
mode: queued
The layers are clear:
- YAML defines
fields,sequenceanddata. - Home Assistant receives a field called
message_textand exposes it as a variable. - Jinja inserts the value using
{{ message_text }}.
The direct call provides the data:
action: script.course_create_message
data:
message_text: "The door has opened."
The field name must match exactly. If the call provides message while the template expects message_text, the expected variable does not exist.
Deliberate error: delimiters outside a YAML value
This block is poorly structured:
message:
{{ states('sensor.course_living_room_temperature') }}
The expression is not indented under message. YAML tries to interpret it as part of its own structure and may report an error before Jinja is ever evaluated.
One correct way is:
message: "{{ states('sensor.course_living_room_temperature') }}"
Or, in block:
message: >-
{{ states('sensor.course_living_room_temperature') }}
When a template fails, ask first which layer is reporting:
- Can YAML build the document?
- Does Home Assistant support a template in that field?
- Can Jinja evaluate the expression?
- Does the expression run but return the wrong value?
Exercise: describe the house with a template
Create a persistent notification that meets these requirements:
- If someone is home, show the temperature and its unit.
- If there is no one, show
The house is empty. - Do not manually write the temperature value.
- Test the template in the editor first.
- Then run the complete action.
As an extension, use another if block to add an opening sentence that says whether the door is open or closed.
See the explained solution
action: persistent_notification.create
data:
title: "YAML Course - State of the House"
message: >-
{% if is_state('binary_sensor.course_front_door', 'on') %}
The door's open.
{% else %}
The door is closed.
{% endif %}
{% if is_state('binary_sensor.course_someone_home', 'on') %}
There's someone at home. The living-room temperature is
{{ states('sensor.course_living_room_temperature') }}
{{ state_attr('sensor.course_living_room_temperature', 'unit_of_measurement') }}.
{% else %}
The house is empty.
{% endif %}
YAML treats everything under message: >- as a text block. The two Jinja blocks make independent decisions. states() gets the main value and state_attr() its unit.
The template performs no arithmetic, so we do not need to convert the temperature yet.
What should you take from this chapter?
YAML structures the data. Jinja generates dynamic values within certain fields. Home Assistant provides states, functions and context, renders the template and executes the resulting configuration.
{{ ... }} inserts a result, {% ... %} controls logic and {# ... #} creates comments from Jinja. states(), is_state() and state_attr() allow reading the house without using fragile shortcuts.
The template editor provides the fastest feedback loop. Use it before embedding an expression in a large automation.
In the next chapter, we will work with variables and data that may be unavailable. We will learn not to confuse “the template does not raise an error” with “the template makes the correct decision when the sensor state is unknown or unavailable.”

