YAML for Ansible
Every playbook, inventory, and variable file is YAML, and the handful of YAML rules that bite Ansible users are not the ones the spec authors warn about. They are indentation, implicit type coercion, and the quoting decisions Ansible inherits from PyYAML. A file that fails to parse stops the run cleanly, but the worse case is a file that parses into the wrong types and runs anyway.
A playbook that breaks with a confusing error is almost always a YAML problem wearing an Ansible costume. Learn the four rules below and most of those errors stop being mysteries: you read the message, recognize the YAML cause, and fix it in the right place rather than three lines down from where the parser pointed.
Indentation Is the Syntax
YAML expresses nesting through indentation — two spaces per level, never tabs — and there are no braces or brackets to fall back on when the indentation is wrong. A task indented one space too far quietly becomes a child of the wrong key, and the structure the parser builds is not the structure you meant. Because the document is still technically valid YAML, the error often surfaces several lines later as "a task is not a dictionary," pointing at the symptom rather than the cause.
This is why the first thing to check on any baffling parse error is alignment. Open the file in an editor that renders whitespace, confirm every sibling key starts in the same column, and confirm not a single tab slipped in. The fix is almost never where the error message points; it is wherever two lines that should be siblings are not in the same column.
Lists vs Dictionaries
YAML has two container shapes, and Ansible's structure is built by nesting them. A leading - makes a list item; a plain key: value makes a mapping, also called a dictionary. A playbook's tasks: is a list of dictionaries — each task is one list item that is itself a dictionary of a name and a module call. Keeping that shape in your head is what lets you read a playbook's indentation as structure.
Confusing the two reshapes the document into something Ansible cannot run. A stray - in front of name: turns a single task into two malformed half-tasks; a missing - collapses two tasks into one dictionary with duplicate keys. The Larkspur site.yml is a list of plays, each play a dictionary, each holding a tasks: list of dictionaries — the same two shapes all the way down.
The Truthiness Trap
YAML coerces a long list of bare words to booleans: yes, no, true, false, on, and off all become true or false. It also reads bare numbers as integers and floats, and certain bare strings as dates. So a host named on, a ZIP code 02134, or a version 1.10 silently becomes a bool, an octal integer (02134 is read in base 8 as 1116, not the 2,134 you meant), or a float that lost its trailing digit.
Some of this coercion is exactly what you want: become: yes is meant to be a boolean, and Ansible relies on that. The bug is the unquoted value you intended as a string that YAML turns into something else. The rule that keeps you safe is to quote anything that could be misread, and to never trust a bare word to stay a word.
Quoting Discipline
Quote any scalar that contains a :, starts with {{, or could be read as a number, bool, or date. The sharpest case is a Jinja2 expression at the start of a value: msg: {{ item }} is a parse error, because YAML sees the leading { and tries to read a dictionary. Written as msg: "{{ item }}" it parses as a string and Jinja2 renders it at run time, which is why a templated value at the start of a line is always quoted.
- hosts: web become: yes # a real boolean — correct unquoted vars: app_version: "1.10" # quoted, or YAML makes it the float 1.1 listen_port: 443 # a real integer — correct unquoted tasks: - name: Show the target host ansible.builtin.debug: msg: "{{ ansible_hostname }} on port {{ listen_port }}"
The pattern is mechanical: when in doubt, quote. Quoting a value that did not need it costs nothing, while leaving one unquoted that did is a bug that can install the wrong package version or compare against a string that never matches. Treat quotes as the default for any value with a special character or an ambiguous shape, not as decoration you add only when something breaks.
yes, no, true, off all coerce to a bool — a string you meant silently becomes true.{{ reads as a dictionary and fails — quote any Jinja2 expression at the start of a value.key: value vs key=value Module Args
Module arguments come in two forms. The structured YAML form puts each argument on its own indented line — name: nginx under the module key — and is the readable default for playbooks, because it diffs cleanly and reviews well. The inline form, name=nginx state=present on the module line itself, is the compact shorthand the ad-hoc -a flag uses on the command line.
Each form is fine in its place; mixing them inside one task is the mistake. Writing inline name=nginx state=present on the same task that also carries a structured become: yes produces an ambiguous parse failure rather than a clear error. Pick the structured form for everything you check into a playbook and reserve the inline form for one-line ad-hoc commands.
- Indenting with a tab anywhere in the file — YAML rejects tabs outright, the editor that inserted it shows nothing visibly wrong, and the parse error reads as a mystery until you reveal whitespace.
- Leaving a version or ID unquoted, like
version: 1.10, and having YAML drop the trailing zero to the float1.1, so the wrong package version installs with no error at all. - Writing
msg: {{ ansible_hostname }}unquoted — YAML reads the leading{{as a dictionary and fails; a Jinja2 expression at the start of a value must be quoted. - Mixing inline
name=nginx state=presentwith a structuredbecome: yeson the same task and getting an ambiguous parse failure instead of a clear message about what is wrong. - Relying on
yesornoas string values — an answer field, a hostname — and having them coerced to booleans, so a string comparison later never matches what you expected.
- Configure your editor for two-space indentation and a no-tabs rule on
.ymlfiles, and runyamllintin CI so structural errors never reach a playbook run. - Quote every scalar that starts with
{{, contains a:, or could be misread as a number, bool, or date, rather than guessing which ones YAML will coerce. - Use the structured
key: valueargument form in playbooks and reservekey=valuefor ad-hoc-a, so playbooks stay diffable and reviewable. - Validate parsing with
ansible-playbook --syntax-check site.ymlbefore a real run, catching YAML and structure errors without touching a single host. - Treat quotes as the default for any ambiguous value, since a needless quote costs nothing while a missing one is a silent type bug.
Knowledge Check
Why does a single tab character break a YAML file in a way that is hard to spot?
- YAML rejects tabs for indentation outright, yet most editors render a tab and spaces identically, so nothing looks wrong
- Tabs are perfectly valid indentation in the YAML spec everywhere, but Ansible layers on its own extra rule that specifically forbids them inside playbook files at parse time
- A tab silently doubles the indentation depth of its line, quietly shifting the nesting of everything that follows
- Editors strip the tab out on save, so the file loses its block structure on disk without you noticing
You write app_version: 1.10 unquoted in a vars block. What does YAML hand to Ansible?
- The float
1.1— the trailing zero is dropped, so the wrong version string is used with no error - The string
"1.10"exactly as you typed it, since YAML preserves version-like values verbatim - A parse error at load time, because any value containing a dot must always be quoted in YAML
- The integer
110, with YAML dropping the dot and concatenating the digits
Why must msg: {{ ansible_hostname }} be written as msg: "{{ ansible_hostname }}"?
- An unquoted leading
{{is read by YAML as the start of a dictionary, so the line fails to parse before Jinja2 ever runs - Jinja2 only ever evaluates the expressions that are wrapped in double quotes, and silently ignores any
{{ }}that is left unquoted, leaving the raw braces in the value - The quotes are what tell Ansible to go and gather the
ansible_hostnamefact before the task runs - Without the quotes YAML would coerce the whole value to a boolean instead of a string
When is the inline key=value argument form the right choice over the structured YAML form?
- In a one-line ad-hoc command passed to
-a, where compactness matters and there is nothing to review or diff - In any task inside a playbook, because the inline form parses measurably faster than the structured YAML form
- In every playbook task, because it is the only argument form that supports Jinja2 expressions
- Whenever a task also sets
become, so the escalation and the arguments stay on one line
You got correct