Ansible Operators and Jinja2 Tests in when Conditions

Learn Ansible comparison and logical operators, Jinja2 tests such as is defined, and safe when expressions for strings, lists, dictionaries, facts, and registered task results.

Published

Updated

Read time 12 min read

Reviewed byDeepak Prasad

Ansible comparison and logical operators with Jinja2 tests in when conditions on Rocky Linux

when decides whether a task runs. The value is a Jinja2 expression built from comparison operators (==, >, in), logical operators (and, or, not), and tests (is defined, is mapping). Get the expression wrong and tasks skip silently—or fail on undefined variables.

This guide covers operators and tests for when conditions: strings, numbers, booleans, lists, dictionaries, registered results, and facts. It assumes variables, YAML syntax, and playbook structure. For failed_when and changed_when task syntax see conditionals; for loops see Ansible loop; for facts see custom facts; for result.rc patterns see register variables.

Tested on: Rocky Linux 10.2 (Red Quartz); kernel 6.12.0-211.16.1.el10_2.0.1.x86_64; ansible-core 2.16.16.

NOTE
This chapter is part of the GoLinuxCloud Ansible tutorial (RHCE EX294). Follow along from ~/ansible-project, inventory group lab, and playbooks in playbooks/. Use your own host names and paths if yours differ.

What are Operators and Tests in Ansible when Conditions?

Operators compare or combine values:

  • app_env == "production"
  • port >= 1024 and port <= 65535
  • "httpd" in packages

Tests evaluate one value and return true or false:

  • my_var is defined
  • app_config is mapping
  • enable_debug is boolean

Ansible uses standard Jinja2 tests plus Ansible-specific tests. Both appear inside when, failed_when, and changed_when expressions.


How when Conditions Work in Ansible

when is a task keyword. Its value is a Jinja2 expression without surrounding {{ }}:

yaml
- name: Production-only task
  ansible.builtin.debug:
    msg: "Production branch"
  when: app_env == "production"

Avoid this—when already expects a Jinja expression, and wrapping the whole condition in {{ }} can cause warnings or confusing parsing:

yaml
when: "{{ app_env == 'production' }}"

Indent when at the same level as the module name (debug, dnf, …). Full task conditional patterns live in conditionals—here we focus on the expression inside when.


Operators vs Jinja2 Tests

Kind Role Examples
Operators Compare or combine values ==, !=, in, and, or
Tests Ask a true/false question about one value is defined, is string, is mapping

Use operators for equality and logic. Use tests before accessing optional variables or checking value types.


Which Operator or Test Should You Use?

Need Use
Compare two values ==, !=, >, <
Match one of many values var in ["a", "b"]
Require multiple conditions List-form when or and
Allow either condition or
Check optional variable var is defined
Check dictionary before nested key parent is mapping and parent.key is defined
Check registered command success result.rc == 0

Comparison and Membership Operators in when Conditions

Operator Meaning
== Equal to
!= Not equal to
> Greater than
< Less than
>= Greater than or equal to
<= Less than or equal to
in Value exists in a string, list, or dictionary keys

Example playbook:

yaml
---
- name: Comparison operators demo
  hosts: localhost
  connection: local
  gather_facts: false
  vars:
    app_env: production
    port: 8080
  tasks:
    - name: Match environment
      ansible.builtin.debug:
        msg: "Production branch"
      when: app_env == "production"

    - name: Port in valid range
      ansible.builtin.debug:
        msg: "Port {{ port }} is in range"
      when: port >= 1024 and port <= 65535

Run it:

bash
ansible-playbook playbooks/operators-demo.yml

Sample output:

output
ok: [localhost] => {
    "msg": "Production branch"
}
ok: [localhost] => {
    "msg": "Port 8080 is in range"
}

Logical Operators in when Conditions

Use logical operators when one variable is not enough to decide whether a task should run. A single comparison such as app_env == "production" answers one question. Real playbooks often need two or more checks—OS family and a debug flag, or environment name in a small set of allowed values.

Keep the condition readable. If the expression grows past two operators, split it into separate tasks or use list-form when for AND logic.

Operator Runs the task when… Typical use
and Every part is true OS matches and feature flag is on
or At least one part is true Staging or development deploy
not The following test or comparison is false Maintenance mode is off

and

Both sides must be true. Ansible evaluates the full expression before deciding to run or skip:

yaml
when: ansible_os_family == "RedHat" and enable_debug

Use and when two independent facts must line up—for example Red Hat family packages and verbose logging enabled. If either side is false, the task is skipped.

or

At least one side must be true:

yaml
when: app_env == "staging" or app_env == "development"

Use or for a small set of named values. When the list grows, app_env in ["staging", "development"] is often easier to read than chaining or.

not

Negate a test or comparison:

yaml
when: not enable_maintenance
when: missing_var is not defined

not flips the result of what follows. when: not enable_maintenance runs when enable_maintenance is false or undefined. Pair not with default(false) when an optional flag may be missing:

yaml
when: not (maintenance_mode | default(false))

Combined example:

