Introduction

The ternary filter is Ansible's inline if-else — it returns one value when a condition is true and another when false. It's cleaner than writing full {% if %} blocks when you just need to pick between two values.

{{ condition | ternary('value_if_true', 'value_if_false') }}

Basic Syntax

# Two arguments: true_value, false_value
{{ my_bool | ternary('yes', 'no') }}

# Three arguments: true_value, false_value, null_value
{{ my_var | ternary('defined_true', 'defined_false', 'undefined') }}

Simple Examples

---
- name: Ternary filter examples
  hosts: localhost
  gather_facts: true
  vars:
    debug_mode: true
    env: production
    max_workers: 0
    app_port: 8080

  tasks:
    - name: Set log level based on debug flag
      ansible.builtin.debug:
        msg: "Log level: {{ debug_mode | ternary('DEBUG', 'INFO') }}"
      # Output: "Log level: DEBUG"

    - name: Choose config based on environment
      ansible.builtin.debug:
        msg: "SSL: {{ (env == 'production') | ternary('enabled', 'disabled') }}"
      # Output: "SSL: enabled"

    - name: Default workers based on value
      ansible.builtin.debug:
        msg: "Workers: {{ (max_workers > 0) | ternary(max_workers, ansible_processor_vcpus) }}"

    - name: Set firewall rule
      ansible.builtin.debug:
        msg: "Port {{ app_port }} {{ (app_port == 80) | ternary('is standard HTTP', 'is non-standard') }}"

Ternary in Templates

---
- name: Generate config from template
  hosts: webservers
  vars:
    ssl_enabled: true
    workers: 4
    env: production

  tasks:
    - name: Deploy nginx config
      ansible.builtin.template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf

nginx.conf.j2:

server {
    listen {{ ssl_enabled | ternary('443 ssl', '80') }};
    server_name {{ inventory_hostname }};

    {{ ssl_enabled | ternary('ssl_certificate /etc/ssl/cert.pem;', '# SSL disabled') }}
    {{ ssl_enabled | ternary('ssl_certificate_key /etc/ssl/key.pem;', '') }}

    worker_connections {{ (env == 'production') | ternary(1024, 256) }};

    access_log {{ (env == 'production') | ternary('/var/log/nginx/access.log combined', 'off') }};
}

Ternary with Variables

---
- name: Dynamic variable assignment
  hosts: all
  vars:
    use_custom_port: false
    custom_port: 9090
    default_port: 8080

  tasks:
    - name: Set application port
      ansible.builtin.set_fact:
        app_port: "{{ use_custom_port | ternary(custom_port, default_port) }}"

    - name: Configure based on OS family
      ansible.builtin.set_fact:
        pkg_manager: "{{ (ansible_os_family == 'RedHat') | ternary('dnf', 'apt') }}"
        service_name: "{{ (ansible_os_family == 'RedHat') | ternary('httpd', 'apache2') }}"
        config_path: "{{ (ansible_os_family == 'RedHat') | ternary('/etc/httpd', '/etc/apache2') }}"

Three-Argument Ternary (Null Handling)

The third argument handles None/undefined values:

---
- name: Handle undefined variables
  hosts: localhost
  vars:
    defined_true: true
    defined_false: false
    # undefined_var is not defined

  tasks:
    - name: Two args — true
      ansible.builtin.debug:
        msg: "{{ defined_true | ternary('YES', 'NO') }}"
      # Output: "YES"

    - name: Two args — false
      ansible.builtin.debug:
        msg: "{{ defined_false | ternary('YES', 'NO') }}"
      # Output: "NO"

    - name: Three args — null/undefined
      ansible.builtin.debug:
        msg: "{{ undefined_var | default(None) | ternary('TRUE', 'FALSE', 'UNKNOWN') }}"
      # Output: "UNKNOWN"

Nested Ternary

Chain ternary filters for multiple conditions (use sparingly — readability drops fast):

- name: Nested ternary (like elif)
  ansible.builtin.set_fact:
    priority: >-
      {{ (severity == 'critical') | ternary('P1',
         (severity == 'high') | ternary('P2',
         (severity == 'medium') | ternary('P3', 'P4'))) }}

For more than 2-3 conditions, use a dictionary lookup instead:

# Cleaner alternative to nested ternary
- name: Dictionary lookup (better for 3+ conditions)
  ansible.builtin.set_fact:
    priority: "{{ {'critical': 'P1', 'high': 'P2', 'medium': 'P3'}.get(severity, 'P4') }}"

Common Use Cases

Package Installation

- name: Install package for OS family
  ansible.builtin.package:
    name: "{{ (ansible_os_family == 'RedHat') | ternary('httpd', 'apache2') }}"
    state: present

Conditional Service State

- name: Enable service only in production
  ansible.builtin.service:
    name: monitoring-agent
    enabled: "{{ (env == 'production') | ternary(true, false) }}"
    state: "{{ (env == 'production') | ternary('started', 'stopped') }}"

Dynamic Paths

- name: Use versioned or latest path
  ansible.builtin.set_fact:
    app_path: "{{ (pin_version is defined) | ternary('/opt/app-' + (pin_version | default('')), '/opt/app-latest') }}"

Boolean to String

# Convert bool to yes/no for config files
- name: Generate config line
  ansible.builtin.lineinfile:
    path: /etc/myapp.conf
    regexp: "^debug="
    line: "debug={{ debug_mode | ternary('yes', 'no') }}"

Ternary vs Jinja2 if-else

# Ternary — concise, one-liners
msg: "{{ is_prod | ternary('WARN', 'DEBUG') }}"

# Jinja2 inline if — also works, slightly different syntax  
msg: "{{ 'WARN' if is_prod else 'DEBUG' }}"

# Jinja2 block — for complex logic
msg: >-
  {% if env == 'prod' %}WARN{% elif env == 'staging' %}INFO{% else %}DEBUG{% endif %}
ApproachBest For
ternarySimple true/false with clear values
Jinja2 inline ifWhen the condition reads more naturally
Jinja2 {% if %} blockComplex multi-branch logic

Troubleshooting

"ternary expects 2 or 3 arguments"

You passed the wrong number of arguments:

# Wrong — 1 argument
{{ flag | ternary('yes') }}

# Right — 2 or 3 arguments
{{ flag | ternary('yes', 'no') }}
{{ flag | ternary('yes', 'no', 'unknown') }}

Unexpected Result with Strings

Non-empty strings are truthy in Jinja2:

# "false" (string) is TRUTHY — returns 'yes'!
{{ "false" | ternary('yes', 'no') }}  # "yes"

# Convert to bool first
{{ "false" | bool | ternary('yes', 'no') }}  # "no"

Integer 0 is Falsy

{{ 0 | ternary('nonzero', 'zero') }}  # "zero"
{{ 1 | ternary('nonzero', 'zero') }}  # "nonzero"

Conclusion

The ternary filter is a clean one-liner for picking between two values based on a condition. Use it for config toggles, OS-specific values, and boolean-to-string conversions. For None/undefined handling, use the three-argument form. Avoid nesting more than two deep — switch to a dictionary lookup or {% if %} block instead. Remember: non-empty strings are truthy, and "false" needs | bool before ternary.