Skip to content
datahacks.dev
YAML

YAML in Data Engineering Workflows and Where It Fits

Where YAML belongs in a data platform: tool config, contracts, schemas, pipelines, tests and docs, with a tested end to end example and the limits of each.

17 min read
Summarize in AI
Flat illustration of a tree of nested objects beside the document it came from, with the YAML logo.

A data platform is mostly files. A warehouse project holds model definitions and test declarations, a deployment holds container and workflow descriptions, a migration holds settings, a scheduler holds job definitions, and every tool in between wants some configuration. A large share of those files are YAML, partly by accident and partly because the format fits the job: it is readable, it takes comments, it diffs well in a pull request and every language can parse it. It also has sharp edges, and the work of using it well is knowing which jobs suit it and which do not. This guide maps the territory. It explains where YAML belongs in data engineering, shows one small end to end example that was run, and links to the detailed posts on each area. Treat it as the hub for the cluster: read it once for the shape of the problem, then follow the links for the parts you need.

What data engineers actually use YAML for

It helps to sort the uses by what the file is, because the right controls differ.

UseExampleWho edits itMain risk
Tool configurationdbt properties, Compose, CI workflows, Helm values, Liquibase settingsEngineers and analystsA typo that the tool ignores or misreads
Dataset definitionColumns, types, constraintsData ownersDrift from the real table
ContractRules a table must satisfyProducer and consumers togetherNobody runs it
Pipeline shapeSteps and dependenciesEngineersLogic creeping into strings
DocumentationDescriptions and ownersAnyoneStaleness
Small reference dataLookup tables, seed rowsAnalystsTypes changing silently
Content metadataFront matter, catalog entriesWritersMissing or mistyped fields

Notice what is not in the table: large datasets, secrets and business logic. The format is a poor carrier for the first (a million rows of YAML is slower and larger than the same rows in CSV, JSON Lines or Parquet), dangerous for the second and clumsy for the third. Most of the trouble teams have with YAML comes from asking it to be one of those three.

One file, five jobs

The idea that makes YAML pay off in a data platform is one source of truth feeding several consumers. A dataset described once can create the table, check the data, document the columns and detect drift. Here is the whole loop in a script that ran. The dataset file:

YAML
dataset: orders
description: One row per customer order.
owner: team-a
columns:
  - {name: id, type: integer, nullable: false, unique: true, description: Primary key.}
  - {name: status, type: varchar, nullable: false, allowed: [new, paid, shipped], description: Order state.}
  - {name: amount, type: "decimal(10,2)", nullable: false, min: 0, description: Total in euros.}

Five steps used it: generate the DDL, load three rows, check the contract, render the documentation line, compare with the live table. The output:

Text
1 DDL : create table "orders" ("id" integer not null, "status" varchar not null, "amount" decimal(10,2) not null)
2 load: 3 rows
3 contract: ['id: duplicated', "status: outside list ['refunded']", 'amount: below 0']
4 docs: | id:integer | status:varchar | amount:decimal(10,2) |
5 drift: []

The three test rows carry deliberate faults. The contract check found each: id 2 twice, a refunded status that is not on the list, an amount of -5. The drift check found nothing, because the table was created from the same file. That is the point and also the limit of the demo: when the table is generated from the file, drift cannot occur at creation; it occurs later, when someone changes the table by another route. Each step is a short function, and each has its own post with the edge cases:

Notice one design rule in the example: the quoting function ident() is used everywhere a name goes into SQL. The check functions in this demo write column names into statements, which is acceptable only because the file comes from your own repository. The SQL post explains why that assumption should be written down and tested.

The tools that already speak YAML

A great deal of data engineering YAML is not yours to design. The tool defines the schema and you fill it in. The skill is writing it so that the tool does what you meant, and reading what it did.

dbt. Model and source properties live in YAML: descriptions, tests, contracts, freshness. Writing dbt model properties in YAML covers the structure, defining dbt tests in YAML the test syntax and YAML sources and freshness checks in dbt the part that tells you when data stopped arriving. Once a project has many files, summarizing dbt project metadata from YAML reads the compiled manifest to report documentation coverage and untested models.

Containers. Compose files define the local database stack that most teams develop against. Docker Compose YAML for a database stack covers the file, and healthchecks and depends_on in Compose for databases covers the single setting most often missing: knowing the database is ready before the application starts. In production the same ideas appear as StatefulSets, ConfigMaps and Helm values, and checking Kubernetes YAML before you apply it is the habit that saves the most outages.

