YAML multiline strings are one of the most confusing parts of the YAML specification. With 6 block scalar combinations, 3 chomping indicators, indentation controls, and flow scalars, it is easy to get lost. This guide covers every multiline string style with practical examples from Docker Compose, Kubernetes, GitHub Actions, and Ansible.
1. Why YAML Multiline Strings Are Confusing
YAML offers at least 9 different ways to write a string that spans multiple lines. Unlike JSON (which only has \n inside double quotes) or TOML (which has triple-quoted strings), YAML gives you fine-grained control over how newlines and trailing whitespace are handled. This power comes at the cost of complexity.
The two main block scalar styles are Literal (|) and Folded (>). Each can be combined with three chomping indicators (clip, strip, keep), resulting in 6 core combinations. On top of that, you can specify explicit indentation levels. Let us break it all down.
- 6 block scalar combinations (|, |-, |+, >, >-, >+)
- Indentation indicators (|2, >2, etc.)
- 3 flow scalar styles (plain, single-quoted, double-quoted)
- Subtle differences in trailing newline behavior
- Interaction with YAML parsers (PyYAML, js-yaml, SnakeYAML)
2. Literal Style (|): Preserves Newlines
The literal block scalar (|) preserves every newline in your text exactly as written. Each line break in the YAML source becomes a literal \n in the parsed value. A single trailing newline is added by default (clip behavior).
# Literal style (|) — preserves newlines exactly
message: |
Line one.
Line two.
Line three.Parsed result:
"Line one\nLine two\nLine three\n"# Preserves indentation beyond the block indent level
script: |
#!/bin/bash
if [ "$ENV" = "prod" ]; then
echo "Production mode"
npm run build
else
echo "Development mode"
npm run dev
fi# SQL query — newlines preserved for readability
query: |
SELECT u.id, u.name, u.email
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.created_at > '2025-01-01'
ORDER BY u.name ASC
LIMIT 100;3. Folded Style (>): Folds Newlines to Spaces
The folded block scalar (>) replaces single newlines with spaces, effectively joining consecutive lines into a single long line. However, blank lines and "more-indented" lines still create real newlines. A single trailing newline is added by default.
# Folded style (>) — joins lines with spaces
description: >
This is a very long string
that I want to wrap across
multiple lines for readability.Parsed result:
"This is a very long string that I want to wrap across multiple lines for readability.\n"Blank lines create real newlines in folded style:
# Blank lines create real newlines in folded style
content: >
Paragraph one.
Paragraph two.
Paragraph three.Parsed result:
"Paragraph one.\nParagraph two.\nParagraph three.\n"More-indented lines also preserve newlines:
# More-indented lines preserve newlines
content: >
Regular text
indented line
another indented
back to normalParsed result:
"Regular text\n indented line\n another indented\nback to normal\n"4. Chomping Indicators: Strip (-), Keep (+), Clip (default)
Chomping controls what happens to the trailing newlines at the end of a block scalar. This is one of the most misunderstood features of YAML.
Clip (default, no indicator)
Keeps a single trailing newline. Any additional trailing newlines are removed.
# Clip (default) — single trailing newline
message: |
hello world
# (two blank lines at end)# Result: "hello world\n" (one trailing newline)Strip (- indicator)
Removes ALL trailing newlines. The string ends with the last non-empty line.
# Strip (-) — no trailing newline
message: |-
hello world
# (two blank lines at end, all removed)# Result: "hello world" (no trailing newline)Keep (+ indicator)
Preserves ALL trailing newlines exactly as written.
# Keep (+) — all trailing newlines preserved
message: |+
hello world
# (two blank lines at end, all kept)# Result: "hello world\n\n\n" (all three trailing newlines kept)5. All 6 Block Scalar Combinations
Here is a comprehensive reference table showing how each combination of style and chomping indicator behaves:
| Syntax | Name | Internal Newlines | Trailing Newlines | Use Case |
|---|---|---|---|---|
| | | Literal Clip | Preserved | Single \n added | Shell scripts, code |
| |- | Literal Strip | Preserved | All removed | Inline values, no trailing NL |
| |+ | Literal Keep | Preserved | All kept | Preserving exact whitespace |
| > | Folded Clip | Folded to spaces | Single \n added | Long descriptions |
| >- | Folded Strip | Folded to spaces | All removed | Clean single-line values |
| >+ | Folded Keep | Folded to spaces | All kept | Paragraphs with trailing space |
Side-by-side examples with parsed results:
# | (literal clip) — preserves newlines, adds single trailing \n
a: |
one
two
# Result: "one\ntwo\n"
# |- (literal strip) — preserves newlines, no trailing \n
b: |-
one
two
# Result: "one\ntwo"
# |+ (literal keep) — preserves newlines, keeps ALL trailing \n
c: |+
one
two
# Result: "one\ntwo\n\n"
# > (folded clip) — folds newlines, adds single trailing \n
d: >
one
two
# Result: "one two\n"
# >- (folded strip) — folds newlines, no trailing \n
e: >-
one
two
# Result: "one two"
# >+ (folded keep) — folds newlines, keeps ALL trailing \n
f: >+
one
two
# Result: "one two\n\n"6. Indentation Control: |2, >2
By default, YAML determines the indentation level of a block scalar from the first non-empty line. Sometimes you need to override this, especially when content starts with spaces.
Why you might need explicit indentation:
- Content that starts with spaces (e.g., indented code)
- YAML parsers that get confused by leading spaces
- Ensuring consistent parsing across different YAML libraries
Without indentation indicator (auto-detect):
# Auto-detect indentation (default behavior)
# YAML detects 2-space indent from first non-empty line
content: |
normal text
more textWith explicit indentation indicator:
# Explicit indentation: |2 means "strip 2 spaces"
# Useful when content starts with spaces
content: |2
This line has 2 leading spaces after stripping
This also has 2 leading spaces
This has 0 leading spaces after stripping
# Result: " This line has 2 leading spaces after stripping\n This also has 2 leading spaces\nThis has 0 leading spaces after stripping\n"# Folded with explicit indentation
description: >2
This paragraph starts with
two extra spaces that are
preserved in the output.
# Result: " This paragraph starts with two extra spaces that are preserved in the output.\n"# Combining indentation with chomping indicators
script: |2-
#!/usr/bin/env python
def main():
print("Hello")
# Result: " #!/usr/bin/env python\n def main():\n print(\"Hello\")"
# (2 spaces stripped, no trailing newline due to -)
config: >2+
This is indented content
that will be folded
with trailing newlines kept.
# Result: " This is indented content that will be folded with trailing newlines kept.\n\n"
# (2 spaces stripped, newlines kept due to +)7. Real-World Examples
Docker Compose
Multiline commands in Docker Compose services:
# docker-compose.yml
version: "3.8"
services:
web:
image: node:20-alpine
# Literal style — each command on its own line
command: |
sh -c "
npm install &&
npm run migrate &&
npm run start
"
environment:
# Folded strip — long value on one line, no trailing newline
DATABASE_URL: >-
postgresql://user:password@db:5432/myapp
?sslmode=require
&connect_timeout=10
healthcheck:
# Literal strip — script without trailing newline
test: |-
curl -f http://localhost:3000/health || exit 1
interval: 30s
timeout: 10s
nginx:
image: nginx:alpine
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
# Folded style — long description for documentation
labels:
description: >
This is the reverse proxy service that handles
SSL termination, load balancing, and static file
serving for the web application.Kubernetes ConfigMap & Pod
Storing configuration files and scripts in ConfigMaps:
# ConfigMap with embedded configuration files
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
# Literal style — preserve the entire nginx config as-is
nginx.conf: |
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://app:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /static {
root /var/www;
expires 30d;
}
}
# Literal style — init script
init.sh: |
#!/bin/bash
set -euo pipefail
echo "Initializing database..."
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" <<'SQL'
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE
);
SQL
echo "Database initialized."
---
# Pod with multiline command
apiVersion: v1
kind: Pod
metadata:
name: debug-pod
annotations:
# Folded style — long description
description: >
This pod is used for debugging network
connectivity issues in the cluster. It
includes curl, nslookup, and ping tools.
spec:
containers:
- name: debug
image: busybox
# Literal strip — command without trailing newline
command: ["sh", "-c"]
args:
- |-
echo "Starting diagnostics..."
nslookup kubernetes.default
curl -s -o /dev/null -w "%{http_code}" http://app-service:8080/health
echo "Diagnostics complete."GitHub Actions
Multi-step shell scripts in workflow steps:
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup and Test
# Literal style — multi-line shell script
run: |
echo "Installing dependencies..."
npm ci
echo "Running linter..."
npm run lint
echo "Running tests..."
npm test -- --coverage
echo "All checks passed!"
- name: Build Docker Image
run: |
docker build \
--build-arg NODE_ENV=production \
--build-arg VERSION=${{ github.sha }} \
-t myapp:${{ github.sha }} \
-t myapp:latest \
.
- name: Set environment variables
# Literal style — setting multi-line env vars
run: |
echo "DEPLOY_MESSAGE<<EOF" >> $GITHUB_ENV
echo "Deployed commit ${{ github.sha }}" >> $GITHUB_ENV
echo "Branch: ${{ github.ref_name }}" >> $GITHUB_ENV
echo "Author: ${{ github.actor }}" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Notify
if: failure()
uses: slackapi/slack-github-action@v1
with:
# Folded strip — JSON payload on one line
payload: >-
{
"text": "CI failed for ${{ github.repository }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Build failed on ${{ github.ref_name }}"
}
}
]
}Ansible Playbooks
Shell commands and templates in Ansible tasks:
# playbook.yml
---
- name: Deploy application
hosts: webservers
become: yes
tasks:
- name: Create deployment script
# Literal style — shell script content
copy:
dest: /opt/deploy.sh
mode: '0755'
content: |
#!/bin/bash
set -euo pipefail
APP_DIR="/opt/myapp"
BACKUP_DIR="/opt/backups"
echo "[$(date)] Starting deployment..."
# Backup current version
if [ -d "$APP_DIR" ]; then
cp -r "$APP_DIR" "$BACKUP_DIR/$(date +%Y%m%d_%H%M%S)"
fi
# Pull and restart
cd "$APP_DIR"
git pull origin main
docker compose down
docker compose up -d --build
echo "[$(date)] Deployment complete."
- name: Configure systemd service
copy:
dest: /etc/systemd/system/myapp.service
content: |
[Unit]
Description=My Application
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
[Install]
WantedBy=multi-user.target
- name: Display deployment summary
debug:
# Folded style — long message
msg: >
Deployment completed successfully on
{{ inventory_hostname }}. The application
is now running on port {{ app_port }}.
Check the logs at /var/log/myapp.log
for any issues.8. Flow Scalars: Plain, Single-Quoted, Double-Quoted
Besides block scalars (| and >), YAML also has flow scalars that handle multiline strings differently. These are important to understand for completeness.
Plain Scalars (no quotes)
Unquoted strings. Newlines are folded to spaces (like > style). Cannot contain special characters at the start (: # [ ] { } etc.).
# Plain scalar — no quotes, newlines folded to spaces
description: This is a long
string that spans multiple
lines in the YAML source.
# Result: "This is a long string that spans multiple lines in the YAML source."Single-Quoted Scalars
Enclosed in single quotes. Newlines are folded to spaces. No escape sequences (\n is literal). Use '' to include a literal single quote.
# Single-quoted — no escape sequences, newlines folded
message: 'This is a long
string with ''escaped'' single
quotes inside it.'
# Result: "This is a long string with 'escaped' single quotes inside it."
# Note: \n is NOT interpreted — it stays as literal characters
path: 'C:\Users\name\docs'
# Result: "C:\Users\name\docs" (backslashes are literal)Double-Quoted Scalars
Enclosed in double quotes. Newlines are folded to spaces. Supports escape sequences (\n, \t, \\, \", \uXXXX). Use \\ for literal backslash.
# Double-quoted — supports escape sequences
message: "Line one\nLine two\nLine three"
# Result: "Line one\nLine two\nLine three" (real newlines!)
# Newlines in source are folded to spaces
long_message: "This is a long
string that wraps across
lines in the source."
# Result: "This is a long string that wraps across lines in the source."
# Use \\ for literal backslash
path: "C:\\Users\\name\\docs"
# Result: "C:\Users\name\docs"
# Unicode escapes
emoji: "\u2764 \u2728"
# Result: "heart sparkles" (actual unicode characters)Flow scalar comparison:
| Style | Escape Sequences | Newline Handling | Special Characters |
|---|---|---|---|
| Plain | No | Folded to spaces | Limited (no : # at start) |
| Single-quoted | No | Folded to spaces | All allowed (use \'\') |
| Double-quoted | Yes | Folded to spaces | All allowed |
9. Decision Flowchart: Which Style to Use
Use this quick reference to pick the right multiline string style:
Q1: Does your string contain literal newlines that must be preserved?
Yes -> Use literal style |
No -> Continue to next question
Q2: Is it a long paragraph that you want to wrap for readability?
Yes -> Use folded style >
No -> Use a flow scalar (plain, single, or double quoted)
Q3: Do you need a trailing newline?
Yes -> Use clip (default) or keep (+)
No -> Use strip (-)
Q4: Does your content start with spaces?
Yes -> Add explicit indentation: |2 or >2
No -> Let YAML auto-detect indentation
Q5: Do you need escape sequences (\n, \t)?
Yes -> Use double-quoted flow scalar
No -> Use plain or single-quoted scalar
Quick summary:
# Shell scripts / code blocks -> | (literal)
# Long descriptions / paragraphs -> > (folded)
# No trailing newline needed -> add - (strip)
# Keep all trailing newlines -> add + (keep)
# Content starts with spaces -> add N (e.g., |2, >2)
# Single-line, no special chars -> plain scalar
# Need escape sequences (\n, \t) -> "double quoted"
# String with special YAML chars -> 'single quoted' or "double"- Shell scripts / code blocks -> | (literal)
- Long descriptions / paragraphs -> > (folded)
- Single-line with no special chars -> plain scalar
- String with special characters -> "double quoted"
- No trailing newline needed -> add - (strip)
10. FAQ
What is the difference between | and > in YAML?
| (literal) preserves all newlines exactly as written. > (folded) replaces single newlines with spaces, joining lines into a paragraph. Both add a single trailing newline by default. Use | for scripts and code; use > for long descriptions.
How do I remove the trailing newline from a YAML block scalar?
Add a strip indicator (-) after the style character: |- or >-. This removes ALL trailing newlines from the parsed string. For example, "message: |-\n Hello world" parses to "Hello world" (no trailing \n).
Can I mix | and > styles in the same YAML file?
Yes, absolutely. Each key can use a different scalar style. For example, you might use | for a shell script and > for a description in the same Kubernetes manifest or Docker Compose file.
What does the number mean in |2 or >2?
The number is an explicit indentation indicator. It tells the YAML parser how many spaces of indentation to strip from each line. This is useful when your content starts with spaces. For example, |2 means strip 2 spaces of leading indentation.
Why does my YAML multiline string have extra spaces or newlines?
Common causes: (1) Using > when you meant | (newlines get folded to spaces). (2) Forgetting the chomping indicator (default clip adds one trailing \n). (3) Inconsistent indentation within the block. (4) Extra blank lines at the end of the block. Use a YAML parser or validator to see exactly how your string is being interpreted.