AnsibleX · YAML for Reliable AutomationYAML for reliable automation
Quoting, and the double-brace rule
What you'll learn
- Explain why an unquoted value starting with a template is a YAML parse error
- Choose between single and double quotes for a value containing a template
- Distinguish what the YAML parser sees from what Jinja receives
- Write when: conditions without templating delimiters
- Recognise when a template must be quoted and when it need not be
Prerequisites
Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11
This is the error every Ansible user meets in their first week, and it is worth understanding rather than memorising, because the understanding transfers to several other situations and the memorised rule does not.
Why the parser objects
In YAML, { at the start of a value opens a flow mapping — the
inline {key: value} form. So when you write:
- name: Print the path
ansible.builtin.debug:
msg: {{ app_root }}/current…the parser sees a value starting with {, decides it is looking at a
flow mapping, and then fails, because {{ app_root }}/current is not a
valid one. Jinja is never consulted; the file never gets that far.
$ ansible-playbook --syntax-check -i localhost, jinja-unquoted.yml[ERROR]: YAML parsing failed: This may be an issue with missing quotes around a template block.
Origin: /home/ops/estate/jinja-unquoted.yml:10:14
8 - name: Print the path
9 ansible.builtin.debug:
10 msg: {{ app_root }}/current
^ column 14
For example:
raw: {{ some_var }}
Should be:
raw: "{{ some_var }}"The fix is quotes, and the rule is narrower than most people state it:
A value must be quoted when the template appears at the start of the value.
msg: The path is {{ app_root }}/current parses fine unquoted, because
the value starts with T. That is why the rule is often learned as
“sometimes you need quotes” — the failure depends on position, not on
the presence of a template.
Which quote style
Both single and double quotes produce a string. They differ in what they do with backslashes and with the other quote character.
| Style | Escapes | Use when |
|---|---|---|
'single' | Only '' for a literal '. Backslashes are literal. | The default. Especially for anything containing \ — Windows paths, regular expressions. |
"double" | Full escape processing: \n, \t, \\, \". | You want an escape sequence interpreted, or the value contains a single quote. |
The practical decision is usually made for you by the template’s own contents, because Jinja string literals need quotes too:
# Jinja needs single quotes inside, so the YAML string uses double.
- name: Report the environment
ansible.builtin.debug:
msg: "{{ app_env | default('staging') }}"
# Jinja needs double quotes inside, so the YAML string uses single.
- name: Split on a quoted delimiter
ansible.builtin.debug:
msg: '{{ csv_line.split(",") | first }}'
# A regular expression: single-quoted YAML keeps the backslashes literal,
# so \. and \1 arrive at the filter exactly as written.
- name: Extract the major version
ansible.builtin.set_fact:
major: '{{ app_version | regex_replace("^([0-9]+)\..*$", "\1") }}'Executed against 2.21.3 with app_version: '1.10.4', that set_fact
produces major: "1".
What the parser sees, and what Jinja receives
{{ var }} and "{{ var }}" are different to the YAML parser — one is
a parse error and one is a string. To Jinja they are identical, because
by the time Jinja is involved the quotes have already been consumed by
the parser and the value is the six-plus characters {{ var }}.
That distinction explains something people find inconsistent: whether a templated value keeps its type.
$ ansible-playbook -i localhost, looptype.ymlTASK [Report the type that survived] *******************************************
ok: [localhost] => {
"msg": "templated_mode=420 type=int"
}
TASK [Report that type too] ****************************************************
ok: [localhost] => {
"msg": "concat_mode=mode-420 type=str"
}So "{{ port }}" where port is the integer 8080 yields the integer
8080, while "port-{{ port }}" yields the string port-8080. The
quotes are the YAML parser’s business; the type is decided by whether
the template is the whole value.
The when: exception
when takes a Jinja expression without delimiters. Not a string
containing a template — the raw expression.
- name: Restart only where it is needed
ansible.builtin.systemd_service:
name: app
state: restarted
when: app_state == 'present' and replicas | int > 1
- name: Skip hosts that opted out
ansible.builtin.debug:
msg: 'running'
when:
- not maintenance_exempt | default(false)
- ansible_facts['os_family'] == 'Debian'A list under when: is an implicit and of every element, which is
almost always more readable than one long expression joined with and.
Wrapping the expression in braces still works, and produces a deprecation warning:
$ ansible-playbook -i localhost, whenraw.ymlTASK [Braces in when, quoted] **************************************************
[DEPRECATION WARNING]: Conditionals should not be surrounded by templating delimiters
such as {{ }} or {% %}. This feature will be removed from ansible-core version 2.23.
Origin: /home/ops/estate/whenraw.yml:17:13
15 ansible.builtin.debug:
16 msg: 'braced expression ran'
17 when: "{{ replicas > 1 }}"
^ column 13
ok: [localhost]That is a dated removal, not a style note. A repository full of
when: "{{ ... }}" has a migration ahead of it, and the fix is
mechanical: delete the braces and the quotes.
The same rule applies to the other keywords that take expressions
directly — changed_when, failed_when, until, and assert’s
that: list. All of them are raw Jinja.
Knowledge check
Knowledge check · 4 questions
Q1. Why does msg: {{ app_root }}/current fail to parse, while msg: The path is {{ app_root }}/current does not?
Q2. Which keywords take a raw Jinja expression with no templating delimiters? Select all that apply.
Q3. Because "{{ port }}" is quoted in the YAML, the value reaching the module is always a string.
Q4. A regex_replace pattern containing backslash escapes is written inside a double-quoted YAML scalar and matches nothing. What is the likely cause?
Passing score: 75%. Answers are checked in this browser.