yaml
---
- name: Logical operators demo
  hosts: localhost
  connection: local
  gather_facts: true
  vars:
    enable_debug: true
    app_env: staging
  tasks:
    - name: Red Hat family with debug enabled
      ansible.builtin.debug:
        msg: "Run verbose tasks on {{ ansible_os_family }}"
      when: ansible_os_family == "RedHat" and enable_debug

    - name: Staging or development deploy path
      ansible.builtin.debug:
        msg: "Non-production deploy checks"
      when: app_env == "staging" or app_env == "development"

    - name: Skip maintenance window tasks
      ansible.builtin.debug:
        msg: "Normal operations"
      when: not (maintenance_mode | default(false))
bash
ansible-playbook playbooks/logical-operators-demo.yml

Sample output:

output
ok: [localhost] => {
    "msg": "Run verbose tasks on RedHat"
}
ok: [localhost] => {
    "msg": "Non-production deploy checks"
}
ok: [localhost] => {
    "msg": "Normal operations"
}

The first task needs both ansible_os_family == "RedHat" and enable_debug. The second runs because app_env is staging. The third runs because maintenance_mode was never set and defaults to false inside not (...).


Check if a Variable is Defined

Optional variables come from inventory, group_vars, extra vars (-e), or prior tasks. Before you read backup_path or app_config.cache, confirm the name exists—otherwise Ansible errors instead of skipping the task.

is defined and is not defined answer whether Ansible knows the variable name. They do not tell you whether the value is empty: optional_var: "" is still defined.

is defined

Run a task only when the caller supplied a value:

yaml
when: optional_var is defined

Use this for optional overrides—custom backup_path, feature flags, or keys that appear only on some hosts.

is not defined

Skip configuration that depends on a missing key:

yaml
when: missing_var is not defined
when: missing_var is undefined

is not defined and is undefined are equivalent for optional keys.

default filter for safe fallback values

In when, test definition first—you are deciding whether to run a task. In templates and module arguments where you need a display value, use default:

yaml
msg: "Cache={{ app_config.cache | default('disabled') }}"

default supplies a fallback when the variable or key is undefined. It does not replace is defined in when when the goal is to skip the whole task.

Example playbook:

yaml
---
- name: Defined checks demo
  hosts: localhost
  connection: local
  gather_facts: false
  tasks:
    - name: optional_flag is defined
      ansible.builtin.debug:
        msg: "Caller set optional_flag"
      when: optional_flag is defined

    - name: backup_path is not defined
      ansible.builtin.debug:
        msg: "Use default backup location"
      when: backup_path is not defined
bash
ansible-playbook playbooks/defined-checks-demo.yml

Sample output:

output
skipping: [localhost]

ok: [localhost] => {
    "msg": "Use default backup location"
}

optional_flag was never set, so the first task is skipped. backup_path is not defined either, so the second task runs and applies your default path logic.


Check Strings in when Conditions

Use in for simple substring checks. Use match or search only when you need a regular expression. match checks from the beginning of the string, while search can match anywhere in the value.

Compare string values

Quote string literals:

yaml
when: app_env == "production"
when: ansible_os_family == 'RedHat'

Check string contains text

yaml
when: "'ssl' in cert_path"
when: cert_path is search('\\.pem$')

Check string starts or ends with text

yaml
when: hostname is match('^web')
when: log_path is search('/var/log')

Check empty strings

An empty string is defined but falsy in Jinja:

yaml
when: optional_var | length > 0
when: optional_var != ""

Check Numbers in when Conditions

Compare numbers without quotes:

yaml
when: port == 8080
when: memory_mb >= 2048

Avoid comparing numbers as strings—port == "8080" fails when port is an integer. Use filters if types might differ: when: port | int == 8080.


Check Booleans in when Conditions

Use real booleans in YAML when possible:

yaml
vars:
  enable_debug: true
yaml
when: enable_debug
when: not enable_debug

String "true" and "false" are not booleans—when: debug == "true" is a string comparison, not a YAML boolean.

This matters when values come from inventory, extra vars, or external files. If a value may arrive as a string, normalize it before relying on boolean logic:

yaml
when: enable_debug | bool

The bool filter has its own rules—use it only when you know the input shape. See the conditionals guide for task-level patterns.

Test Use
is boolean Value is a real boolean
is true / is false Strict boolean checks

Check Lists in when Conditions

Check if item exists in a list

yaml
when: "'httpd' in packages"
when: item in allowed_packages

Check list length

yaml
when: packages | length > 0

Check empty lists

yaml
when: packages | length == 0

Example:

yaml
vars:
  packages:
    - httpd
    - mod_ssl
tasks:
  - name: httpd is listed
    ansible.builtin.debug:
      msg: "httpd is in package list"
    when: "'httpd' in packages"

Check Dictionaries in when Conditions

Dictionary checks are safest when you verify the parent object before reading a child key. Otherwise Ansible may fail before it gets a chance to skip the task.

Check if key exists

yaml
when: app_config is mapping
when: "'region' in app_config"

Check nested dictionary values

yaml
when: app_config.region == "us-east"

Avoid undefined nested variable errors

If the parent might be missing, test the parent first:

yaml
when: app_config is mapping and app_config.region is defined

