What Is YAML (YAML Ain't Markup Language)? A Complete Guide
Learn what YAML is, how indentation, mappings, lists, scalars, and serialization work, and why YAML is widely used for configuration, automation, and infrastructure files.
Open almost any modern infrastructure project and there’s a good chance you’ll eventually encounter a file that looks something like this:
app:
name: web-api
environment: production
replicas: 3
features:
- logging
- authentication
There are no braces surrounding objects, very few quotation marks, and hardly any punctuation. Instead, most of the structure is communicated through indentation.
That’s YAML.
YAML is used throughout modern software development for configuration files, deployment definitions, automation workflows and infrastructure. Kubernetes manifests use it. Docker Compose files use it. Ansible playbooks commonly use it. GitHub Actions workflows are written in it.
The syntax can look unusually simple when you first encounter it.
That simplicity is one of YAML’s biggest strengths, but it also explains many of its sharp edges. In YAML, whitespace matters. An innocent-looking indentation mistake can change the structure of the data or make the file invalid entirely.
Understanding YAML therefore isn’t just about memorizing where to put a colon or dash. It starts with understanding what YAML actually represents.
What Is YAML?
YAML is a human-readable data serialization language used to represent structured data.
The name is a recursive acronym:
YAML Ain’t Markup Language.
That name helps distinguish YAML from markup languages such as HTML and XML. YAML isn’t primarily intended to describe how a document should be presented. Its main purpose is to represent data in a format that both humans and software can work with.
A simple YAML document might describe an application:
name: web-app
environment: production
replicas: 3
To a person, the meaning is fairly obvious.
The application is called web-app, it runs in the production environment, and it should have three replicas.
Software can parse the same YAML into structured data:
┌─────────────────────────────┐
│ YAML File │
│ │
│ name: web-app │
│ environment: production │
│ replicas: 3 │
└──────────────┬──────────────┘
│
│ YAML parser
▼
┌─────────────────────────────┐
│ Application Data │
│ │
│ name → "web-app" │
│ environment → "production" │
│ replicas → 3 │
└─────────────────────────────┘
This is why YAML appears so often in configuration.
A developer can open the file and understand it without much ceremony, while an application can parse the same file and use those values programmatically.
YAML can represent much more than simple key-value pairs. It supports nested structures, lists, strings, numbers, booleans, null values and other data types.
For example:
server:
host: api.example.com
port: 443
secure: true
features:
- authentication
- logging
- caching
Here we have nested data, several different value types and a list.
To understand why YAML is designed this way, we first need to understand the problem it solves: data serialization.
YAML as a Data Serialization Language
Data serialization sounds more complicated than it is.
Software works with data structures in memory. An application might have an object representing a server configuration:
Server
├── host: api.example.com
├── port: 443
└── secure: true
That structure is useful while the program is running, but sometimes the data needs to exist outside the program.
Perhaps it needs to be saved in a file.
Perhaps another program needs to read it.
Perhaps a developer needs to edit it before the application starts.
The data therefore needs a representation that can be stored and later reconstructed.
That is the basic idea behind serialization.
Application Data
│
│ serialize
▼
┌──────────────────────┐
│ YAML Document │
│ │
│ server: │
│ host: example.com │
│ port: 443 │
└──────────┬───────────┘
│
│ parse
▼
Application Data
YAML provides a standardized way to represent that structured information as text.
Its structures also map naturally to concepts used by most programming languages.
At a high level, YAML data is commonly made from three things:
YAML
│
├── Mappings
│ key → value
│
├── Sequences
│ ordered lists of values
│
└── Scalars
individual values
A mapping associates keys with values.
name: web-app
port: 8080
A sequence contains an ordered collection of values.
features:
- logging
- caching
- metrics
A scalar is an individual value such as a string, number, boolean or null value.
name: web-app
replicas: 3
enabled: true
Those concepts translate cleanly into the native structures used by programming languages.
Conceptually, a YAML mapping might become a dictionary in Python, an object in JavaScript, a map in Go, or an equivalent structure in another language.
YAML
│
▼
┌─────────────────┐
│ YAML Parser │
└────────┬────────┘
│
┌──────────┼───────────┐
▼ ▼ ▼
Python JavaScript Go
dictionary object map
This doesn’t mean every language represents every YAML feature identically. The parser and YAML version matter, particularly once more advanced features are involved.
But for ordinary configuration data, the relationship is intuitive.
That makes YAML particularly useful when humans need to create or modify structured data that software will later consume.
And that’s exactly what happens with configuration files.
YAML Configuration Files
Configuration tells software how it should operate without requiring those choices to be hard-coded into the application itself.
Imagine an application needs to know which port to listen on, whether logging is enabled, and how many worker processes it should start.
Those values could be written directly into the program:
port = 8080
logging = true
workers = 4
But now changing the production configuration may require changing application code.
Instead, those values can live in a configuration file:
server:
port: 8080
logging:
enabled: true
workers: 4
The application reads the configuration when it starts.
┌────────────────────┐
│ config.yaml │
│ │
│ port: 8080 │
│ workers: 4 │
└─────────┬──────────┘
│
│ parse
▼
┌────────────────────┐
│ Application │
│ │
│ starts using the │
│ configured values │
└────────────────────┘
YAML works well here because configuration is usually consumed by software but frequently inspected and changed by people.
A configuration format therefore benefits from being structured without being unnecessarily difficult to edit.
YAML can also represent hierarchy naturally.
A real application rarely has a completely flat set of settings. It might have server settings, database settings, logging settings and feature flags:
server:
host: 0.0.0.0
port: 8080
database:
host: db.example.internal
port: 5432
logging:
level: info
enabled: true
features:
authentication: true
caching: false
The relationships are visible immediately.
port: 5432 belongs to database.
level: info belongs to logging.
caching: false belongs to features.
This becomes particularly useful in infrastructure and automation, where configuration can grow considerably larger than a handful of application settings.
A deployment definition might describe containers, networking and storage. An automation workflow might contain jobs, steps and environment variables. Infrastructure configuration might describe many resources and their properties.
YAML provides enough structure to represent these relationships while remaining plain text.
That plain-text nature has another useful property: YAML files work well with version control.
A configuration change can be committed alongside source code, reviewed in a pull request and compared against an earlier version.
For example, a change might be as small as:
replicas: 3
becoming:
replicas: 5
The configuration itself becomes part of the system’s history.
YAML isn’t the only format capable of doing this. JSON, TOML, XML and other formats can also represent configuration.
One reason YAML became particularly popular is the way it minimizes visual syntax.
Why YAML Is Human-Readable
YAML is designed so that ordinary structured data can be read without navigating large amounts of punctuation.
Consider this data represented in JSON:
{
"server": {
"host": "api.example.com",
"port": 443,
"secure": true
}
}
The equivalent YAML can look like this:
server:
host: api.example.com
port: 443
secure: true
Both represent essentially the same structure.
The difference is how that structure is expressed.
JSON uses explicit punctuation:
{ } object boundaries
[ ] array boundaries
" " strings and property names
, separators
: key/value separation
YAML can often omit much of that syntax.
Instead, it relies heavily on newlines and indentation:
server:
host: api.example.com
port: 443
secure: true
A person can visually follow the hierarchy.
server
│
├── host
├── port
└── secure
This is particularly convenient for configuration files because people frequently need to inspect or edit them directly.
Lists are similarly lightweight.
JSON:
{
"features": [
"logging",
"authentication",
"caching"
]
}
YAML:
features:
- logging
- authentication
- caching
Again, neither representation is inherently correct for every situation.
JSON’s explicit punctuation can be useful for machine-generated data and interchange. YAML’s lighter syntax can be pleasant when humans spend a lot of time editing configuration.
But the lack of punctuation comes with a trade-off.
Something still needs to communicate structure.
In YAML, that job falls largely to whitespace.
This is why indentation is one of the first YAML rules worth understanding properly.
YAML Indentation and Whitespace
In YAML, indentation communicates hierarchy.
Consider this configuration:
app:
name: web-portal
environment: production
name and environment are indented beneath app, so they belong to the mapping represented by app.
Visually:
app
│
├── name: web-portal
└── environment: production
Now add a database:
app:
name: web-portal
environment: production
database:
host: db.example.internal
port: 5432
The indentation tells us exactly where each value belongs.
app
│
├── name: web-portal
├── environment: production
│
└── database
├── host: db.example.internal
└── port: 5432
database belongs to app.
host and port belong to database.
This hierarchy isn’t decorative formatting. It is part of the data structure.
If the indentation changes, the meaning can change.
For example, compare:
app:
name: web-portal
database:
host: db.example.internal
with:
app:
name: web-portal
database:
host: db.example.internal
In the first example, database belongs inside app.
In the second, app and database are separate top-level keys.
Conceptually:
FIRST
app
├── name
└── database
└── host
SECOND
├── app
│ └── name
│
└── database
└── host
A small visual difference has changed the structure of the document.
This is why YAML indentation errors can be frustrating. Something that looks like ordinary formatting in another language may determine whether the YAML parses correctly at all.
Use Spaces for Indentation
YAML indentation should use spaces rather than tab characters.
A common convention is two spaces per indentation level:
application:
database:
host: localhost
port: 5432
The important part is maintaining a clear and valid structure.
If related entries are intended to exist at the same level, they should line up:
database:
host: localhost
port: 5432
username: app
A malformed indentation pattern can cause a parser error or produce a structure different from what the author intended.
One useful habit is to think about YAML as a tree rather than simply lines of text.
configuration
│
├── application
│ ├── name
│ └── environment
│
└── database
├── host
└── port
The indentation in the YAML is expressing that tree.
Once that idea is clear, most everyday YAML syntax becomes much easier to understand.
The first structure to learn is the one we’ve already been using throughout these examples: key-value pairs.
YAML Key-Value Pairs
One of the most common structures in YAML is a mapping, usually written as a collection of key-value pairs.
The basic syntax is:
key: value
For example:
name: web-app
environment: production
Here, name is a key and web-app is its value.
Likewise, environment is a key whose value is production.
You can think of the structure like this:
┌─────────────┬────────────┐
│ Key │ Value │
├─────────────┼────────────┤
│ name │ web-app │
│ environment │ production │
└─────────────┴────────────┘
Values don’t have to be strings.
They can be numbers:
replicas: 3
port: 8080
They can be booleans:
enabled: true
debug: false
And a value can itself contain another mapping:
database:
host: localhost
port: 5432
Here, database doesn’t point to one simple scalar value.
It points to another structure:
database
│
├── host → localhost
└── port → 5432
Mappings can therefore be nested as deeply as the data requires, although deeply nested configuration can eventually become difficult for humans to navigate.
A value can also be a collection of multiple items.
For example, an application may have several enabled features:
features:
- logging
- authentication
- caching
The dash syntax introduces another fundamental YAML structure: the sequence, or list.
YAML Sequences and Lists
A YAML sequence represents an ordered list of items.
In the block style you’ll see most often in configuration files, each item begins with a dash:
features:
- logging
- authentication
- caching
Here, features is the key and its value is a sequence containing three items.
Conceptually, the structure looks like this:
features
│
├── logging
├── authentication
└── caching
This is roughly equivalent to an array or list in a programming language.
Sequences aren’t limited to strings.
They can contain numbers:
ports:
- 80
- 443
- 8080
Or other scalar values:
settings:
- true
- 3
- production
More importantly, sequence items can themselves be mappings.
Suppose an application has several servers:
servers:
- name: web-1
host: 10.0.0.10
- name: web-2
host: 10.0.0.11
Now servers is a sequence containing two mappings.
The structure is:
servers
│
├── item 1
│ ├── name: web-1
│ └── host: 10.0.0.10
│
└── item 2
├── name: web-2
└── host: 10.0.0.11
This pattern appears constantly in real YAML files.
A CI/CD pipeline may contain a list of steps. A Docker Compose service may contain a list of ports. A Kubernetes resource may contain a list of containers.
For example:
containers:
- name: web
image: web-app:1.4
- name: metrics
image: metrics-agent:2.1
The dash begins a new item in the sequence. The indentation underneath it tells YAML which properties belong to that item.
Sequences can also appear inside mappings that are themselves inside sequences, so YAML can represent surprisingly complex data with only a few pieces of syntax.
But whether a value appears inside a mapping or sequence, eventually you reach an individual value.
Those individual values are called scalars.
Scalars in YAML
A scalar is a single value rather than a collection.
Consider:
name: web-app
replicas: 3
enabled: true
The values web-app, 3, and true are all scalars.
YAML scalars commonly represent things such as:
String → web-app
Integer → 3
Float → 3.14
Boolean → true
Null → null
Strings are especially common.
In many ordinary cases, YAML doesn’t require them to be surrounded by quotation marks:
environment: production
region: europe
name: web-api
You can also quote strings explicitly:
environment: "production"
region: 'europe'
Quoted strings become useful when the value contains characters or formatting that could otherwise have special meaning, or when you want to make it unambiguous that a value should be treated as text.
For example:
message: "server: unavailable"
Without quoting, punctuation such as a colon followed by a space can affect how YAML interprets the line.
Quoting can also help when a value looks like another data type but is intended to remain a string.
Consider:
version: "1.20"
The author may specifically want the text 1.20, rather than a parser interpreting the value as a number.
This distinction matters because YAML parsers don’t necessarily treat every unquoted value as text.
They resolve values according to YAML’s data type rules.
YAML Data Types
Although YAML files are plain text, the data they represent has types.
A parser reading this:
replicas: 3
doesn’t necessarily produce the string "3".
It can produce an integer with the value 3.
Likewise:
enabled: true
represents a boolean rather than the text "true".
Common YAML data types include strings, integers, floating-point numbers, booleans and null values.
For example:
name: web-app
replicas: 3
timeout: 2.5
enabled: true
cache: null
Conceptually:
name ─────► string
replicas ─────► integer
timeout ─────► floating-point number
enabled ─────► boolean
cache ─────► null
Mappings and sequences provide the larger structures that contain those values:
server:
host: localhost
port: 8080
features:
- logging
- caching
Here:
server → mapping
features → sequence
localhost → string
8080 → integer
logging → string
caching → string
YAML can also represent date and timestamp-like values, although this is an area where parser behaviour and YAML versions deserve attention.
That’s an important point more generally.
Values that look obvious to a human aren’t always interpreted exactly as expected by every YAML parser or schema.
For configuration where the exact type matters, quoting an ambiguous value can make the author’s intention clearer:
build: "0012"
version: "1.20"
Rather than assuming that anything without quotation marks is simply a string, it’s better to remember that YAML has a type system and that parsers resolve scalar values according to YAML rules.
Most everyday configuration files remain straightforward because they use a relatively small subset of these types.
The real expressive power comes from combining those values into larger structures.
Nested Structures in YAML
Real configuration rarely consists of one flat list of settings.
An application might have settings for its server, database, logging system and enabled features.
YAML can represent that hierarchy directly:
application:
name: web-portal
environment: production
server:
host: 0.0.0.0
port: 8080
database:
host: db.example.internal
port: 5432
The indentation expresses parent-child relationships.
application
│
├── name: web-portal
├── environment: production
│
├── server
│ ├── host: 0.0.0.0
│ └── port: 8080
│
└── database
├── host: db.example.internal
└── port: 5432
A mapping can contain another mapping.
It can also contain a sequence:
application:
features:
- logging
- authentication
- caching
And a sequence can contain mappings:
application:
servers:
- name: web-1
port: 8080
- name: web-2
port: 8081
Put those structures together and you can describe fairly sophisticated configurations:
application
│
├── name
│
├── features
│ ├── feature
│ ├── feature
│ └── feature
│
└── servers
├── server
│ ├── name
│ └── port
│
└── server
├── name
└── port
This is why understanding indentation is more important than memorizing a large collection of syntax rules.
Most ordinary YAML can be understood by asking three questions:
Is this a mapping, a sequence, or a scalar?
Then:
What is it nested inside?
The indentation answers the second question.
Let’s put those pieces together in one practical example.
YAML Example
Consider a configuration file for a small web application:
app:
name: web-portal
environment: production
replicas: 3
features:
- logging
- authentication
database:
host: db.example.internal
port: 5432
There isn’t much syntax here, but the document contains most of the YAML concepts we’ve covered.
At the top is a mapping with the key app.
Everything indented underneath it belongs to that application:
app
│
├── name
├── environment
├── replicas
├── features
└── database
name, environment, and replicas contain scalar values:
name: web-portal
environment: production
replicas: 3
Two of those values are strings.
replicas is an integer.
Then we have a sequence:
features:
- logging
- authentication
features contains two items.
Finally, database contains another mapping:
database:
host: db.example.internal
port: 5432
The complete data structure can therefore be visualized as:
app
│
├── name: web-portal
│
├── environment: production
│
├── replicas: 3
│
├── features
│ ├── logging
│ └── authentication
│
└── database
├── host: db.example.internal
└── port: 5432
This example contains mappings, a sequence, nested data, strings and integers.
More complicated YAML files are usually built from these same basic structures.
A Kubernetes manifest may be hundreds of lines long, but it’s still composed of mappings, sequences and scalar values arranged into a hierarchy.
Once you can identify those structures, unfamiliar YAML becomes considerably easier to read.
YAML isn’t the only serialization format that can represent this data, though.
The same structure could also be written as JSON.
YAML vs JSON
YAML and JSON have a lot in common.
Both can represent structured data. Both support key-value structures, ordered collections and individual values. Both are commonly parsed by software into native data structures.
Take our application configuration.
In YAML:
app:
name: web-portal
environment: production
replicas: 3
features:
- logging
- authentication
The same basic structure in JSON looks like this:
{
"app": {
"name": "web-portal",
"environment": "production",
"replicas": 3,
"features": [
"logging",
"authentication"
]
}
}
Structurally, these documents are very similar.
Structured Data
│
┌──────────┴──────────┐
▼ ▼
YAML JSON
│ │
├── mappings ├── objects
├── sequences ├── arrays
└── scalars └── values
One of the most visible differences is punctuation.
JSON uses braces to delimit objects, brackets to delimit arrays, commas to separate items, and quotation marks around property names and strings.
YAML can express the same kind of structure primarily through indentation and newlines.
Compare a simple object:
{
"database": {
"host": "localhost",
"port": 5432
}
}
with:
database:
host: localhost
port: 5432
For files that people regularly edit by hand, YAML’s lighter syntax can make configuration easier to scan.
Comments are another practical advantage for configuration. YAML supports comments using #:
database:
host: localhost
port: 5432 # PostgreSQL
That can be useful when a configuration file also needs to explain why a particular setting exists.
JSON has a different strength: its syntax is deliberately explicit and extremely common for machine-to-machine data interchange.
APIs, for example, frequently send JSON because software generates and consumes the data rather than humans maintaining a large configuration document manually.
A simplified request might return:
{
"id": 42,
"name": "web-app",
"status": "running"
}
YAML is much more commonly encountered when a human is expected to maintain the file:
services:
web:
replicas: 3
logging: true
This isn’t a rule that says “JSON is for machines and YAML is for humans.” Either format can be generated or consumed programmatically.
It’s a difference in where their respective syntax tends to be convenient.
Another useful relationship is that YAML can represent JSON-compatible data structures. If your data consists of ordinary objects, arrays and values, it can generally be represented naturally in either format.
So which should you use?
It depends on the context.
YAML
├── convenient for human-edited configuration
├── minimal visual punctuation
├── comments
└── indentation-sensitive
JSON
├── common for APIs and data interchange
├── explicit structural punctuation
├── simple machine generation
└── widely supported
Neither is universally better.
If an existing platform expects YAML, use YAML. If an API expects JSON, use JSON. If you’re designing your own configuration format, the way people and software will interact with it matters more than declaring one format the winner.
There’s another comparison that is especially relevant to YAML’s unusual name.
If YAML “ain’t markup language,” how does it differ from an actual markup language such as XML?
YAML vs XML
YAML and XML can both represent hierarchical information, but they approach the problem very differently.
XML uses elements and tags.
A simple application configuration might look like this:
<app>
<name>web-portal</name>
<environment>production</environment>
<replicas>3</replicas>
</app>
The equivalent YAML could be:
app:
name: web-portal
environment: production
replicas: 3
Visually, the difference is immediate.
XML expresses structure using explicit opening and closing tags:
<app>
<name>...</name>
</app>
YAML expresses the hierarchy primarily through mappings and indentation:
app:
name: ...
But the distinction is deeper than syntax.
XML is a markup language.
Markup languages can represent documents where text itself is part of the content and markup adds structure or meaning around it.
For example:
<article>
<title>Understanding Servers</title>
<paragraph>
A <strong>server</strong> provides a service to another system.
</paragraph>
</article>
Here, markup is embedded around document content.
YAML is primarily a data serialization language.
It is generally concerned with representing structured values:
article:
title: Understanding Servers
published: true
tags:
- networking
- servers
That difference is the reason behind the modern expansion of YAML’s name:
YAML Ain’t Markup Language.
The name emphasizes that YAML’s primary purpose is serialization rather than document markup.
That doesn’t mean XML cannot represent application data. It absolutely can, and many systems use it for exactly that purpose.
Likewise, the fact that YAML isn’t primarily a document markup language doesn’t mean it lacks sophisticated data features.
The distinction is about what the formats are designed around.
A useful simplified comparison is:
Hierarchical Information
│
┌──────────┴──────────┐
▼ ▼
YAML XML
│ │
data serialization markup language
│ │
mappings/sequences elements/tags
│ │
indentation-based explicit markup
XML’s explicit structure can be valuable in document-oriented systems and ecosystems built around XML technologies.
YAML’s lightweight representation is particularly attractive when the goal is to express configuration or other structured data that developers frequently read and edit.
In both cases, software still needs something capable of interpreting the format.
A YAML file doesn’t execute itself.
An application reads the YAML, parses its contents, and turns those contents into data structures it understands.
That distinction becomes particularly important when people describe YAML as a “language.”
YAML is a language for representing data, but it is not a general-purpose programming language.
YAML and Programming Languages
YAML is sometimes called a language, but it is not a general-purpose programming language.
You don’t normally write loops, functions, classes, or application logic directly in YAML.
Instead, YAML describes data.
Software written in a programming language then reads that data and decides what to do with it.
For example:
server:
host: localhost
port: 8080
debug: false
A Python application might parse that YAML into a dictionary.
A JavaScript application might parse it into an object.
A Go application might parse it into a map or struct.
Conceptually:
YAML File
│
▼
┌────────────────┐
│ YAML Parser │
└───────┬────────┘
│
┌─────────┼─────────┐
▼ ▼ ▼
Python JavaScript Go
data data data
structure structure structure
The YAML itself isn’t executing anything.
It’s providing structured input to software.
That distinction matters because many tools make YAML look more powerful than it really is.
Consider a CI/CD workflow:
steps:
- run: npm install
- run: npm test
The YAML isn’t running npm install.
The CI/CD platform reads the YAML, sees that a step has been defined, and then executes the command according to its own rules.
The behaviour comes from the platform.
YAML only describes the configuration.
This is why the same YAML syntax can appear across completely different ecosystems while meaning different things.
For example:
name: web
In one application, name might define a service name.
In another, it might define a workflow.
In another, it might define a Kubernetes resource.
YAML understands that name is a key with the scalar value web.
It does not understand what name means to Kubernetes, Docker Compose, GitHub Actions, or your application.
That meaning belongs to the system consuming the file.
Most major programming languages have libraries capable of parsing YAML, including Python, JavaScript, Java, Ruby and Go.
The workflow usually looks something like this:
YAML text
│
▼
Parser library
│
▼
Native data structure
│
▼
Application logic
This language-neutral design is one of the reasons YAML became so common in tooling.
A platform can define a configuration format once, and developers using many different programming languages can still work with it.
That becomes especially useful in DevOps environments, where YAML frequently sits between developers and the systems they operate.
YAML in DevOps
YAML has become strongly associated with DevOps because modern infrastructure depends heavily on configuration.
Deployments need configuration.
Build systems need configuration.
Infrastructure needs configuration.
Automation tools need configuration.
CI/CD pipelines need configuration.
YAML provides a convenient way to express those settings in plain text.
A simplified deployment configuration might look like this:
application:
name: web-api
environment: production
replicas: 4
database:
host: db.internal
port: 5432
monitoring:
enabled: true
This configuration can be stored alongside source code.
That matters because infrastructure and operational settings can then be managed in much the same way as application code.
A change can be committed:
replicas: 3
↓
replicas: 4
It can be reviewed.
It can be compared against earlier versions.
It can be tested before deployment.
And if something goes wrong, teams can see exactly what changed.
This practice is closely related to configuration as code.
The idea isn’t that YAML suddenly becomes executable software.
It’s that important configuration is treated as a versioned, reviewable artifact rather than something edited manually on a production server with no history.
For example:
Developer
│
│ edits YAML
▼
Git Repository
│
│ pull request / review
▼
Automation Platform
│
│ applies configuration
▼
Infrastructure
This workflow provides visibility.
Instead of someone changing an important production value by hand, the change can pass through the same review process as the code it supports.
YAML is useful here because the files are both machine-readable and reasonably approachable to humans.
That combination appears repeatedly throughout DevOps tooling.
The same principle also explains why YAML is so common in automation.
YAML for Automation
Automation tools often need a way for humans to describe what should happen without requiring them to write the orchestration engine itself.
YAML fits that role well.
A configuration might describe a sequence of operations:
tasks:
- name: install package
package: nginx
- name: start service
service: nginx
The YAML isn’t installing anything by itself.
An automation tool reads those values and performs the work.
The relationship looks like this:
┌──────────────────────┐
│ YAML File │
│ │
│ desired tasks │
│ configuration │
│ workflow │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Automation Tool │
│ │
│ interprets the YAML │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Actual Actions │
│ │
│ install │
│ configure │
│ deploy │
└──────────────────────┘
This is often described as declarative automation.
Instead of writing every low-level operation required to reach a result, the configuration describes the intended state or workflow and the automation platform determines how to apply it.
Not every YAML-based tool is purely declarative, and many include procedural concepts such as ordered steps.
But the overall pattern remains the same:
YAML describes. Another system executes.
Ansible is one recognizable example.
An Ansible playbook might contain YAML that describes hosts and tasks:
- hosts: web
tasks:
- name: install nginx
package:
name: nginx
state: present
Again, the behaviour belongs to Ansible.
YAML provides the structure in which that behaviour is described.
The same relationship appears in container orchestration, CI/CD platforms, infrastructure tools and deployment systems.
One of the most visible examples is Kubernetes.
YAML in Kubernetes
Kubernetes uses YAML extensively to describe resources.
Rather than manually telling Kubernetes which low-level operation to perform at every moment, you usually define the desired state of a resource.
A simplified Kubernetes Deployment might look like this:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 3
Several fields appear repeatedly in Kubernetes manifests.
apiVersion identifies the Kubernetes API version used for the resource.
kind identifies the type of resource.
metadata contains identifying information such as the resource name.
spec describes the desired configuration.
Conceptually:
Kubernetes Manifest
│
├── apiVersion
├── kind
├── metadata
│ └── name
│
└── spec
└── desired state
If the manifest says:
replicas: 3
the desired state is that three replicas should exist.
Kubernetes then works to make reality match that declaration.
YAML says:
replicas: 3
│
▼
Kubernetes observes:
replicas: 2
│
▼
Kubernetes creates another replica
│
▼
Actual state:
replicas: 3
This is an important distinction.
The YAML does not contain the code responsible for starting containers or scheduling workloads onto machines.
It describes the resource Kubernetes should manage.
Kubernetes uses YAML for many resource types, including:
Deployment
Service
ConfigMap
Secret
Job
Ingress
PersistentVolumeClaim
Each resource has its own expected schema.
For example, a Service and Deployment are both written in YAML, but Kubernetes interprets their fields differently because their kind values identify different resource types.
A slightly more complete Deployment might look like this:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: example/web-app:1.0
This looks more complicated than our earlier YAML examples, but structurally it contains the same building blocks.
There are mappings:
metadata:
name: web-app
Nested mappings:
selector:
matchLabels:
app: web
Sequences:
containers:
- name: web
image: example/web-app:1.0
And scalar values such as 3, web, and example/web-app:1.0.
Once you understand ordinary YAML, Kubernetes manifests become much easier to read because the difficult part is no longer the serialization syntax.
The remaining challenge is understanding Kubernetes itself.
YAML plays a similar role in Docker Compose.
YAML in Docker Compose
Docker Compose uses YAML to describe applications made up of multiple containers.
Suppose a web application requires two services:
- a web application
- a PostgreSQL database
A simplified Compose file might look like this:
services:
web:
image: example/web-app:1.0
ports:
- "8080:8080"
database:
image: postgres:17
The top-level services mapping contains two entries.
services
│
├── web
│ ├── image
│ └── ports
│
└── database
└── image
Each service can have its own configuration.
A more realistic example might include environment variables and a volume:
services:
web:
image: example/web-app:1.0
ports:
- "8080:8080"
environment:
DATABASE_HOST: database
database:
image: postgres:17
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
The YAML now describes several relationships.
The web service runs one image and exposes a port.
The database service runs another image.
The web application receives configuration telling it where to find the database.
A named volume is used to persist database data.
Compose can also describe networks, health checks, restart behaviour, dependencies and many other container settings.
The important point is that Compose YAML describes a multi-container application in one structured document.
compose.yaml
│
▼
┌──────────────────────┐
│ Docker Compose │
└──────────┬───────────┘
│
┌───┴────────┐
▼ ▼
Web Container DB Container
Once again, YAML is the representation layer.
Docker Compose is the system that understands what keys such as services, image, ports, and volumes actually mean.
The same pattern shows up in another major area of software delivery: CI/CD pipelines.
YAML in CI/CD Pipelines
CI/CD systems automate the work required to build, test, and deploy software.
A pipeline might need to:
Checkout code
│
▼
Install dependencies
│
▼
Run tests
│
▼
Build application
│
▼
Deploy
These workflows need configuration.
YAML is frequently used because the workflow is structured, ordered, version-controlled, and regularly edited by developers.
A simplified pipeline definition might look like this:
jobs:
test:
steps:
- checkout
- install-dependencies
- run-tests
deploy:
steps:
- build
- deploy
Different CI/CD platforms have different schemas, but the concepts are often similar.
A workflow has jobs.
Jobs contain steps.
Jobs may depend on earlier jobs.
Environment values may be supplied.
Different runners or execution environments may be selected.
For example:
Pipeline
│
├── Test Job
│ ├── checkout
│ ├── install
│ └── test
│
└── Deploy Job
├── build
└── deploy
The YAML makes the workflow visible in the repository.
That has practical advantages.
If a deployment pipeline changes, the change can be reviewed with the rest of the code.
If a build suddenly starts failing after a workflow edit, the history of the YAML configuration can be inspected.
If a team wants the same workflow on another branch or environment, the configuration can be reused or adapted.
As with other YAML-based systems, the pipeline platform determines the meaning of the keys.
YAML itself doesn’t know what jobs, steps, or deploy mean.
One widely recognized implementation of this model is GitHub Actions.
YAML in GitHub Actions
GitHub Actions workflows are defined in YAML files stored inside:
.github/workflows/
A repository might contain:
.github/
└── workflows/
├── test.yml
└── deploy.yml
Each workflow defines when it should run and what work should happen.
A small example might look like this:
name: Test
on:
push:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: npm test
At a high level:
Workflow
│
├── name
├── trigger
│ └── push
│
└── jobs
└── test
├── runner
└── steps
├── checkout
└── npm test
The on section defines the event that triggers the workflow.
In this example, the workflow runs when code is pushed.
jobs contains the work to perform.
runs-on selects the type of runner.
steps contains the individual operations within the job.
A step can use an existing action:
- uses: actions/checkout@v4
Or it can execute a command:
- name: Run tests
run: npm test
More complicated workflows can contain multiple jobs and dependencies between them.
For example:
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh
Conceptually:
Push
│
▼
Test Job
│
│ success
▼
Deploy Job
The workflow remains structured data.
GitHub Actions reads that structure and turns it into actual execution.
At this point we’ve seen YAML used for application configuration, automation, Kubernetes, Docker Compose and CI/CD.
Most files in these systems can be written using the basic YAML structures we’ve already covered.
But YAML also contains more advanced features intended to reduce duplication.
One of the best-known examples is anchors and aliases.
YAML Anchors and Aliases
YAML anchors and aliases let you reuse values or structures within the same document.
They are useful when a configuration file contains repeated data.
Suppose several services share the same logging configuration:
service_a:
logging:
level: info
format: json
service_b:
logging:
level: info
format: json
This works, but the same structure is duplicated.
YAML anchors let you define that structure once.
An anchor is created using &:
default_logging: &default_logging
level: info
format: json
An alias refers back to the anchored value using *:
service_a:
logging: *default_logging
Put together:
default_logging: &default_logging
level: info
format: json
service_a:
logging: *default_logging
service_b:
logging: *default_logging
Conceptually:
default_logging
│
│ anchor
▼
┌─────────────────┐
│ level: info │
│ format: json │
└────────┬────────┘
│
┌─────┴─────┐
▼ ▼
service_a service_b
The advantage is obvious when the repeated configuration is larger.
Instead of updating the same block in several places, you can maintain one reusable definition.
YAML also commonly supports merge patterns using the << merge key in tooling that implements that behavior.
For example:
defaults: &defaults
retries: 3
timeout: 30
production:
<<: *defaults
timeout: 60
Here, production starts with the values from defaults and overrides timeout.
Conceptually:
defaults
├── retries: 3
└── timeout: 30
│
▼
production
├── retries: 3
└── timeout: 60
Anchors and aliases can reduce duplication, but they should be used carefully.
A small amount of reuse can make a file easier to maintain.
Too much reuse can make a configuration difficult to follow because the reader has to jump around the document to understand where values came from.
This:
database:
host: db.internal
port: 5432
is immediately understandable.
A heavily abstracted configuration involving many anchors, aliases and merges may be technically shorter while being much harder to read.
The same principle applies here as elsewhere in configuration design:
reuse is useful when it reduces repetition without hiding the structure people need to understand.
Anchors and aliases are an advanced YAML feature, and many everyday YAML files don’t need them at all.
For most users, knowing how mappings, sequences, scalars and indentation work is far more important.
Another practical question comes up much earlier: what should a YAML file actually be called?
YAML File Extensions: .yaml and .yml
YAML files commonly use one of two extensions:
.yaml
.yml
Both represent YAML files.
For example:
config.yaml
docker-compose.yml
workflow.yaml
application.yml
There is no fundamental difference in the YAML syntax inside a .yaml file and a .yml file.
The extension simply tells people and tools what kind of content the file contains.
The longer .yaml extension is generally clearer because it matches the full name of the format.
But .yml remains extremely common, particularly in ecosystems and older tooling where that naming convention became established.
For example, you may encounter:
docker-compose.yml
or:
.github/workflows/test.yml
while another project may use:
config.yaml
The right choice is usually whichever convention the tool or project already uses.
YAML files are plain-text files.
That means they can be opened and edited in an ordinary text editor:
config.yaml
│
▼
Text Editor
│
▼
name: web-app
port: 8080
Editors and IDEs often provide YAML-aware features such as syntax highlighting, indentation assistance, schema validation and error detection.
Those features are useful because YAML’s lightweight syntax can make small structural mistakes easy to miss.
The exact rules that determine whether YAML is valid don’t come from individual editors or platforms, though.
They come from the YAML specification.
The YAML Specification
YAML is formally defined by the YAML specification.
The specification describes the syntax and behavior of the language: how mappings work, how sequences are represented, how scalars are interpreted, how tags behave, how documents are separated and how other features of YAML should be parsed.
This matters because YAML is more sophisticated than the small subset most configuration files use.
The everyday syntax might look simple:
name: web-app
replicas: 3
enabled: true
But parsers still need precise rules for interpreting that document.
For example:
Is this a string?
Is this a number?
Is this a boolean?
Where does this nested mapping end?
What does this quoted value contain?
The specification defines those rules.
YAML has also evolved over time, and different tools may support different YAML versions or subsets of the language.
That means parser behavior can sometimes vary between environments.
A file accepted by one tool may behave differently in another if the tools use different YAML parsers, schemas or supported versions.
This is especially relevant around edge cases involving implicit types and older YAML behavior.
For ordinary configuration, the safest approach is usually to keep syntax simple and explicit.
Use predictable indentation.
Quote values when their intended type might otherwise be ambiguous.
Avoid unnecessary advanced features.
And when parser behavior matters, check the documentation for the tool consuming the YAML as well as the official YAML specification.
That distinction is important because the YAML specification defines the language, while platforms define their own schemas on top of it.
For example:
YAML specification
│
▼
valid YAML structure
│
▼
Kubernetes schema
│
▼
valid Kubernetes manifest
A document can be valid YAML while still being invalid for Kubernetes.
For example:
banana: production
replicas: 3
That may be perfectly valid YAML.
But a Kubernetes resource won’t accept arbitrary fields simply because the syntax parses correctly.
Likewise, a GitHub Actions workflow, Docker Compose file or application configuration must follow the schema expected by that specific system.
Understanding this separation helps explain why YAML errors sometimes come in two stages:
1. YAML parser error
"The document structure is invalid"
2. Application/schema error
"The YAML is valid, but this field is not supported"
The first problem belongs to YAML syntax.
The second belongs to the tool consuming it.
At this point, we’ve covered what YAML is, how its core structures work and where it is commonly used.
That leaves one naming question worth answering directly.
Why is it called YAML Ain’t Markup Language?
What Does YAML Stand For?
YAML currently stands for:
YAML Ain’t Markup Language.
This is a recursive acronym, meaning the acronym itself appears inside its expansion.
You can think of it as:
YAML
│
└── YAML Ain't Markup Language
│
└── YAML Ain't Markup Language
...
The name is deliberately emphasizing what YAML is not.
YAML was historically associated with the phrase:
Yet Another Markup Language.
The name was later changed to YAML Ain’t Markup Language to better reflect the format’s purpose as a data serialization language rather than a document markup language.
That distinction matters.
HTML is designed to mark up web documents.
XML can represent structured documents and data using tags and elements.
YAML is primarily designed to serialize structured data.
Compare:
<h1>Server Configuration</h1>
<p>The server uses port 8080.</p>
with:
server:
port: 8080
The HTML describes document structure and presentation semantics.
The YAML represents data.
That’s why the phrase “YAML Ain’t Markup Language” is more than a joke in the name.
It points to the format’s core purpose.
YAML FAQ
Is YAML a programming language?
No.
YAML is a data serialization language, not a general-purpose programming language.
It represents structured data and configuration:
server:
port: 8080
Another application or platform reads that data and performs the actual logic.
For example:
YAML configuration
│
▼
Application
│
▼
Program behavior
The YAML describes values.
The application decides what those values mean and what actions to take.
Is YAML a markup language?
Not in its modern primary purpose.
YAML’s current name, YAML Ain’t Markup Language, emphasizes that it is designed primarily for data serialization rather than document markup.
Markup languages such as HTML and XML commonly use elements or tags to add structure or meaning to documents.
YAML primarily represents mappings, sequences and scalar values.
Is YAML easy to learn?
The basic syntax is relatively small.
Most everyday YAML can be understood by learning four concepts:
Mappings
Sequences
Scalars
Indentation
For example:
app:
name: web-app
features:
- logging
- caching
That is enough to understand a large amount of real-world YAML.
The difficulties tend to appear around indentation mistakes, quoting, implicit data types and more advanced features such as anchors.
YAML is therefore easy to begin reading but still has edge cases worth understanding when configuration becomes more complex.
What is YAML used for?
YAML is widely used for structured configuration and automation.
Common examples include:
Application configuration
Kubernetes manifests
Docker Compose files
Ansible playbooks
CI/CD pipelines
GitHub Actions workflows
Infrastructure configuration
Automation definitions
The common theme is that software needs structured input that people are also likely to read and edit.
What is the difference between YAML and JSON?
YAML and JSON can both represent structured data.
JSON uses explicit punctuation:
{
"name": "web-app",
"replicas": 3
}
YAML can represent the same data more lightly:
name: web-app
replicas: 3
YAML relies more heavily on whitespace and indentation, while JSON uses braces, brackets, commas and quotation marks.
JSON is extremely common for APIs and machine-generated data interchange.
YAML is especially common where humans regularly maintain configuration files.
Neither format is universally better.
The right choice depends on what the software expects and how people will interact with the data.
Conclusion
YAML is a human-readable data serialization language used to represent structured data in plain text.
Its syntax is built around a small number of concepts:
Mappings → key-value structures
Sequences → ordered lists
Scalars → individual values
Indentation → hierarchy
That combination makes YAML well suited to configuration.
A small file can describe an application:
app:
name: web-portal
replicas: 3
features:
- logging
- authentication
A much larger file can describe infrastructure, a deployment, an automation workflow or a CI/CD pipeline using the same fundamental structures.
This is why YAML appears throughout modern development tooling.
Kubernetes uses YAML to describe desired resources. Docker Compose uses it to describe multi-container applications. Ansible uses it for automation. GitHub Actions uses it for CI/CD workflows.
But YAML itself doesn’t perform any of those jobs.
It describes data.
The system reading the YAML gives that data meaning and turns it into behavior.
That distinction is the most useful thing to remember when working with the format:
YAML is not the application, deployment system, or automation engine. It is the structured language those systems use to understand configuration.