Introduction

The ternary filter is Ansible's one-line if-else — choose between two values based on a condition without verbose when blocks or multi-line Jinja2 {% if %} statements. But many users hit the template error: unexpected character error because they use JavaScript-style syntax. Here's the correct way.

The Wrong Way (JavaScript-style)

{# WRONG — Jinja2 doesn't support this syntax #}
{{ env == 'dev' ? 'localhost' : 'db.prod.internal' }}

Error:

template error while templating string: unexpected character

The Correct Way: Ternary Filter

db_host: "{{ (env == 'dev') | ternary('localhost', 'db.prod.internal') }}"

Syntax

{{ (condition) | ternary(true_value, false_value) }}

Three rules:

  1. Wrap the condition in parentheses
  2. Use the pipe | before ternary
  3. ternary is a filter, not a function

Ternary vs Inline If-Else

Jinja2 also supports inline if-else:

# Ternary filter
db_host: "{{ (env == 'dev') | ternary('localhost', 'db.prod.internal') }}"

# Jinja2 inline if-else (also valid)
db_host: "{{ 'localhost' if env == 'dev' else 'db.prod.internal' }}"

Both are equivalent. The ternary filter is better when chaining with other filters:

# Chain-friendly
service: "{{ use_ssl | bool | ternary('https', 'http') }}"

# Inline if-else equivalent (more verbose)
service: "{{ 'https' if use_ssl | bool else 'http' }}"

Ternary with Three Values (Null/Undefined)

The ternary filter accepts an optional third argument for None/undefined:

# Returns 'default_mode' if debug is None or undefined
mode: "{{ debug | ternary('verbose', 'quiet', 'default_mode') }}"
debug valueResult
trueverbose
falsequiet
None / undefineddefault_mode

Common Patterns

Boolean Variables

# Always use | bool for variables that might be strings
ssl_redirect: "{{ enable_ssl | bool | ternary('on', 'off') }}"

Default + Ternary (Undefined-Safe)

# Handle undefined variables
log_level: "{{ (verbose | default(false)) | bool | ternary('debug', 'info') }}"

Nested Ternary

# Multi-level selection
env_name: "{{ (is_prod | bool) | ternary('production', (is_staging | bool) | ternary('staging', 'development')) }}"

Select from Mapping (Alternative to Nested Ternary)

# Cleaner than deeply nested ternary
env_name: "{{ {'prod': 'production', 'staging': 'staging', 'dev': 'development'}[env] | default('development') }}"

In Templates (.j2 files)

server {
    listen {{ (enable_ssl | bool) | ternary('443 ssl', '80') }};
    server_name {{ domain }};
    root {{ (env == 'dev') | ternary('/var/www/dev', '/var/www/html') }};
}

With set_fact

- name: Set database connection
  ansible.builtin.set_fact:
    db_host: "{{ (env == 'production') | ternary('db.prod.internal', 'localhost') }}"
    db_port: "{{ (use_ssl | default(false) | bool) | ternary(5433, 5432) }}"
    db_pool: "{{ (env == 'production') | ternary(20, 5) }}"

In Task Parameters

- name: Configure service
  ansible.builtin.systemd:
    name: myapp
    state: "{{ (maintenance_mode | default(false) | bool) | ternary('stopped', 'started') }}"
    enabled: "{{ (env == 'production') | ternary(true, false) }}"

Common Mistakes

MistakeFix
env == 'dev' ? 'a' : 'b'(env == 'dev') | ternary('a', 'b')
Missing parentheses around conditionAlways wrap: (condition) | ternary(...)
Missing pipe | before ternaryternary is a filter: | ternary(...)
Unquoted Jinja2 expressionAlways quote: "{{ ... }}"
Comparing bool to string 'true'Use | bool filter first

Debugging

- name: Debug ternary expression
  ansible.builtin.debug:
    msg: |
      Condition value: {{ env }}
      Condition type: {{ env | type_debug }}
      Result: {{ (env == 'dev') | ternary('localhost', 'db.prod.internal') }}

Conclusion

The ternary filter syntax is {{ (condition) | ternary(true_val, false_val) }}. Always use parentheses around the condition, pipe | before ternary, and | bool when working with variables that might be strings. For undefined-safe expressions, chain | default(false) | bool | ternary(...). Never use JavaScript-style ? : syntax — Jinja2 doesn't support it.