CI. Workflows that test migrations, run dbt and deploy are YAML as well. Writing GitHub Actions YAML that runs database tests shows the shape, and a workflow is also where your checks run, so it deserves the same review as the code it protects.

Migrations and infrastructure. Flyway and Liquibase settings in YAML and Ansible playbooks for database setup cover the files that change a database. Storing database connection config in YAML deals with the one setting every file shares, and with keeping passwords out of it.

The common advice for all of these is the same. Pin the tool version you tested with, because a key that exists in one release is ignored or rejected in the next. Run the tool's own validation in CI, and add a generic check on top for what the tool ignores. And when a tool offers a way to dump its effective configuration (docker compose config, dbt parse, helm template, ansible-inventory --list), read that output at least once. The file you wrote and the configuration the tool uses are different things, and the difference is where surprises live.

Moving data in and out of YAML

Data work needs conversion, since YAML is rarely the end of the road. A configuration written by an analyst in YAML has to become a table, and a table has to become a file someone can review. Converting YAML to JSON, CSV or XML covers the shape problems that arise when the structure is nested, and the same cluster has recipes for CSV, Markdown tables and spreadsheets. Round-tripping YAML without losing comments matters for tools that edit a file in place, and flattening nested YAML into rows is the step that turns configuration into something SQL can query.

Two lessons recur. First, YAML has more types than CSV, so a conversion must decide what to do with a date, a null or a list inside a cell. Decide once, write it down and test the round trip. Second, comments are lost in any conversion that goes through a plain data structure. If the comments matter, edit with a round-trip loader or a command-line tool such as yq, and treat the conversion as one way. The most common mistake is converting in both directions on a schedule and expecting the file to stay the same. It will not: key order, quoting style and comments will all drift, and the diffs will bury real changes.

YAML next to databases

There are three relationships between YAML and a database, and the posts keep them apart because the failure modes differ.

YAML describes the database. Schemas, seeds, fixtures and migrations. Loading YAML seed data into PostgreSQL covers loading reference rows in dependency order inside a transaction, and YAML fixtures for database tests covers the test-suite version of the same idea.

YAML is stored in the database. Sometimes a column holds a YAML document. Storing YAML text in a database column covers the choice between text, JSON and normalized tables, and what you lose by choosing text: you cannot index or constrain what the database cannot see into.

YAML is queried as data. Using DuckDB to query data stored as YAML shows how to ask questions of files without building a pipeline. The pattern that scales is converting to rows once and querying them, which analyzing configuration and metadata held in YAML works through for Compose fleets, Kubernetes audits, version drift and duplicate keys. When the files themselves are the thing you are replacing, migrating YAML data files into a database gives the procedure.

If a field in a file really holds a table's worth of rows, the question to ask is whether it should be a table.

Check every file before it runs

The cheapest bugs to catch are the ones in the file, before any job reads it. Build the checks in layers, each catching what the previous one cannot.

  1. Syntax. Does the file parse at all? yamllint or any parser answers it, and it catches tabs and bad indentation. Validating YAML before it breaks a deploy sets up this layer, and building a pre-commit check for YAML puts it where nobody can forget it.

  2. Structure and types. Are the keys the tool expects, with the right types? JSON Schema does this for any YAML file (validating YAML with JSON Schema), and a typed model in code does it for files your program reads. Catching type mistakes in YAML early covers the values that parse as the wrong type.

  3. Tool validation. Does the tool accept it? For Kubernetes, a dry run against the API server. For dbt, dbt parse. For Compose, docker compose config.

  4. Loading. Does the application's loader produce the configuration you meant after merging defaults and overrides? Testing config loading in CI shows how.

  5. Dead weight. Detecting unused keys in YAML config finds the settings that nothing reads, which is where a typo hides.

  6. Meaning. Does the data satisfy what the file promised? That is the contract check.

A file that passes layer 1 and fails layer 2 is common, and a file that passes layers 1 to 5 and fails layer 6 is the interesting one. Each layer costs seconds. The order matters for the people reading the output: a syntax error should stop the run before a schema complaint that is a consequence of it.

Security: the file is input

A YAML file in your repository is trusted. A YAML file from a user, a partner, an upload or another team is not, and the format has features that make that difference matter.

  • Code execution through the loader. Some loaders construct arbitrary objects from tags. Use the safe loader every time (YAML safe loaders in Python and Ruby).

  • Resource exhaustion. Aliases let a small file expand to a very large structure, and limiting alias expansion in a YAML parser explains the defense.

  • Uploads. Handling YAML uploads from users puts size limits, safe parsing and schema checks together.

  • Secrets. Secrets accidentally committed in YAML files is about finding them, and the better fix is not to put them there in the first place.

  • Injection downstream. The file is safe to parse and unsafe to paste into SQL. In the test for generating SQL from YAML templates safely, a job file with a crafted table name executed a stacked statement and dropped a table, and a crafted value counted every row. Bound values, driver-quoted identifiers and an allow-list fixed both.

A summary of the whole area is in YAML security pitfalls.

Pipelines described in YAML

Scheduling and orchestration tools often take a YAML description of steps and dependencies. The basic idea, a runner that validates the graph, orders it and executes each step, fits in about twenty lines (declaring pipelines in YAML for data jobs). Two questions decide whether the design survives.

Does it scale? Config driven pipelines and their limits measured this. A generated pipeline of 1,000 steps loaded in 20 ms and one of 10,000 steps in 243 ms with the C loader, and checking dependencies and ordering the graph added a small fraction of that. Size is not the problem. Expressiveness is: the first if: and the first for_each: mean the file has begun to contain logic.

Can you test it? Testing YAML driven jobs splits the runner into a pure planning function and a thin executor, tests the plan with examples and a property test, and shows that removing a validation makes a test fail. A pipeline you cannot test is a pipeline you edit nervously.

For larger systems the choice among tools matters more than the format. Dhall, CUE and Jsonnet for config at scale covers what to reach for when YAML needs functions and types.

Format choice

YAML is not the only way to write configuration, and part of good practice is knowing when to choose another. Choosing YAML, TOML, JSON or INI compares them. In short: JSON when machines write and read the file, TOML for a flat configuration with few nested structures, YAML when humans edit nested structures and comments matter. If you must keep YAML and want fewer surprises, StrictYAML and safer subsets removes the features behind most of the bugs.

The short list of things that go wrong

A modest set of parser behaviors causes most YAML incidents in data work, and they are the same in every tool. Learn them once.

  • Booleans from words. Under YAML 1.1 rules, no, yes, on and off become booleans, and so does the country code NO (the Norway problem). Quote the value, or choose a loader that follows 1.2.

  • Numbers from strings. 3.10 becomes 3.1, a leading zero may turn a code into an octal number, and 1e3 becomes a float (a version number turns into a float). Quote versions and identifiers.

  • Duplicate keys. A key written twice keeps the last value and says nothing (duplicate keys and silent overwrites). Run a linter that rejects them.

  • Tabs and indentation. The errors look like nonsense until you know a tab character is the cause.

  • Null versus empty. An empty value is null, which a database does not treat like an empty string.

  • Merge and anchors. Anchors reduce repetition and hide where a value came from (anchors and merge keys); in data files they are fine for repeated records and risky anywhere an override is easy to misread.

When something fails and the message is obscure, fixing YAML errors that make no sense is faster than guessing, and YAML problems worked from real files walks through five complete cases.

What a bad YAML day looks like

The abstract list above becomes concrete in a typical incident. A team adds a new environment by copying prod.yaml to eu.yaml. In the copy, someone edits the database host, forgets a second reference to the old host in a comment-adjacent block, and leaves retries: no meaning zero but written as a word. The file parses. The pipeline starts. The loader reads no as false, which the job interprets as "use the default of 3", and the first batch quietly reads from the US database while writing to the EU one. Nothing crashes. Two days later a finance report is off by a region.

Every layer of the checks above would have caught something. The schema would have rejected a boolean where an integer belongs. The duplicate-key and unused-key checks would have flagged the leftover. A diff of the effective configuration between prod and eu would have shown a line that references the wrong host. A contract on the output table would have noticed rows with the wrong region. No single layer is required, and the incident needed all of them to be absent. That is the argument for cheap, layered checks: any one of them, run automatically, turns a two-day silent error into a failed build.

Ownership and review

Files without owners rot. Give every dataset, pipeline and contract an owner field, review changes to them as code and have a rule for who may approve. The questions a reviewer asks of a YAML change differ from those for application code:

  • What does the tool do with this key, and which version of the tool is pinned?

  • Which environments does this change, and is the effective configuration for each as intended?

  • Does a downstream consumer depend on what was removed or renamed?

  • Is there a test or check that would have failed if this were wrong?

Reviewing YAML config changes in pull requests turns these into a checklist. A stable key order and consistent formatting make diffs readable, so format files automatically and fail the build when a file is not formatted. Reviewers who have to find the one real change among forty reflowed lines will miss it.

Environments and promotion

Most platforms run the same pipelines in several places: a laptop, a CI database, staging and production. The YAML question is how the files differ between them. Three patterns cover nearly every case, and each has a typical failure.

Separate full files per environment. dev.yaml, staging.yaml, prod.yaml, each complete. This is easy to read and easy to review, and the files drift apart. A key added to production and forgotten in staging means staging no longer tests what runs. The repository-wide duplicate and difference search from the analysis cluster finds this drift, and a CI step that fails when the key sets of two environment files differ stops it.

A base file plus small overrides. The base holds everything shared and each environment holds only what differs. Drift is limited, and the cost moves to the merge. The merge must be deep for mappings, and the rule for lists (replace or append) must be decided once. A shallow merge that replaces a whole db block because production overrides db.host loses the other settings without an error, which is one of the worked cases in YAML problems worked from real files.

Templates rendered per environment. Helm values or a Jinja pass produce the final files. This is the most flexible and the hardest to review, because the reviewer reads a template while the system runs its output. If you use templates, commit or print the rendered result in the pull request and run the validation layers against the rendered files and not the templates.

Whichever pattern you choose, promote the same artifact through environments and vary only configuration, and treat a change to production configuration as a deploy with a rollback plan. A pipeline file that passed in staging and reaches production through a hand edit is an unreviewed change.

What to measure

A YAML-heavy platform produces numbers that tell you whether the habits are working, and they are cheap to compute from the files themselves.

  • Validation coverage: the share of YAML files in the repository that some schema or tool validates in CI. Anything below all of them is a list of files with no safety net.

  • Documentation coverage: the share of dataset columns with a description, and the count of drift findings. The dbt manifest gives the first number for free.

  • Contract failures per week, split by rule. A rule that fails every night is either a real producer problem or a rule that should change. A rule that has never failed in a year may be testing nothing.

  • Time to load the largest file, recorded once and checked when the file doubles. The measurements in the pipeline post show loader choice dominating, so a regression usually means someone changed the loader.

  • Findings by owner. A finding with no owner is not going to be fixed, so report the files that have none.

None of these needs a dashboard to start. A script that prints them in CI and a spreadsheet that records the weekly values is enough to see a trend. The point of measuring is to decide where the next hour of effort goes, not to produce a score.

Where to start

A team new to this does not need all of the above at once. A sensible order, by return on effort:

  1. **Add yamllint to CI and a pre-commit hook.** An afternoon. It catches syntax, tabs and duplicate keys across every file.

  2. Validate the files your own code reads against a schema. A day per format. It catches typos and wrong types where they matter most.

  3. Render and diff the effective configuration of each environment for the tools that can print it. It catches the wrong-host class of error.

  4. Write contracts for the two or three tables other teams depend on, and run them at the producer.

  5. Document datasets and fail the build on drift, starting with the tables that have the most readers.

  6. Move logic out of any file that has grown conditions.

Each step stands alone and delivers value without the next. Stop when the remaining incidents are no longer about files.

When YAML is the wrong tool in a data platform

Large or fast data. Rows belong in tables, Parquet or CSV. YAML is slow to parse and verbose, and a type system that guesses is a poor fit for data that has a schema.

Logic. Conditions, loops, retries with backoff, computed values and string templating are code. A YAML file that needs them is a program with no tests.

Secrets. A value that grants access belongs in a secret store, referenced by name. Files get copied, pasted and committed.

Machine-to-machine exchange. When two programs talk, JSON with a schema or a binary format with a contract is a better fit than a format designed for people to type.

Files nobody edits by hand. If a tool writes the file and only tools read it, the readability argument vanishes.

Strong guarantees. If a single wrong value is costly, use a language with types and a compiler for the definition, and generate YAML as an output.

If none of those apply, YAML is a sensible default. The next step is the specific post for your problem. The format itself is defined in the YAML 1.2.2 specification, the worked-out checks use yamllint and DuckDB, and files can be tidied in the YAML formatter or converted in the YAML to JSON converter.

Also inMigrations
Taggedetlhub
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.