Not safe when app_config itself is undefined:

yaml
when: app_config.region == "us-east"

Use Multiple when Conditions

YAML list form uses AND logic. The task runs only when every condition is true:

yaml
when:
  - app_env == "production"
  - port == 443

This is the same as:

yaml
when: app_env == "production" and port == 443

For OR logic, use or in one expression:

yaml
when: app_env == "production" or app_env == "staging"

For membership-style OR checks, use in:

yaml
when: app_env in ["production", "staging"]

Use list form for readable AND conditions. Use or, in, or parentheses when one of several values should match.


Use Parentheses for Complex Conditions

Without parentheses, mixed and / or expressions can be read incorrectly during review, even when Jinja evaluates them consistently.

Group logic with parentheses:

yaml
when: (app_env == "production" or app_env == "staging") and port >= 1024

Parentheses clarify precedence when mixing and and or.


Use when with Registered Variables

After register: probe, use fields from the result hash. Details: register variables.

Check rc

yaml
when: probe.rc == 0

Check stdout

yaml
when: probe.stdout != ""
when: "'READY' in probe.stdout"

Check failed or changed status

yaml
when: not probe.failed
when: probe.changed

Example:

yaml
- name: Probe file
  ansible.builtin.command: test -f /etc/hosts
  register: probe
  changed_when: false
  failed_when: false

- name: Continue when hosts file exists
  ansible.builtin.debug:
    msg: "rc={{ probe.rc }}, changed={{ probe.changed }}"
  when: probe.rc == 0 and not probe.failed

Sample output:

output
ok: [localhost] => {
    "msg": "rc=0, changed=False"
}

Use when with Facts

Facts are variables after gathering—use them like any other value:

yaml
when: ansible_os_family == "RedHat"
when: ansible_distribution_major_version | int >= 9
when: ansible_virtualization_role == "guest"

Do not re-teach fact gathering here—see facts and custom facts. With gather_facts: false, fact names are undefined unless you call setup.


Use when with Loops

when on a looping task is evaluated per item:

yaml
- name: Configure web packages only
  ansible.builtin.debug:
    msg: "Would configure {{ item }}"
  loop: "{{ packages }}"
  when: item in ['httpd', 'nginx']

Sample output:

output
ok: [localhost] => (item=httpd) => { "msg": "Would configure httpd" }
ok: [localhost] => (item=nginx) => { "msg": "Would configure nginx" }
skipping: [localhost] => (item=php)

Loop mechanics (loop_control, until) belong in the loop guide.


Safe Conditional Expressions

Technique Why
var is defined before use Avoid undefined variable errors
default('fallback') Safe display when a key may be missing
parent is mapping and parent.key is defined Safe nested access
Parentheses Clear mixed and / or logic
Short expressions Easier review in Git and exams

Common tests:

Test Use
is defined Variable exists
is not defined / is undefined Variable does not exist
is string Value is a string
is number Value is numeric
is boolean Value is boolean
is iterable Value can be iterated; not necessarily a list
is mapping Value behaves like a dictionary

Common when Condition Mistakes

Mistake Symptom Fix
when: "{{ expr }}" Parsing errors or always true string Drop outer {{ }}
Compare number to string Condition never matches Use port == 8080 or port | int
Access nested key on undefined parent Task fails before skip Test parent is mapping first
"true" string vs boolean Wrong branch Use YAML true / false
One 120-character when Hard to debug Split tasks or use parentheses
Use facts with gather_facts: false Undefined fact Call setup or enable gathering
when on wrong indent YAML error Align with module name

In a small Ansible project:

  1. Write when as a plain Jinja expression—no {{ }}.
  2. Check is defined before optional inventory or registered keys.
  3. Use and / or sparingly; split tasks when readability suffers.
  4. Branch on ansible_os_family and version facts instead of hard-coding host names.
  5. Leave failed_when / changed_when overrides to the conditionals guide when task status needs customization.

References


Summary

Ansible when conditions are Jinja2 expressions: comparison operators (==, in, …), logical operators (and, or, not), and tests (is defined, is mapping). Do not wrap the whole expression in {{ }}. Check types and defined state before nested access; use default for fallbacks in values. Combine with registered results, facts, and loop item filters using the same rules. Keep expressions short and readable.


Frequently Asked Questions

1. Do I use {{ }} inside a when condition?

No. when is already a Jinja2 expression. Write app_env == "production", not {{ app_env == "production" }}.

2. What is the difference between an operator and a test in Ansible?

Operators compare values—==, and, in. Tests evaluate a value and return true or false—is defined, is string, is mapping.

3. How do I check if a variable exists before using it?

Use my_var is defined or my_var is not defined in when. Combine with the default filter in templates when you need a fallback value.

4. How do I test membership in a list?

Use when: "httpd" in packages or when: item in allowed_packages inside a loop.

5. Can I use registered task output in when?

Yes. After register: probe, use when: probe.rc == 0, when: probe.stdout != "", or when: not probe.failed.

6. Where do failed_when and changed_when fit?

They use the same Jinja2 expression rules as when. See the conditionals guide for task status overrides—this article focuses on expression syntax.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …