The Ops Atlas xOps School

Data & AI Emerging practice Sources verified

DataOps

DataOps is the practice of delivering and running data pipelines with software discipline: changes are versioned and tested in CI, each load is checked before consumers see it, and data is monitored after, so correct data arrives on time. The label is real but not standardised.

Key takeaways

  • No standard defines DataOps. The label dates from 2014–2015, a practitioner manifesto was online by 2017, and Gartner still called the tools market emerging in 2025. This page uses it for delivering and operating data pipelines.
  • Treat every published dataset as a release: change it through version control and CI, audit each load before consumers see it, and monitor it after.
  • Write-audit-publish is the central control: a bad load stays invisible instead of being rolled back in public.
  • Contracts at producer boundaries prevent silent upstream changes; tests and statistical monitors catch the rest.
  • Measure what consumers feel: freshness against an agreed time, correctness, and the share of incidents that consumers found before your monitors did.
  • Baselines must respect seasonality: in this page's lab, 3-sigma limits over all days missed a 19% weekday drop that a weekday-only baseline caught.
  • Access policy (DataSecOps), metric definitions (AnalyticsOps), engine internals (WorkflowOps) and model lifecycles (MLOps) are linked from here, not re-taught.

DataOps is the practice of delivering and running data pipelines with the discipline good teams apply to software. Every change to a pipeline is versioned, tested in continuous integration (CI) and released deliberately. Every load of data is checked before consumers can see it, and monitored after. The aim: the data people act on arrives correct and on time, and when it does not, the data team knows before its consumers do.

The label is real but not standardised. Lenny Liebmann named and described it in 2014, and Andy Palmer of Tamr defined it in 2015.[1][2] A manifesto stewarded by the vendor DataKitchen lists 18 principles, plus a later “+1”.[3] Gartner’s October 2025 Market Guide defines DataOps and still calls the tools market “emerging”.[4] An academic study found “no general accepted definition of DataOps yet”,[5] and in April 2026 English Wikipedia merged its DataOps article into its DevOps article.[6] This page uses the label for one coherent practice: data delivery run as a production line, with quality gates, statistical monitoring and producer-consumer contracts.

It is for data and analytics engineers, platform leads, architects and the leaders who pay for trustworthy data.

Item Summary
Goal Correct, complete data on an agreed schedule; failures caught before consumers see them; safe pipeline changes
Primary users Data and analytics engineers, platform teams, data product owners, and the people and services consuming data
Scope (this page) Version control and CI/CD for pipelines, environments for data, data tests, data observability (freshness, volume, schema, distribution, lineage), orchestration as used, data contracts, write-audit-publish, data incidents
Out of scope (linked) Who may see which data (DataSecOps); security telemetry (SecDataOps); research data stewardship (ResearchDataOps); metric definitions (AnalyticsOps); engine internals (WorkflowOps); databases as running systems (DBOps); models (MLOps)
Prerequisites SQL, Git and pull requests, what CI does; DevOps helps
Key outputs Tested pipelines, Slim CI, contracts for critical inputs, monitors, an incident process, a weekly scorecard
Term status Emerging. In use since about 2015, with a manifesto and a tools market, but no standard definition or standards body

Applications usually fail loudly; data pipelines usually fail quietly. A job reports success after loading half the rows. A producer renames a column or changes what a status code means. A late file makes yesterday’s partition (the day’s slice of a table) look complete. Nothing crashes: a dashboard shows a plausible wrong number and someone acts on it. Google’s SRE Workbook warns that every stage of a pipeline can report that all is well while data is lost between stages.[7] Without deliberate practice, the first monitor is a consumer. In 2019 an internal survey at Airbnb found that data scientists had trouble identifying which sources met their quality bar,[8] and at GoCardless engineers changed services without knowing that dropping a field could break dashboards downstream.[9]

Without DataOps With DataOps
Pipeline code edited in production; no review Every change goes through a pull request, CI and review
A load that finished is assumed correct Each load is audited before it is published
Upstream teams change schemas without warning Critical inputs have contracts with owners and versions
Consumers report errors; fixes overwrite history Monitors alarm first; loads are replayable; incidents name affected tables and periods

What DataOps is not: naming and boundaries

Link to section “What DataOps is not: naming and boundaries”
  • Not “DevOps for databases”. Running database engines is DBOps.
  • Not a product. Gartner reports DataOps features moving into larger data management platforms, so stand-alone DataOps solutions “risk being viewed as redundant”.[4] A tool does not create the practice.
  • Not governance or security (DataSecOps, PrivacyOps).
  • Not only the Manifesto’s analytics-team process (“Working analytics over comprehensive documentation”).[3] Centring this page on the pipeline, and the split of the data family below, are an editorial distinction of this Atlas, not a standard.
Page Object it manages Question it answers
DataOps (this page) The data pipeline Is the data arriving correct and on time?
DataSecOps Business data as a protected asset Who may see which data, and can we prove it?
SecDataOps Security telemetry Do defenders have complete, affordable evidence?
ResearchDataOps Research data over decades Can others find, cite and reuse it?
AnalyticsOps Metrics and dashboards Is there one trusted definition per metric?

Where DataOps comes from and where it sits

Link to section “Where DataOps comes from and where it sits”
When Milestone
June 2014 Liebmann (IBM blog): best practices that improve coordination between data science and operations[1]
May 2015 Palmer: “DataOps is a data management method that emphasizes communication, collaboration, integration, automation and measurement of cooperation between data engineers, data scientists and other data professionals.”[2]
By August 2017 The DataOps Manifesto is online with 18 principles (earliest Internet Archive capture)[10]
2018 Ereth’s review and expert interviews find no accepted definition[5]
2021 A research review calls DataOps “an emerging approach advocated by practitioners”[11]
October 2025 Gartner Market Guide: five capability sets, features converging into platforms[4]
2026 Wikipedia redirects “DataOps” to its DevOps article (April);[6] dbt v2.0 and ODCS v3.2.0 ship (September)[12][13]

The practices are older than the name: the Manifesto cites Agile, DevOps, Deming and lean manufacturing and asks for monitoring that detects “unexpected variation”,[3] the job of the manufacturing control chart.[14] Two readings coexist: an analytics-team reading (the Manifesto, Palmer) about collaborating to deliver insight, and a pipeline-operations reading (Gartner’s capability sets) about orchestrating, testing, deploying and observing data flows.[4] Vendor definitions blend them; IBM’s stresses collaboration, speed of delivery and quality.[15] This page teaches the pipeline reading.

Mental model: two loops (this page’s teaching device). In the change loop someone edits pipeline code, and CI and review protect it, as in DevOps. In the run loop the code stays the same but the data changes every hour or day: fewer rows, a new category value, a field whose meaning shifted. Data can break with no deploy at all, so DataOps checks every load and monitors every published table.

Term Meaning Example
Partition; backfill A slice of a table, usually one day, loaded and replaced as a unit; a backfill reruns past partitions Rebuild 1–15 October after a fix
CDC (change data capture) Copying each insert, update and delete from a source database’s log Checkout’s order changes stream to the raw zone
SLA, SLO, error budget SLA: a promise to consumers; SLO: the internal target; error budget: the misses an SLO allows (SRE) SLO 07:00 on 95% of business days: 3 late days a quarter
dbt model; materialisation A file holding one select; how dbt persists it (table, view, incremental…)[16][17] fct_orders.sql built as a table
Manifest, state, defer dbt’s manifest.json describes a run; --state points at a saved one; --defer reads unbuilt parents from it[18] --state prod-artifacts --defer
Dag Airflow’s workflow: tasks and the order they run in orders_daily: write → audit → publish
Data test An assertion that returns failing rows; zero rows means pass[19] No duplicate order_id today
Unit test (for data) Checks transformation logic on small fixed inputs[20] A refund yields a negative amount
Data contract Bitol: “an agreement between a data producer and its consumers”; schema, quality rules, service levels, owners[21] Checkout guarantees unique order_id, loaded by 06:00
Freshness (latency) Age of the newest data Newest order is 1.2 hours old
Distribution drift Values change while the schema does not Share of returned jumps from 3% to 21%
Lineage Which jobs read and write which datasets[22] fct_orders feeds three dashboards and a model
Write-audit-publish (WAP) Write to a hidden place, check, then expose[23] Iceberg audit branch fast-forwarded to main[24]
Idempotent load A rerun for a period gives the same result, never duplicates[25] Replace the 2026-10-15 partition
Slim CI Build and test only changed models and their children, reusing production for the rest[26] dbt build --select state:modified+ --defer --state prod-artifacts
Data downtime Vendor term: “periods of time when data is partial, erroneous, missing, or otherwise inaccurate”[27] 6 hours on 15 October
Figure 1. Where DataOps sits among neighbouring Atlas pages
practicesretriesCDC, schemasDevOps: CI/CD habitsWorkflowOps: enginesDBOps: source databasesDataOps: correct, on timeAnalyticsOps: metrics ontested tablesMLOps: training data onwardDataSecOps: access policy,using lineageSecDataOps: same tools,security dataResearchDataOps: afterpublication
Diagram source (Mermaid)
flowchart TB
  DEV["DevOps: CI/CD habits"]:::neutral
  WF["WorkflowOps: engines"]:::neutral
  DB["DBOps: source databases"]:::neutral
  D["DataOps: correct, on time"]:::core
  AN["AnalyticsOps: metrics on tested tables"]:::neutral
  ML["MLOps: training data onward"]:::neutral
  DS["DataSecOps: access policy, using lineage"]:::neutral
  SD["SecDataOps: same tools, security data"]:::neutral
  RD["ResearchDataOps: after publication"]:::neutral
  DEV -->|"practices"| D
  WF -->|"retries"| D
  DB -->|"CDC, schemas"| D
  D --> AN
  D --> ML
  D --> DS
  AN ~~~ SD
  ML ~~~ RD
  D -.-> SD
  D -.-> RD
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029

Text equivalent: DevOps supplies CI/CD habits, WorkflowOps engine behaviour (retries, backfills) and DBOps the source databases and their change data capture (CDC) feeds. DataOps hands tested tables to AnalyticsOps, training data to MLOps, and lineage and owners to DataSecOps, which sets access policy. Dotted links, drawn one row lower, are looser: SecDataOps uses the same tools for security data; ResearchDataOps takes over research data at publication.

Neighbour What the neighbour owns Handoff
DevOps General delivery practice, DORA metrics Applied to pipelines, plus data checks
WorkflowOps Engine semantics: retries, durable execution, backfill mechanics DataOps uses Airflow or Dagster; engine tuning goes there
DBOps Engine operations, migrations, backups Producer schema changes arrive through contracts
AnalyticsOps Semantic layer, metric definitions, dashboard certification DataOps guarantees the tables beneath
MLOps Features, training, serving, skew and drift Upstream training-data quality stays here
DataSecOps Classification, masking, row and column policy DataOps keeps personal data out of test outputs
ObservabilityOps Application telemetry pipelines Data observability stays here
IncidentOps Severity, roles, communication DataOps adds data diagnosis and backfill

The Manifesto addresses analytics teams.[3] This charter is this Atlas’s proposal for pipeline-centred DataOps.

Mission: data that people rely on is correct, complete and on time, and every change to it is deliberate and reversible.

  1. Every published dataset is a release: reviewed code, audited data.
  2. Consumers never see unaudited data for critical assets (write, audit, then publish).
  3. Checks fail closed where wrong data is expensive, and warn elsewhere. Severity is a decision, not a default.
  4. Producers own the shape of what they emit; consumers own their expectations. Contracts capture the first, tests the second.
  5. Every load is idempotent and replayable by period.
  6. Watch distributions, not only rules, to find failures nobody wrote a rule for.
  7. Measure what consumers feel: freshness, correctness, and who found the problem first.
  8. Environments for data are cheap and disposable (the Manifesto’s principle 12).

Non-goals: perfect data; a test on every column; one central team owning every pipeline; replacing governance.

Six building blocks recur in every implementation: version control and CI/CD; environments for data (CI schemas, table or lake branches); tests of logic, of each load and at boundaries; orchestration on time or on data; observability and lineage; and incident and change management (owners, severity, backfills, deprecation). The practices and tooling tables below fill each one in.

Airbnb gives every certified data model an engineering owner and a data science owner, and invites consuming teams to review designs.[8] The RACI below generalises that (R responsible, A accountable, C consulted, I informed). As charter principle 4 says, the producer owns the input contract it emits; the data product owner owns the published table’s contract.

Activity Producer team Pipeline owner Platform team Data product owner Consumers
Define, change or deprecate an input contract A/R C I C I
Define, change or deprecate the published-table contract I R C A C
Maintain tests on a published table I R C A C
Provide CI, environments, orchestrator, monitors I C A/R I I
Respond to a tier-1 data incident C R C A I
Run a backfill after a fix I R C A I

Skills: SQL, Git, an orchestrator, a test framework, basic statistics and incident handling; platform teams add infrastructure as code and cost awareness (FinOps).

  • Day 0, design: tier, owners, consumers and deadline; contracts for critical inputs; a design spec sized to the tier (Airbnb’s specs cover metrics, pipeline, service levels, backfill, schemas and checks; some complex ones exceed 20 pages).[8]
  • Day 1, release: build in CI, backfill history, run old and new side by side for critical assets, publish behind write-audit-publish, announce the service level.
  • Day 2, operate: monitor every load, handle incidents, review contract changes, tune thresholds, retire unused assets.

Tiers keep cost proportional to harm:

Tier Example Required controls
1, critical Finance close, regulatory returns, training sets, customer-facing data Input contracts, write-audit-publish with blocking checks, freshness SLO, on-call, incident and change review
2, important Team dashboards, weekly reporting Key tests, freshness and volume monitors, owner, visible warnings
3, exploratory Sandboxes, one-off analysis Version control; no service level; labelled unsupported

Ceremonies: a weekly reliability review of the scorecard, a contract change review, and a blameless review of each tier-1 incident (IncidentOps).

Reference architecture: data plane and control plane

Link to section “Reference architecture: data plane and control plane”

The data plane moves data; the control plane decides what runs, checks the output and alerts people. Many failures are a missing link between them, such as checks that cannot stop a publish.

Figure 2. Reference architecture: a data plane governed by a control plane
checks passdeploysrunsruleswatchesalertsimpactSources: apps, SaaS, filesIngest: CDC, batch, streamRaw zone, append-onlyTransform: SQL modelsAudit area, hiddenPublished tablesConsumers: BI, ML, servicesGit and CIOrchestratorContractsMonitorsLineage, metadataOn-call, incidents
Diagram source (Mermaid)
flowchart TB
  S["Sources: apps, SaaS, files"]:::neutral
  I["Ingest: CDC, batch, stream"]:::neutral
  R["Raw zone, append-only"]:::neutral
  T["Transform: SQL models"]:::core
  A["Audit area, hidden"]:::core
  P["Published tables"]:::core
  C["Consumers: BI, ML, services"]:::neutral
  G["Git and CI"]:::decision
  O["Orchestrator"]:::decision
  K["Contracts"]:::decision
  M["Monitors"]:::decision
  L["Lineage, metadata"]:::decision
  N["On-call, incidents"]:::risk
  S --> I --> R --> T --> A -->|"checks pass"| P --> C
  G -.->|"deploys"| O
  O -.->|"runs"| T
  K -.->|"rules"| A
  M -.->|"watches"| P
  M -.->|"alerts"| N
  L -.->|"impact"| N
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029

Text equivalent: the data plane runs top to bottom: sources, ingestion, an append-only raw zone, SQL transformations, a hidden audit area and, only after checks pass, published tables that consumers read. Control-plane components (amber, dotted links): Git and CI deploy to the orchestrator, which runs transformations; contracts supply audit rules; monitors watch published tables and alert the on-call owner (red); lineage shows which assets an incident affects.

  • Keep raw data immutable so any period can be replayed after a fix.
  • Separate audit from publish: a staging schema plus an atomic swap, an Apache Iceberg audit branch fast-forwarded to main after validation,[24] or a lakeFS branch merged after checks.[28]
  • Trigger on data, not only on time. Airflow marks an asset as updated “only if the task completes successfully”, so a failed publish does not trigger consumers.[29]
  • Emit lineage in a standard form. OpenLineage models jobs, runs and datasets, extended by “facets”.[22]

End-to-end workflow: from pull request to published table

Link to section “End-to-end workflow: from pull request to published table”
Figure 3. The change loop: from pull request to published data, with the exception paths
noyesyesnoPull request openedCI builds changed modelsand their children in a PRschemaUnit, data and contractchecks pass?Breaking change to apublished contract?Author fixes; CI rerunsNew contract version,deprecation window,consumer reviewOwner review and mergeDeploy to the orchestratorNext run: write, audit, publish
Diagram source (Mermaid)
flowchart TB
  PR["Pull request opened"]:::neutral
  CI["CI builds changed models and their children in a PR schema"]:::core
  T{"Unit, data and contract checks pass?"}:::decision
  BR{"Breaking change to a published contract?"}:::decision
  FIX["Author fixes; CI reruns"]:::risk
  VER["New contract version, deprecation window, consumer review"]:::risk
  REV["Owner review and merge"]:::neutral
  DEP["Deploy to the orchestrator"]:::core
  RUN["Next run: write, audit, publish"]:::core
  PR --> CI --> T
  T -->|"no"| FIX --> CI
  T -->|"yes"| BR
  BR -->|"yes"| VER --> REV
  BR -->|"no"| REV
  REV --> DEP --> RUN
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029

Text equivalent: a pull request triggers CI, which builds only the changed models and their children in a pull-request schema. Failed checks return the change to the author. If checks pass but the change breaks a published contract, a new contract version with a deprecation window goes to consumers for review. The owner merges, the change is deployed, and the next run writes, audits and publishes.

Plan for the exception paths:

  1. CI is red because data changed, not code. Keep logic checks (unit tests on fixtures, which dbt Labs recommends running only in development and CI)[20] separate from checks on today’s data.
  2. A breaking change is unavoidable. Publish the new shape beside the old with a dated window. GoCardless made changing a contract “intentionally heavy”.[9]
  3. Tonight’s run fails its audit. Consumers keep yesterday’s data; see the runbook.
Figure 4. Write-audit-publish for one daily load
PublishedAudit areaOrchestratorChecksasset marked updatedlast good data stays, owner pagedalt[blocking checks pass][a blocking check fails]write partition (hidden)run checks on the partitionpasspublishatomic swap or fast-forwardfail + failing rowskeep as quarantine
Diagram source (Mermaid)
sequenceDiagram
  participant Q as Checks
  participant O as Orchestrator
  participant A as Audit area
  participant P as Published
  O->>A: write partition (hidden)
  O->>Q: run checks on the partition
  alt blocking checks pass
    Q-->>O: pass
    O->>A: publish
    A->>P: atomic swap or fast-forward
    Note over O: asset marked updated
  else a blocking check fails
    Q-->>O: fail + failing rows
    O->>A: keep as quarantine
    Note over A,P: last good data stays, owner paged
  end

Text equivalent: the orchestrator’s load job writes one day’s partition into a hidden audit area, then asks the checks to run on it. If every blocking check passes, the orchestrator publishes: the audit area replaces the published data in one atomic step (a table swap or a branch fast-forward), and the asset is marked updated so consumers can run. If one fails, the checks return the failing rows, the audit area is kept as a quarantine, the last good data stays published and the owner is paged.

A subtlety: dbt build skips downstream models when a test fails,[30] but the tested model has already been rebuilt in its target schema, so anyone querying it directly sees the bad version (an inference from the documented order of operations). If consumers query the model itself, build into an audit schema and publish separately, as in Lab 2.

Practice Goal Trigger Owner Verification
Slim CI Catch logic and contract breaks before merge Pull request Pipeline owner Escaped defects fall
Unit tests Prove tricky logic[20] New logic, past bugs Pipeline owner Fail when logic is broken on purpose
Data tests Keys, nulls, accepted values, relationships Every load Pipeline owner Thresholds documented
Contracts Stop silent upstream changes New producer, breaking change Producer Checked in producer CI
Write-audit-publish No unaudited tier-1 data Every tier-1 load Pipeline owner Zero unaudited publishes
Monitors Late, partial or shifted data Every load, schedule Platform team Coverage, precision
Idempotent loads Safe reruns and backfills Retries, fixes Pipeline owner Reruns reproduce counts
Incident handling Contain, fix, inform Alert, consumer report On-call owner Detect and restore times fall
Late-data handling Late rows land in their own period Every incremental run Pipeline owner Restated totals reconcile with source

Late and out-of-order data. A load can be on time while its data is not. Reconcile each partition with the source’s own count; reprocess a lookback window (dbt’s incremental example re-reads the last three days for late-arriving records[31]); and tell consumers when a period is final. Stream processors use event-time watermarks, a declaration that events up to a timestamp should have arrived; later events need an explicit policy.[32]

None is mandatory in general; regulation may make parts so (compliance).

Item Kind What it gives DataOps
DataOps Manifesto Vendor-stewarded manifesto 18 principles plus a later “+1”, including reproducibility and monitoring for unexpected variation[3]
Open Data Contract Standard v3.2.0 Open standard (Bitol, Linux Foundation AI & Data) YAML for schema, quality rules, service levels, roles[13][21]
OpenLineage Open standard Run, job and dataset events; Marquez is the reference implementation[22]
ISO/IEC 25012 International standard 15 data quality characteristics, such as accuracy and completeness[33]
SRE guidance for pipelines Engineering practice Freshness and correctness SLOs; end-to-end measurement[7]
Statistical process control Statistical method Control charts, usually with 3-sigma limits[14]
Data mesh Organisational approach Domain ownership, data as a product, self-serve platform, federated governance[34]

Gartner groups DataOps tools into generalists, orchestrators and specialists, across five capability sets: pipeline orchestration, observability, test automation, deployment automation and environment management.[4] Choose by capability first:

Capability Examples checked for this page (October 2026) Watch out for
Transformation, tests, contracts dbt v1.12 and v2.0: data tests, unit tests, model contracts, freshness[12][35] Contracts cover only some materialisations; tests cost warehouse compute
Validation frameworks Great Expectations (GX Core 1.24), Soda Core v4 contracts, Deequ on Spark[36][37][38] Another runtime to operate; Python version floors
Orchestration Airflow 3.3 assets and Deadline Alerts; Dagster asset checks and freshness policies[39][40][41] Airflow Deadline Alerts are experimental (since 3.1) and Dagster freshness policies in preview; engine semantics belong to WorkflowOps
Environments, versioned data Iceberg branches and tags; lakeFS branches[24][28] Branch retention and clean-up
Observability and lineage OpenLineage with Marquez; commercial data observability platforms[22][27] Heavy overlap with platforms’ built-in features[4]
Streaming contracts Schema Registry compatibility rules (Confluent default: BACKWARD);[42] Confluent data contracts add metadata, plus CEL rules on paid tiers (7.4+)[43] Compatibility checks are structural, not semantic

Ask: can it block a publish? Where does it store failing rows? What does each check cost? Can it emit OpenLineage? How hard is it to leave? Gartner advises checking whether your main platform’s embedded features are “good enough” first.[4]

The useful AI here is mostly statistics. Uber’s Data Quality Monitor bundles table metrics into principal components (“the top-ranked PC series explain more than 90 percent of the variation”) and flags deviations from one-step-ahead forecasts.[44] Deequ uses machine learning to enhance constraint suggestions and to detect anomalies in quality time series,[38] and Google’s data validation in TFX checks data fed to ML pipelines against a schema[45] (its training/serving skew checks belong to MLOps). Generative assistants now draft tests (dbt documents one).[19] A generated test describes the data as it is, not as it should be, so someone who knows the business must review it.

Assumptions: a warehouse supported by dbt, Git with CI, Airflow 3.x, and one tier-1 asset, fct_orders, fed by the checkout team’s orders table (versions as of October 2026: dbt Core 1.12, Airflow 3.3, ODCS v3.2.0).

data-platform/
  contracts/orders.odcs.yaml        # producer-owned contract (ODCS v3.2.0)
  models/staging/_sources.yml       # sources, freshness, source tests
  models/staging/stg_orders.sql
  models/marts/_marts.yml           # contract, data tests, unit tests
  models/marts/fct_orders.sql
  dags/orders_daily.py              # write -> audit -> publish
  .github/workflows/dbt-ci.yml      # Slim CI on pull requests

1. The contract, owned by the producer and reviewed by consumers. Metrics such as nullValues, SLA properties such as latency (which ODCS prefers to “freshness”) and the {object} and {property} placeholders in SQL rules come from the standard.[13]

Example status: validated on 2026-10-17 against the ODCS v3.2.0 JSON Schema with Ajv 8.17.1 in Draft 2019-09 mode (rejects unknown keys); executed in Lab 2.

apiVersion: v3.2.0
kind: DataContract
id: 7f1c2a4e-0b8d-4d55-9a51-2f3c8e0d9b11
name: orders
version: 1.2.0
status: active
domain: checkout
description:
  purpose: One row per customer order, loaded daily for finance and analytics.
  limitations: Amounts are in the order currency; no FX conversion.
  usage: Join to customers on customer_id. Do not use for real-time stock decisions.
schema:
  - name: orders
    physicalName: orders
    logicalType: object
    physicalType: table
    quality:
      - id: orders_daily_volume
        metric: rowCount
        mustBeGreaterThan: 500
        dimension: completeness
        description: A normal day has 900 to 1,100 orders; fewer than 500 means a broken load.
    properties:
      - name: order_id
        logicalType: string
        physicalType: varchar(36)
        primaryKey: true
        primaryKeyPosition: 1
        required: true
        quality:
          - id: order_id_unique
            metric: duplicateValues
            mustBe: 0
            dimension: uniqueness
      - name: customer_id
        logicalType: string
        physicalType: varchar(36)
        required: true
        quality:
          - id: customer_id_not_null
            metric: nullValues
            mustBe: 0
            dimension: completeness
      - name: status
        logicalType: string
        physicalType: varchar(16)
        required: true
        quality:
          - id: status_accepted
            metric: invalidValues
            arguments:
              validValues: [placed, shipped, completed, returned]
            mustBe: 0
            dimension: conformity
      - name: amount
        logicalType: number
        physicalType: decimal(12,2)
        required: true
        quality:
          - id: amount_not_negative
            type: sql
            query: SELECT COUNT(*) FROM {object} WHERE {property} < 0
            mustBe: 0
            dimension: accuracy
      - name: loaded_at
        logicalType: timestamp
        physicalType: timestamp
        required: true
team:
  name: checkout-platform
  members:
    - username: owner@example.com
      role: owner
slaProperties:
  - id: orders_latency
    property: latency
    value: 6
    unit: h
    element: orders.loaded_at
  - id: orders_frequency
    property: frequency
    value: 1
    unit: d
    element: orders.loaded_at
  - id: orders_time_to_detect
    property: timeToDetect
    value: 30
    unit: min

2. dbt sources and marts: freshness on the input; an enforced contract, data tests with explicit severities and a unit test on the output.[46][35][47] The arguments: key needs dbt 1.10.5 or later.[19]

Example status: YAML syntax-checked; written for dbt Core 1.12 against the documentation; not executed here (see Lab 3).

# models/staging/_sources.yml
sources:
  - name: checkout
    schema: raw_checkout
    config:
      freshness:
        warn_after: {count: 6, period: hour}
        error_after: {count: 12, period: hour}
      loaded_at_field: loaded_at
    tables:
      - name: orders
        columns:
          - name: order_id
            data_tests: [unique, not_null]
---
# models/marts/_marts.yml
models:
  - name: fct_orders
    config:
      contract: {enforced: true}
    columns:
      - name: order_id
        data_type: varchar
        constraints: [{type: not_null}]
        data_tests: [unique]
      - name: customer_id
        data_type: varchar
        data_tests:
          - relationships:
              arguments: {to: ref('dim_customers'), field: customer_id}
              config: {severity: warn}
      - name: status
        data_type: varchar
        data_tests:
          - accepted_values:
              arguments: {values: [placed, shipped, completed, returned]}
              config: {severity: error, error_if: ">10", warn_if: ">0"}
      - name: net_amount
        data_type: decimal(12,2)
unit_tests:
  - name: returned_orders_have_negative_net_amount
    model: fct_orders
    given:
      - input: ref('stg_orders')
        rows:
          - {order_id: o1, customer_id: c1, status: returned, amount: 40.00}
          - {order_id: o2, customer_id: c1, status: completed, amount: 15.50}
    expect:
      rows:
        - {order_id: o1, net_amount: -40.00}
        - {order_id: o2, net_amount: 15.50}

3. The orchestration: write, audit and publish as separate tasks; an asset so consumer Dags run only after a successful publish;[29] and a Deadline Alert (SLAs were removed in Airflow 3[48]) that pages the owner if the run has not finished an hour after it was scheduled to start. Deadline Alerts “are new in Airflow 3.1 and should be considered experimental” (as of October 2026),[39] so tier 1 also needs a monitor outside Airflow that pages if fct_orders has no new partition by 06:00; it also catches a stalled scheduler. Credentials stay out of the Dag: profiles.yml reads them with dbt’s env_var() (values of DBT_ENV_SECRET variables are scrubbed from logs),[49] set by the worker’s deployment or secrets backend, and BashOperator passes the worker’s environment on by default.[50]

Example status: Python syntax-checked; imports and parameters taken from the Airflow 3.3 documentation; not executed (requires an Airflow deployment and a warehouse).

"""orders_daily.py: write -> audit -> publish for fct_orders (Airflow 3.x)."""
from datetime import datetime, timedelta

from airflow.providers.slack.notifications.slack_webhook import SlackWebhookNotifier
from airflow.providers.standard.operators.bash import BashOperator
from airflow.sdk import DAG, Asset, AsyncCallback, DeadlineAlert, DeadlineReference

FCT_ORDERS = Asset("warehouse://analytics/fct_orders")
DBT = "cd /opt/data-platform && dbt"

with DAG(
    dag_id="orders_daily",
    schedule="0 5 * * *",
    start_date=datetime(2026, 10, 1),
    catchup=False,
    deadline=DeadlineAlert(
        reference=DeadlineReference.DAGRUN_LOGICAL_DATE,
        interval=timedelta(hours=1),
        callback=AsyncCallback(
            SlackWebhookNotifier,
            kwargs={"text": "{{ dag_run.dag_id }} missed its deadline "
                            "({{ deadline.deadline_time }})"},
        ),
    ),
):
    # No credentials here: profiles.yml uses env_var("DBT_ENV_SECRET_..."); BashOperator
    # (env=None) inherits the worker environment set by the deployment or secrets backend.
    write = BashOperator(
        task_id="write_to_audit_schema",
        bash_command=f"{DBT} source freshness --select source:checkout "
                     f"&& {DBT} build --select +fct_orders --exclude test_type:unit --target audit",
    )
    audit = BashOperator(
        task_id="audit_contract",
        bash_command="python /opt/data-platform/ci/verify_contract.py "
                     "contracts/orders.odcs.yaml --target audit --date {{ ds }}",
    )
    publish = BashOperator(
        task_id="publish",
        bash_command="python /opt/data-platform/ci/publish_partition.py fct_orders --date {{ ds }}",
        outlets=[FCT_ORDERS],
    )
    write >> audit >> publish

verify_contract.py and publish_partition.py are your own small scripts (Lab 2 shows the logic); the second replaces the day’s partition in one transaction, so a retry never duplicates rows, as Airflow advises for tasks.[25]

Labs 1 and 2 need only Python and were executed for this page; Lab 3 needs Python 3.10 or later and was not executed.

Lab 1 (beginner): checks and a control chart on one table. Objective: write the checks DataOps runs on every load and see why a baseline must respect seasonality. Prerequisites: Python 3.8+, no packages; about 20 minutes; local files only, no cost.

Example status: executed on Python 3.8.10 with SQLite 3.31.1 on 2026-10-16 and re-run on 2026-10-17; the output below is verbatim.

  1. Save the generator: 28 daily loads (about 1,000 orders on weekdays, 820 at weekends); the last load has five defects.
"""make_orders.py: build shop.db with 28 daily loads of an orders table; day 28 carries five defects."""
import random
import sqlite3
from datetime import datetime, timedelta

random.seed(42)
db = sqlite3.connect("shop.db")
db.executescript("""
DROP TABLE IF EXISTS customers;
DROP TABLE IF EXISTS orders;
CREATE TABLE customers (customer_id TEXT PRIMARY KEY);
CREATE TABLE orders (order_id TEXT, customer_id TEXT, status TEXT,
                     amount REAL, load_date TEXT, loaded_at TEXT);
""")
db.executemany("INSERT INTO customers VALUES (?)", [(f"c{n:04d}",) for n in range(2000)])
start = datetime(2026, 9, 18, 5, 0)
statuses = ["placed", "shipped", "completed", "returned"]
seq = 0
for day in range(28):
    load = start + timedelta(days=day)
    weekend = load.weekday() >= 5
    n = int(random.gauss(820 if weekend else 1000, 30))
    if day == 27:
        n = 610                                   # defect 1: a partial load
    rows = []
    for _ in range(n):
        seq += 1
        rows.append([f"o{seq:06d}", f"c{random.randrange(2000):04d}", random.choice(statuses),
                     round(random.uniform(5, 400), 2), load.date().isoformat(),
                     (load + timedelta(minutes=random.randrange(50))).isoformat(" ")])
    if day == 27:
        rows[0][1] = None                         # defect 2: missing customer_id
        rows[1][1] = None
        rows[2][2] = "refunded_partial"           # defect 3: a status nobody agreed on
        rows[3][1] = "c9999"                      # defect 4: orphan customer
        rows.append(list(rows[4]))                # defect 5: a duplicated order
    db.executemany("INSERT INTO orders VALUES (?,?,?,?,?,?)", rows)
db.commit()
print(db.execute("SELECT COUNT(*), MIN(load_date), MAX(load_date) FROM orders").fetchone())
  1. Save the checker. Errors block publication. The volume checks compare today’s count with the mean ± 3 standard deviations of history,[14] over all days and over weekdays only.
"""check_orders.py: run DataOps checks on the latest load of shop.db; exit 1 if an error-level check fails."""
import sqlite3
import statistics
import sys
from datetime import datetime

NOW = datetime(2026, 10, 15, 7, 0)       # pin "now" so the lab is repeatable
db = sqlite3.connect("shop.db")
latest = db.execute("SELECT MAX(load_date) FROM orders").fetchone()[0]
day = f"load_date = '{latest}'"           # internal constant, not user input

def scalar(sql):
    return db.execute(sql).fetchone()[0]

checks = []  # (name, severity, failures, detail)

age_h = (NOW - datetime.fromisoformat(scalar("SELECT MAX(loaded_at) FROM orders"))).total_seconds() / 3600
checks.append(("freshness <= 6 h", "error", int(age_h > 6), f"newest row is {age_h:.1f} h old"))

counts = [r[0] for r in db.execute(
    "SELECT COUNT(*) FROM orders GROUP BY load_date ORDER BY load_date")]
history, today = counts[:-1], counts[-1]
mean, sd = statistics.mean(history), statistics.stdev(history)
low, high = mean - 3 * sd, mean + 3 * sd
checks.append(("volume within mean +/- 3 sd", "error", int(not low <= today <= high),
               f"today {today}, limits {low:.0f}..{high:.0f} from {len(history)} days"))
# Same check against weekdays only: seasonality makes the all-days limits too wide.
wk = [c for (d, c) in db.execute("SELECT load_date, COUNT(*) FROM orders GROUP BY load_date")
      if d != latest and datetime.fromisoformat(d).weekday() < 5]
wl, wh = statistics.mean(wk) - 3 * statistics.stdev(wk), statistics.mean(wk) + 3 * statistics.stdev(wk)
checks.append(("volume vs weekdays only", "error", int(not wl <= today <= wh),
               f"limits {wl:.0f}..{wh:.0f} from {len(wk)} weekdays"))

checks.append(("customer_id not null", "error",
               scalar(f"SELECT COUNT(*) FROM orders WHERE {day} AND customer_id IS NULL"), "rows"))
checks.append(("order_id unique", "error", scalar(
    f"SELECT COUNT(*) FROM (SELECT order_id FROM orders WHERE {day} "
    "GROUP BY order_id HAVING COUNT(*) > 1)"), "duplicated ids"))
checks.append(("status in accepted set", "warn", scalar(
    f"SELECT COUNT(*) FROM orders WHERE {day} AND status NOT IN "
    "('placed','shipped','completed','returned')"), "rows"))
checks.append(("customer_id exists in customers", "warn", scalar(
    f"SELECT COUNT(*) FROM orders o LEFT JOIN customers c USING (customer_id) "
    f"WHERE o.{day} AND o.customer_id IS NOT NULL AND c.customer_id IS NULL"), "orphans"))

print(f"checks for load_date {latest}")
worst = 0
for name, sev, fails, detail in checks:
    status = "PASS" if fails == 0 else sev.upper()
    worst = max(worst, 2 if status == "ERROR" else 1 if status == "WARN" else 0)
    print(f"  {status:5}  {name:34} {fails:>4}  {detail}")
print("verdict:", ["publish", "publish with warning", "BLOCK: do not publish"][worst])
sys.exit(1 if worst == 2 else 0)
  1. Run python3 make_orders.py && python3 check_orders.py; echo "exit code: $?". Expected output:
(26453, '2026-09-18', '2026-10-15')
checks for load_date 2026-10-15
  PASS   freshness <= 6 h                      0  newest row is 1.2 h old
  ERROR  volume within mean +/- 3 sd           1  today 611, limits 695..1219 from 27 days
  ERROR  volume vs weekdays only               1  limits 913..1106 from 19 weekdays
  ERROR  customer_id not null                  2  rows
  ERROR  order_id unique                       1  duplicated ids
  WARN   status in accepted set                1  rows
  WARN   customer_id exists in customers       1  orphans
verdict: BLOCK: do not publish
exit code: 1
  1. Make the partial load subtler: change n = 610 to n = 820 and run both scripts again. Executed result (first lines of the checker’s output): weekend history widens the all-days limits, so only the weekday baseline catches a load about 19% below a normal weekday.
checks for load_date 2026-10-15
  PASS   freshness <= 6 h                      0  newest row is 1.2 h old
  PASS   volume within mean +/- 3 sd           0  today 821, limits 695..1219 from 27 days
  ERROR  volume vs weekdays only               1  limits 913..1106 from 19 weekdays

Validation: run 1 exits with code 1; run 2 shows the volume checks disagreeing. Troubleshooting: other counts mean the seed was changed. Teardown: delete shop.db. Take-away: mean ± 3 sd assumes one stable process; a weekly cycle is two. Compare like with like, or forecast as Uber does.[44]

Lab 2 (intermediate): contract-driven write-audit-publish. Objective: audit a batch against the ODCS contract from the reference implementation, publish or quarantine it, and prove a rerun does not duplicate data. Prerequisites: Python 3.8+ with PyYAML; the contract saved as orders.odcs.yaml; about 20 minutes; local files.

Example status: executed twice on Python 3.8.10 with PyYAML 5.3.1 on 2026-10-16 and again on 2026-10-17; the output below is verbatim.

"""wap.py: write-audit-publish for one daily batch, audited against an ODCS v3.2.0 contract (subset)."""
import random
import sqlite3
import sys
import yaml  # PyYAML

contract = yaml.safe_load(open("orders.odcs.yaml"))
obj = contract["schema"][0]
TABLE, AUDIT = obj["physicalName"], obj["physicalName"] + "__audit"
COLS = [p["name"] for p in obj["properties"]]
db = sqlite3.connect("warehouse.db")
db.execute(f"CREATE TABLE IF NOT EXISTS {TABLE} ({', '.join(COLS)})")
db.execute("CREATE TABLE IF NOT EXISTS publish_log (load_date, outcome, detail)")

def metric_sql(rule, column):
    """Translate ODCS library metrics (and sql rules) into a failure-count query on the audit table."""
    m = rule.get("metric")
    if rule.get("type") == "sql":  # ODCS: {object} = table, {property} = column
        sql = rule["query"].replace("{object}", AUDIT)
        return sql.replace("{property}", column) if column else sql
    if m == "rowCount":
        return f"SELECT COUNT(*) FROM {AUDIT}"
    if m == "nullValues":
        return f"SELECT COUNT(*) FROM {AUDIT} WHERE {column} IS NULL"
    if m == "duplicateValues":
        return f"SELECT COUNT(*) - COUNT(DISTINCT {column}) FROM {AUDIT}"
    if m == "invalidValues":
        allowed = ", ".join("'" + v.replace("'", "''") + "'" for v in rule["arguments"]["validValues"])
        return f"SELECT COUNT(*) FROM {AUDIT} WHERE {column} NOT IN ({allowed})"
    raise ValueError(f"unsupported rule {rule}")

def passes(rule, value):
    ops = {"mustBe": lambda v, t: v == t, "mustBeGreaterThan": lambda v, t: v > t,
           "mustBeLessThan": lambda v, t: v < t}
    return all(fn(value, rule[op]) for op, fn in ops.items() if op in rule)

def audit():
    rules = [(r, None) for r in obj.get("quality", [])]
    rules += [(r, p["name"]) for p in obj["properties"] for r in p.get("quality", [])]
    failures = []
    for rule, column in rules:
        value = db.execute(metric_sql(rule, column)).fetchone()[0]
        if not passes(rule, value):
            failures.append(f"{rule['id']}={value}")
    return failures

def run(load_date, rows):
    db.execute(f"DROP TABLE IF EXISTS {AUDIT}")
    db.execute(f"CREATE TABLE {AUDIT} AS SELECT * FROM {TABLE} WHERE 0")      # WRITE (hidden)
    db.executemany(f"INSERT INTO {AUDIT} VALUES ({', '.join('?' * len(COLS))})", rows)
    failures = audit()                                                         # AUDIT
    with db:                                                                   # one transaction
        if failures:
            quarantine = f"{TABLE}__quarantine_{load_date.replace('-', '')}"
            db.execute(f"DROP TABLE IF EXISTS {quarantine}")
            db.execute(f"ALTER TABLE {AUDIT} RENAME TO {quarantine}")
            db.execute("INSERT INTO publish_log VALUES (?, 'quarantined', ?)", (load_date, ", ".join(failures)))
        else:
            # Idempotent publish: replace the whole partition, never append to it.
            db.execute(f"DELETE FROM {TABLE} WHERE substr(loaded_at, 1, 10) = ?", (load_date,))
            db.execute(f"INSERT INTO {TABLE} SELECT * FROM {AUDIT}")             # PUBLISH
            db.execute(f"DROP TABLE {AUDIT}")
            db.execute("INSERT INTO publish_log VALUES (?, 'published', ?)", (load_date, f"{len(rows)} rows"))
    print(f"{load_date}: {'QUARANTINED ' + ', '.join(failures) if failures else 'published'}")

def batch(load_date, n, broken=False):
    random.seed(load_date)
    rows = [[f"{load_date}-{i:05d}", f"c{random.randrange(2000):04d}",
             random.choice(["placed", "shipped", "completed", "returned"]),
             round(random.uniform(5, 400), 2), f"{load_date} 05:{random.randrange(60):02d}:00"]
            for i in range(n)]
    if broken:
        rows[0][1] = None          # customer_id missing
        rows[1][2] = "on_hold"     # undeclared status
        rows[2][3] = -12.5         # negative amount
        rows.append(list(rows[3])) # duplicate order_id
    return rows

run("2026-10-14", batch("2026-10-14", 1000))
run("2026-10-15", batch("2026-10-15", 980, broken=True))
print("published rows:", db.execute(f"SELECT COUNT(*) FROM {TABLE}").fetchone()[0])
print("tables:", [r[0] for r in db.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")])
sys.exit(1 if db.execute("SELECT COUNT(*) FROM publish_log WHERE outcome='quarantined'").fetchone()[0] else 0)

Run it twice. Expected output, identical both times (exit code 1, because a batch was quarantined):

2026-10-14: published
2026-10-15: QUARANTINED order_id_unique=1, customer_id_not_null=1, status_accepted=1, amount_not_negative=1
published rows: 1000
tables: ['orders', 'orders__quarantine_20261015', 'publish_log']

Validation: after two runs the published table still holds 1,000 rows and the quarantine table holds the 981 rejected rows. Troubleshooting: unsupported rule means the contract uses a metric this subset lacks (such as missingValues). Teardown: delete warehouse.db. Caution: the script runs SQL from the contract, so review contracts like code and never load them from untrusted sources.

Lab 3 (advanced): Slim CI with dbt and DuckDB. Objective: change one model and let CI build and test only what changed, deferring unchanged parents to production.[26][18] Prerequisites: Python 3.10+ (required by dbt Core 1.12.6 and dbt-duckdb 1.11.0 as of October 2026[51]); about 45 minutes; no cloud cost.

Example status: YAML and bash syntax-checked; not executed (this environment has Python 3.8). Expected results are derived from the dbt documentation, not observed.

  1. Create the project. Both targets share one DuckDB file, so CI can defer to “production”.
# profiles.yml
dataops_lab:
  target: prod
  outputs:
    prod: {type: duckdb, path: lab.duckdb, schema: prod}
    ci: {type: duckdb, path: lab.duckdb, schema: ci}
---
# dbt_project.yml
name: dataops_lab
version: "1.0.0"
profile: dataops_lab
# seeds/raw_orders.csv
order_id,customer_id,status,amount,loaded_at
o1,c1,completed,15.50,2026-10-15 05:10:00
o2,c2,returned,40.00,2026-10-15 05:12:00
o3,c1,shipped,22.00,2026-10-15 05:20:00
-- models/stg_orders.sql
select order_id, customer_id, lower(status) as status,
       cast(amount as decimal(12, 2)) as amount, cast(loaded_at as timestamp) as loaded_at
from {{ ref('raw_orders') }}

-- models/fct_orders.sql
select order_id, customer_id, status,
       case when status = 'returned' then -amount else amount end as net_amount
from {{ ref('stg_orders') }}

Add models/_models.yml from the reference implementation, minus the relationships test (no dim_customers here).

  1. Build production once and keep its manifest.
python3 -m venv .venv && . .venv/bin/activate
pip install "dbt-core==1.12.6" "dbt-duckdb==1.11.0"
dbt build --profiles-dir . --target prod
mkdir -p prod-artifacts && cp target/manifest.json prod-artifacts/
  1. Introduce a bug: change -amount to amount in fct_orders.sql (a lost sign), then run the Slim CI command.
dbt build --profiles-dir . --target ci --select "state:modified+" --defer --state prod-artifacts

Expected (not observed): only fct_orders and its tests are selected; ref('stg_orders') resolves to prod because it was not selected and does not exist in ci;[18] the unit test fails. Validation: revert and rerun; the build passes. Troubleshooting: if dbt selects nothing, the saved manifest already contains the change. If the contract check reports a type mismatch, write the type your platform reports (DuckDB’s names are DECIMAL and VARCHAR); dbt ignores size, precision and scale when it compares.[52] Teardown: delete lab.duckdb, target/ and prod-artifacts/.

  1. In a team, CI runs the same command against the shared warehouse on every pull request. This sketch is not runnable as is: replace dbt-<adapter> and the login step with your warehouse’s; fetch_prod_manifest.sh downloads the last production manifest. Actions are pinned to commit SHAs; GitHub ranks a workflow lockfile, where adoptable, above SHA pinning.[53]
# .github/workflows/dbt-ci.yml (sketch: placeholders in <angle brackets>)
name: dbt-ci
on:
  pull_request:
    branches: [main]
permissions:
  contents: read
  id-token: write # lets the job request an OIDC token; grants no repository write access
jobs:
  slim-ci:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
        with:
          python-version: "3.12"
      - run: pip install "dbt-core==1.12.6" "dbt-<adapter>==<version>"
      - name: Exchange the OIDC token for short-lived warehouse credentials
        run: ./ci/warehouse_login.sh # <your cloud or warehouse OIDC login>
      - name: Fetch the manifest saved by the last production run
        run: ./ci/fetch_prod_manifest.sh prod-artifacts/
      - name: Build and test only what changed
        run: dbt build --target ci --select "state:modified+" --defer --state prod-artifacts

Who can reach the warehouse through this job.

  • Every branch pull request. Without an environment, the default OIDC subject of a pull_request job is repo:ORG/REPO:pull_request (plus owner and repository IDs for repositories created after 15 July 2026 or opted in; not on GitHub Enterprise Server),[54][55] whoever wrote the pull request, so a trust policy on it admits unreviewed code.
  • Not forks. Fork pull requests get no secrets except GITHUB_TOKEN,[56] which is read-only for them;[57] never run pull-request code under pull_request_target.[53]
  • Production reads are exposed. Deferral reads production tables, and a new model is arbitrary SQL, so a pull request could copy production data, including personal data, into a CI schema or the job log. Grant only masked or non-sensitive views, defer to a sanitised clone, or put the job in a GitHub environment whose required reviewers approve each run (prevent self-review, disallow administrator bypass; on Free, Pro and Team plans, public repositories only); the subject becomes repo:ORG/REPO:environment:NAME.[58][54]
  • Writes and deploys. CI writes only to CI schemas; deployment uses a separate role behind a protected environment.[59]

These are the organisations’ own accounts, not independent audits. Contexts and methods come from the sources; the pitfalls marked “ours” and the last column are this page’s suggestions, not reported results.

Scenario Context Method Trade-off or pitfall What to measure (suggested)
Certified data (Airbnb, 2020) Growing warehouse; scientists unsure which sources to trust[8] Owners, design spec, validation, four reviews, landing-time SLAs, badges Certified models are much slower to build Key metrics on certified models; SLA attainment
Contracts at the service boundary (GoCardless) Critical data copied by CDC; upstream changes broke dashboards[9] Self-service contracts in Git that provision the resources Speed versus sprawl Upstream-caused incidents; adoption
Statistical monitoring (Uber, 2020) Tens of thousands of tables; per-metric alerts too noisy[44] Principal components, one-step-ahead forecasts, table-level scores Scores are harder to explain than rules; a learned baseline can absorb a slow decline (ours) Alert precision
Pipeline SLOs (Spotify, in Google’s SRE Workbook) Event delivery feeding many jobs[7] Timeliness, completeness, skewness SLOs measured by independent systems Reprocessing when completeness or skew breaks; a monitor sharing the pipeline’s failure modes goes silent with it (ours) SLO attainment by tier
Training-data validation (Google) Data into ML pipelines for hundreds of teams[45] Schema-based validation of the data in TFX Schemas must evolve with data; an inferred schema accepts today’s errors unless someone reviews it (ours) Errors caught before training

Two lessons travel. Airbnb’s certified data has “landing time SLAs, backed by a central incident management process”, and building it is “much slower than building uncertified data”,[8] so certify only the few assets that carry decisions. At GoCardless, merging a Jsonnet contract provisions BigQuery and Pub/Sub resources; after six months about 30 contracts powered roughly 60% of asynchronous inter-service events, with the best adoption where teams wanted new data streams.[9] Producers adopt contracts when they get something back.

Measure what consumers feel, then the machinery. Google’s SRE Workbook’s freshness objectives include “The oldest data is no older than Y”.[7] Delivery metrics borrow from DORA.[60]

KPI Definition and formula Pitfall Cadence
Freshness SLO attainment Loads published by the agreed time ÷ all loads, per tier-1 asset Count audited publishes, not finished jobs Weekly
Correctness incidents Periods when consumers could see wrong or missing data Unlogged small incidents Monthly
Consumer-detected share Incidents first reported by a consumer ÷ all incidents (this page’s proposed headline indicator) Needs an honest “who found it” field Monthly
Time to detect and restore Visible → detected; detected → corrected (p50, p90) Averages hide tails Monthly
Data downtime Sum of (time to detect + time to restore), a vendor-coined measure[27] Weight by tier Monthly
Check coverage Tier-1 assets with freshness, volume, key tests and contracts ÷ tier-1 assets Test counts reward noise Monthly
Alert precision Alerts that led to action ÷ all alerts Unlabelled alerts Monthly
Change fail rate (pipelines) Deployments needing immediate intervention ÷ deployments[60] Data-only incidents are not deployment failures Monthly
Cost per tier-1 load Compute for build and checks ÷ loads (FinOps) Moves with prices Monthly

Worked calculation 1: a freshness error budget. SLO: the finance mart is published by 07:00 on 95% of business days in a quarter. Over 63 business days the budget is 63 × 5% = 3.15, so 3 late days. Two late days by week 6 use two-thirds of the budget at half-time and should trigger the pre-agreed policy (say, no feature work on that pipeline until the cause is fixed); as in SRE, a budget without consequences is only a chart.

Worked calculation 2: downtime is mostly detection. Four tier-1 incidents in a month:

Incident Time to detect (h) Time to restore (h) Found by
A 0.5 2 Monitor
B 6 3 Consumer
C 0.25 1 Monitor
D 26 5 Consumer

Data downtime = 2.5 + 9 + 1.25 + 31 = 43.75 hours, of which detection is 32.75 hours (75%). Consumer-detected share = 2 ÷ 4 = 50%. The best investment is the monitor that would have caught D, not a faster fix.

Worked calculation 3: the false-alarm bill of 3-sigma limits. For a normally distributed statistic in control, a point falls outside ±3σ with probability 0.0027.[14] With 200 tables × 3 statistics checked daily, that is 600 × 0.0027 ≈ 1.6 false alarms a day, about 49 a month. Real data is rarely normal or independent, so measure precision; page only on tier-1 checks and send the rest to a daily digest.

  • Least privilege. Production jobs write only to their own schemas. CI identities write only to CI schemas and read only what unreviewed pull-request code may see, because every branch pull request can assume them (see Lab 3). General access policy belongs to IAMOps, data-level policy to DataSecOps.
  • Test outputs copy data. dbt’s store_failures saves failing records to an audit schema,[61] and GX publishes validation results in Data Docs.[36] If failing rows hold personal data, apply the source’s access rules and retention, or store only keys.
  • Development data copied from production keeps its legal obligations: masking rules come from DataSecOps and PrivacyOps; DataOps applies them in every environment build.
  • Regulation (jurisdiction-specific, not legal advice). For high-risk AI systems, Article 10 of the EU AI Act requires training, validation and testing data to be “relevant, sufficiently representative, and to the best extent possible, free of errors and complete in view of the intended purpose”.[62] As of October 2026 this applies from 2 December 2027 (Annex III systems) or 2 August 2028 (Annex I), dates set by Regulation (EU) 2026/1744.[63] Banks look to BCBS 239.[64] Model governance belongs to ModelOps and ComplianceOps.
  • Safe repairs. A backfill rewrites history people have used: for tier-1 assets require approval, announce the period, and keep the previous version (for example an Iceberg tag).[24]

Checks are queries, and queries cost money. Under BigQuery’s on-demand model, for example, you pay for bytes processed; LIMIT does not reduce them, while partitioning and clustering do.[65] Levers: scope checks to the new partition (dbt’s freshness filter limits the data scanned[46]); build only what changed in CI and run unit tests only in CI;[26][20] use dbt build --empty for structural dry runs;[30] reserve write-audit-publish for tier 1: it adds latency and orchestration,[23] and doubles writes when publishing copies data (as in Lab 2) rather than swapping metadata.

Capacity is mostly time: slack between the slowest load on the heaviest day and consumers’ deadlines; deadline alerts and an independent freshness monitor page on overruns. Fewer redundant scans also save energy (GreenOps).

  • Monitor the monitors: alert when an asset has produced no check results for longer than its schedule allows. A silent monitor looks exactly like a healthy one.
  • Log overrides: every manual publish that bypassed an audit is justified and reviewed weekly.
  • Close the loop: for each incident, add the check that would have caught it earlier and watch the consumer-detected share.
  • Sample audits: each quarter re-derive a few tier-1 figures from source, or compare against “golden data” with known expected output, as the SRE Workbook suggests.[7]
Symptom Likely cause Diagnosis Remediation and prevention
Load “succeeded” with far fewer rows Partial or late extract Volume against a seasonal baseline; source counts Quarantine and rerun; volume checks plus write-audit-publish
Numbers jump, no code change Upstream change in meaning Distribution monitors; lineage; producer deploys Pin consumers to the last good version; contracts with semantics
Duplicates after a retry Non-idempotent append Count by key per partition Replace the partition; Airflow advises UPSERT over INSERT[25]
Build fails after a producer deploy Renamed column or changed type Schema diff against the contract Versioned change; contract checks in producer CI; registry rules[42]
Late every Monday Weekend backlog, contention Duration and queue time by weekday Capacity or earlier start; deadline alerts
Published totals change days later Late-arriving records Published count against source count a day later Lookback window; mark periods final; completeness checks
Tests green, consumers still find errors Checks test the wrong things Which check would have caught it? Add it; track consumer-detected share
Alerts ignored Static thresholds on seasonal data Precision per check Seasonal baselines; fewer, sharper checks

Worked runbook: the revenue dashboard dropped overnight

Link to section “Worked runbook: the revenue dashboard dropped overnight”
Figure 5. Data incident runbook: from alarm to repaired history
realfault or unsurecodedataAlert or consumer reportReal-world change or datafault?Confirm with the businessowner; annotate thedashboardContain: stop downstreampublishes; pin to last gooddataDiagnose: freshness, volume,schema, tests, lineageupstreamCode change or datachange?Revert the commitFix with the producer or add aguardBackfill affected partitionsidempotentlyVerify with consumers;publish; review
Diagram source (Mermaid)
flowchart TB
  A["Alert or consumer report"]:::risk
  B{"Real-world change or data fault?"}:::decision
  C["Confirm with the business owner; annotate the dashboard"]:::neutral
  D["Contain: stop downstream publishes; pin to last good data"]:::core
  E["Diagnose: freshness, volume, schema, tests, lineage upstream"]:::core
  F{"Code change or data change?"}:::decision
  G["Revert the commit"]:::core
  H["Fix with the producer or add a guard"]:::core
  I["Backfill affected partitions idempotently"]:::core
  J["Verify with consumers; publish; review"]:::neutral
  A --> B
  B -->|"real"| C
  B -->|"fault or unsure"| D --> E --> F
  F -->|"code"| G --> I
  F -->|"data"| H --> I
  I --> J
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029

Text equivalent: an alert or consumer report starts the runbook. If the world really changed, confirm with the business owner and annotate the dashboard. If the data is wrong, or nobody is sure, contain (stop downstream publishes, pin consumers to the last good data), then diagnose through freshness, volume, schema, tests and upstream lineage. Revert a code change; fix a data change with the producer or a new guard. Backfill affected partitions idempotently, verify with consumers, publish and review.

  • “The job succeeded, so the data is fine.” Success means the code ran; only checks on the output speak about the data.
  • Testing everything, warning forever. Thousands of warn-only tests hide the few that matter; a warning nobody reads is a decision to publish bad data.
  • One central team owns every pipeline. It becomes the bottleneck GoCardless removed by letting producers own contracts;[9] data mesh argues the same.[34] Central teams should run the platform.
  • Fixing history in place without a snapshot, or copying unmasked production data into sandboxes.
  • Myth: “DataOps is DevOps with SQL.” DevOps also runs and monitors production; DataOps adds the run loop: checks on data that changes with no deploy.

Use DataOps practices when decisions, money, customers or models depend on regularly refreshed data, when several teams produce or consume it, and when consumers have found errors before.

Do not adopt the full practice for one-off analyses, exploratory notebooks, throwaway prototypes, small datasets whose owner is the only consumer, or reference data that changes once a year; version control and a few sanity checks are enough. The full practice has hidden costs: on-call duty, contract negotiation, compute for checks and slower delivery of certified assets.[8]

Figure 6. Decision tree: how much DataOps, and what to do first
noyesupstreamloadsour codedon't knowyesnoDo decisions, money ormodels depend on thisregularly refreshed data?Light hygiene: Git, a fewchecks, no service levelWhere did recent errors start?Contracts firstFreshness, volume, WAPCI with unit and data testsStart an incident logTier-1 consumers with adeadline?Add SLOs, on-call and errorbudgetsOwners, monitors, weeklyreview
Diagram source (Mermaid)
flowchart TB
  Q1{"Do decisions, money or models depend on this regularly refreshed data?"}:::decision
  N1["Light hygiene: Git, a few checks, no service level"]:::neutral
  Q2{"Where did recent errors start?"}:::decision
  P1["Contracts first"]:::core
  P2["Freshness, volume, WAP"]:::core
  P3["CI with unit and data tests"]:::core
  P4["Start an incident log"]:::risk
  Q3{"Tier-1 consumers with a deadline?"}:::decision
  S1["Add SLOs, on-call and error budgets"]:::core
  S2["Owners, monitors, weekly review"]:::neutral
  Q1 -->|"no"| N1
  Q1 -->|"yes"| Q2
  Q2 -->|"upstream"| P1 --> Q3
  Q2 -->|"loads"| P2 --> Q3
  Q2 -->|"our code"| P3 --> Q3
  Q2 -->|"don't know"| P4 --> Q3
  Q3 -->|"yes"| S1
  Q3 -->|"no"| S2
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029

Text equivalent: if decisions, money or models do not depend on regularly refreshed data, light hygiene is enough. If they do, ask where recent errors started: upstream producers call for contracts first; late or partial loads for freshness and volume checks with write-audit-publish; the team’s own changes for CI with tests; unknown, for an incident log. Tier-1 consumers with a deadline then need SLOs, on-call and error budgets; otherwise owners, monitors and a weekly review suffice.

Assumption: the cheapest control aims at where failures start, and you cannot aim without an incident log.

This is this Atlas’s model, not an industry standard. Move up when the exit criteria hold for tier-1 assets.

Level Observable behaviour Exit criteria
0, Ad hoc Scripts edited in place; consumers find most errors Tier-1 pipeline code in Git with review
1, Versioned Git, an orchestrator, a few tests; informal incidents Owners, freshness and volume checks on tier 1; incidents logged with “who found it”
2, Tested and monitored CI with data tests on every pull request; tier-1 monitors Slim CI in use; consumer-detected share measured for a quarter
3, Contracted and audited Input contracts; write-audit-publish, SLOs and lineage for tier 1 No unaudited tier-1 publish for a quarter; error-budget policy used
4, Optimised Seasonal monitors with tracked precision; cost per load managed; self-service platform Consumer-detected share low and falling
Period Actions Milestone and metric
Days 1–30 List up to ten tier-1 assets with owners and deadlines; Git with review; freshness, volume and key tests; incident log Baselines for consumer-detected share and freshness
Days 31–60 Slim CI; idempotent loads; write-audit-publish for the three most critical assets; first contract with the most damaging producer; weekly review No duplicate-on-retry incidents; first contract merged
Days 61–90 Tier-1 SLOs and error budgets; lineage; seasonal monitors; two more contracts; first sample audit and cost-per-load figure Level 2 exit criteria met; SLO attainment and alert precision reported

Quick wins: freshness checks on the five most-used tables, a dashboard banner procedure, a publish log. Dependencies: a platform team for CI schemas and credentials; producers willing to review contracts.

Quick start for a new pipeline

  • Tier, owner, consumers and their deadline written down
  • Code in Git; changes by pull request only
  • Idempotent load (replace a partition, never append twice)
  • Freshness, volume and key tests in place
  • Failures route to a named person

Before merging a change

  • Slim CI passed for changed models and their children
  • Unit tests cover new date, window or case logic
  • Breaking contract changes have a new version and a deprecation date
  • Backfill plan written if history must change

Tier-1 launch readiness

  • Input contracts merged and checked in the producers’ CI
  • Write-audit-publish with blocking checks agreed with consumers
  • Freshness SLO and error-budget policy agreed
  • Lineage emitted; runbook and on-call rota exist; cost per load estimated
  • Test outputs that copy personal data are access-controlled

Weekly reliability review

  • Error budgets, new incidents and who found them; low-precision alerts, audit overrides, silent monitors

During a data incident

  • Real-world change ruled out; downstream publishes paused; consumers informed
  • Affected assets and period listed from lineage; fix verified in the audit area; review booked

Data contract, one page (the human summary of an ODCS file)

Contract: orders  (version 1.2.0, status: active)        Producer: checkout-platform team
Purpose: one row per customer order for finance and analytics
Schema: order_id (string, unique, required) | customer_id (string, required)
        | status (placed|shipped|completed|returned)
        | amount (decimal 12,2; >= 0; order currency) | loaded_at (timestamp)
Semantics: "returned" = whole order refunded. Partial refunds are not "returned".
Service levels: latency <= 6 h; frequency daily; time to detect <= 30 min
Breaking change process: new major version, 30-day overlap, consumers review the PR
Consumers: finance mart (tier 1), forecasting features (tier 1), ops dashboard (tier 2)

Data incident record

Incident ID:            Asset(s) and tier:
Visible from / to:      Detected at:            Restored at:
Found by: monitor | consumer (name)             Severity:
What consumers saw:
Root cause (code change | data change | infrastructure):
Containment taken:
Backfill: partitions, approval, verification
Check that would have caught it earlier (added? yes/no):
Actions, owners, due dates:

DataOps versus DevOps

Dimension DevOps DataOps
Unit of change Code and configuration Also the data itself, on every run
Rollback Redeploy the previous version Revert code and rebuild data; consumers may have used bad data
Environments Containers, infrastructure as code Plus data isolation: CI schemas, clones, branches
Headline metrics DORA delivery metrics Freshness, correctness, consumer-detected incidents; DORA for pipeline changes

Checks you write versus a data observability platform

Factor dbt tests, GX, Soda Observability platform
Finds What you anticipated Also unanticipated anomalies, from learned baselines
Blocks a publish? Yes, inside the pipeline Often monitors after publication; check the integration
Cost and risk Engineering time; gaps nobody wrote a check for Licence, alert tuning, lock-in, overlap with platform features[4]

Mature setups use both: rules as gates, statistical monitors as a net.

Is DataOps a job title or a team? Both appear. DataOps engineers usually build the platform that pipeline owners use; a team owning everyone’s data quality becomes a bottleneck.

How is DataOps different from data governance? Governance sets rules (classification, access, retention, accountability); DataOps runs pipelines that follow them and produces the evidence.

Should every check block publication? No. Block where failure means consumers would act on wrong data (keys, totals, volume collapses, contract breaks on tier-1 assets); warn on the rest and review warnings weekly.

How is a data contract different from a schema registry? A registry enforces compatibility between schema versions (Confluent’s default is BACKWARD).[42] Some registries now attach metadata and rules as well (Confluent’s rules need a paid tier, as of October 2026),[43] but compatibility checks stay structural. A contract adds meaning, quality rules, service levels and owners,[21] which is why the runbook’s returned failure passed every schema check.

Where does DataOps end and MLOps begin? At the training or feature dataset: DataOps makes it correct and on time; features, training, serving and model drift are MLOps; retrieval corpora for LLM applications are LLMOps.

  • Established: Slim CI;[26] asset-aware orchestration;[29] table-format branching for write-audit-publish;[24] open standards for contracts and lineage.[13][22]
  • Newer, still settling (October 2026): dbt v2 “checks” that enforce project standards before warehouse work;[66] column-level lineage in dbt’s information schema;[30] Dagster freshness policies, “under active development”;[41] Bitol’s Open Data Product Standard (v1.0 in September 2025; v1.1 released with ODCS v3.2.0, which shares five RFCs with it).[67][13]
  • Vendor direction, not settled practice: AI agents that build and operate pipelines (Gartner reports AI-assisted and agentic tools emerging;[4] lakeFS markets branches for agents behind human review[28]). Treat autonomous repair as unproven.
Stage What to do Outcome
Prerequisites SQL joins and aggregates; Git pull requests; what CI does Can review a pipeline change
1-day primer Read the Manifesto and this page’s charter; run Labs 1 and 2 Can explain checks, severity, write-audit-publish
1 week Small dbt project on DuckDB with freshness, tests, a unit test and a contract; Lab 3 Can set up Slim CI
30 days Write-audit-publish and one producer contract on a real tier-1 asset; incident log and scorecard Can run the weekly review
Advanced Seasonal and multivariate monitoring;[44] OpenLineage; streaming contracts; cost with FinOps Can design a platform for many teams
  • Decide which assets are tier 1, who owns them, and which failures block publication.
  • Practise CI for every pipeline, idempotent loads, write-audit-publish for tier 1, contracts with the producers that hurt you most, seasonal monitors.
  • Measure freshness against an error budget, consumer-detected share, detect and restore times, alert precision, cost per load.
  • Watch out for checks that cannot block, warn-only sprawl, personal data in failing-row tables, CI roles that read production.
  • Next: list tier-1 assets this week, add freshness and volume checks, start the incident log with “who found it”.

A judgement call: block the 07:00 finance load or publish with a warning?

Link to section “A judgement call: block the 07:00 finance load or publish with a warning?”

Assumptions. A false alarm is possible (a holiday, a genuinely bad day); the board pack is copied into slides and e-mails; yesterday’s figures are correct but a day old.

Why B tempts, and why it is usually wrong for tier 1. It keeps the schedule, but the banner stays on the dashboard while the numbers travel into slides without it. Stale-but-labelled data fails safe; wrong-but-plausible data does not.

Deciding in fifteen minutes. Check the cheapest discriminating evidence: the source system’s own order count, the holiday calendar, the producer’s deploy log. If the source agrees with the low number, the world changed: publish and state the cause. If it shows more orders than were loaded, it is a partial load: block, page the producer, rerun. If you cannot tell by 07:00, block; one late morning costs little next to a board decision on a wrong number.

What would change the answer. For a staffing dashboard used within the hour, freshness may matter more than a 12% count error: publish with the warning. Either way, agree the rule in advance and write it into the contract or runbook; alert precision tells you how often such alarms are false.

You should now be able to explain DataOps and its contested label, design write-audit-publish with blocking checks, write checks with a seasonal baseline, specify a contract, measure error budgets and downtime, and run a data incident.

1. A load succeeded and all schema checks passed, yet dashboard revenue fell 38%. Which checks could have caught it, and why not schema checks?

A distribution monitor on status shares, and accepted values whose meanings are written in the contract. The schema did not change; the meaning of returned did, which column names and types cannot show. See the runbook.

2. Why does dbt build not protect consumers who query the model whose test failed?

It skips downstream resources, but the tested model has already been rebuilt in its target schema.[30] Build into an audit schema and publish separately.

3. A 28-day mean ± 3 sd volume check missed a partial Thursday load. Why, and what would you change?

Weekend days in the baseline inflate the standard deviation. In Lab 1 the all-days limits (695 to 1,219) accepted a load about 19% below a normal weekday; weekday-only limits (913 to 1,106) caught it. Compare like with like, or forecast seasonality.

4. A retry doubled yesterday’s rows. Which principle broke, and what is the fix?

Idempotency: the task appended. Replace the partition (or merge on a key) in one transaction, as Airflow advises and Lab 2 does.[25]

5. Which identity should a pull-request CI job use to reach the warehouse, and what can it expose?

A dedicated CI identity with short-lived OpenID Connect credentials and no deployment rights. With GitHub’s default subject, a pull-request job presents repo:ORG/REPO:pull_request (with IDs in newer repositories), which every branch pull request shares,[54] so any contributor’s unreviewed SQL runs with it. Let it write only to CI schemas and read only masked or non-sensitive views (or a sanitised clone), or require an environment approval before each run (private repositories: GitHub Enterprise only).[58]

6. Yesterday’s order total grew 4% after today’s run, with no code change. What happened, and what is missing?

Late-arriving records: the incremental run reprocessed its lookback window and added them to yesterday’s period, which is correct. What is missing is a rule, agreed with consumers, for when a period becomes final, and a check of published counts against the source.

DataOps is a young label on an old idea: treat the data you deliver as the output of a production line and inspect it before it leaves the factory. The label is contested, but the problem it names is not: pipelines fail quietly, and without deliberate practice the first monitor is the person who trusted the number.

What works is narrower than the hype: test every change, make loads idempotent, audit tier-1 data before anyone sees it, write contracts where producers can break you, use baselines that know what day it is, and judge yourself by who finds problems first.

Changelog: 2026-10-16, first version: researched, drafted, Labs 1 and 2 executed, Lab 3 not executed, by the managing author; evidence ledger in research/dataops-ledger.md. 2026-10-17, r1 revision (reviews/dataops-r1-response.md): CI security side effects, experimental Deadline Alerts, glossary, late-arriving data, ODCS placeholders, RACI, AI Act dates, URLs; Labs 1 and 2 re-run. 2026-10-17, post-score polish (reviews/dataops-polish.md): GitHub plan and Enterprise Server limits, Confluent rule tiers, glossary SLO, DevOps myth, Airbnb spec length; five sources re-checked, repetition trimmed.


  1. 3 reasons why DataOps is essential for big data success — L. Liebmann, IBM Big Data & Analytics Hub, 2014-06-19 (Internet Archive copy). Checked 2026-10-16. ↩︎ ↩︎

  2. From DevOps to DataOps — A. Palmer, Tamr, 2015-05-07 (Internet Archive copy). Checked 2026-10-16. ↩︎ ↩︎

  3. The DataOps Manifesto — stewarded by DataKitchen, living page. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  4. Market Guide for DataOps Tools — M. Simone, S. Menon, R. Thanaraj, Gartner, 2025-10-24 (ID G00836742; licensed reprint hosted by Astronomer). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  5. DataOps – Towards a Definition — J. Ereth, LWDA 2018, CEUR-WS Vol-2191, 2018. Checked 2026-10-16. ↩︎ ↩︎

  6. DataOps: revision history — Wikipedia, redirect created 2026-04-08. Checked 2026-10-16. ↩︎ ↩︎

  7. Data Processing Pipelines — R. Sodt and I. Maravić, The Site Reliability Workbook, chapter 13, Google, 2018. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  8. Data Quality at Airbnb, Part 2 — A New Gold Standard — V. Quoss et al., The Airbnb Tech Blog, 2020-11-24. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  9. 7 Lessons From GoCardless’ Implementation of Data Contracts — A. Jones, Monte Carlo blog (guest post), updated 2026-03-30. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  10. The DataOps Manifesto, capture of 24 August 2017 — Internet Archive, 2017. Checked 2026-10-16. ↩︎

  11. Discovering DataOps: A Comprehensive Review of Definitions, Use Cases, and Tools — K. Mainali et al., DATA ANALYTICS 2021, IARIA. Checked 2026-10-16. ↩︎

  12. About dbt versions — dbt Labs (v2.0 released 2026-09-14; v1.12 released 2026-07-16). Checked 2026-10-16. ↩︎ ↩︎

  13. Open Data Contract Standard v3.2.0 — Bitol, released 2026-09-08 (release history). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  14. What are Control Charts? — NIST/SEMATECH e-Handbook of Statistical Methods, section 6.3.1. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  15. What is DataOps? — IBM Think, living page. Checked 2026-10-16. ↩︎

  16. About dbt models — dbt Labs. Checked 2026-10-16. ↩︎

  17. Materializations — dbt Labs. Checked 2026-10-16. ↩︎

  18. Defer — dbt Labs. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  19. Add data tests to your DAG — dbt Labs. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  20. Unit tests — dbt Labs. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  21. Bitol: open standards for data contracts and data products — Bitol (Linux Foundation AI & Data). Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  22. About OpenLineage — OpenLineage project, docs v1.54.0; object model. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  23. Build Write-Audit-Publish pattern with Apache Iceberg branching and AWS Glue Data Quality — AWS Big Data Blog, 2024-12-09. Checked 2026-10-16. ↩︎ ↩︎

  24. Branching and Tagging — Apache Iceberg docs. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  25. Best Practices — Apache Airflow docs. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  26. About local state in dbt — dbt Labs. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  27. What Is Data Observability? 5 Key Pillars To Know — B. Moses, Monte Carlo (vendor), updated 2026-10-08. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  28. Welcome to lakeFS — lakeFS documentation. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  29. Asset-Aware Scheduling — Apache Airflow docs, version 3.3.2. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  30. About dbt build command — dbt Labs. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  31. About microbatch incremental models — dbt Labs. Checked 2026-10-16. ↩︎

  32. Timely Stream Processing — Apache Flink docs (stable). Checked 2026-10-16. ↩︎

  33. ISO/IEC 25012 — iso25000.com (informational site; iso.org blocked automated access). Checked 2026-10-16. ↩︎

  34. Data Mesh Principles and Logical Architecture — Z. Dehghani, martinfowler.com, 2020-12-03. Checked 2026-10-16. ↩︎ ↩︎

  35. Model contracts — dbt Labs. Checked 2026-10-16. ↩︎ ↩︎

  36. GX Core overview — Great Expectations, GX Core 1.24.0. Checked 2026-10-16. ↩︎ ↩︎

  37. Soda Core — Soda, README (v4). Checked 2026-10-16. ↩︎

  38. Automating large-scale data quality verification — S. Schelter et al., VLDB 2018. Checked 2026-10-16. ↩︎ ↩︎

  39. Deadline Alerts — Apache Airflow docs. Checked 2026-10-16. ↩︎ ↩︎

  40. Testing assets with asset checks — Dagster docs. Checked 2026-10-16. ↩︎

  41. Asset freshness policies — Dagster docs. Checked 2026-10-16. ↩︎ ↩︎

  42. Schema Evolution and Compatibility — Confluent docs. Checked 2026-10-16. ↩︎ ↩︎ ↩︎

  43. Data Contracts for Schema Registry on Confluent Platform — Confluent docs. Checked 2026-10-17. ↩︎ ↩︎

  44. Monitoring Data Quality at Scale with Statistical Modeling — S. Shanmugam et al., Uber Engineering, 2020-05-07. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎

  45. Data Validation for Machine Learning — E. Breck et al., SysML 2019. Checked 2026-10-16. ↩︎ ↩︎

  46. freshness — dbt Labs. Checked 2026-10-16. ↩︎ ↩︎

  47. severity, error_if, and warn_if — dbt Labs. Checked 2026-10-16. ↩︎

  48. Upgrading to Airflow 3 — Apache Airflow docs. Checked 2026-10-16. ↩︎

  49. About env_var function — dbt Labs. Checked 2026-10-16. ↩︎

  50. airflow.providers.standard.operators.bash — Apache Airflow standard provider. Checked 2026-10-16. ↩︎

  51. dbt-duckdb — DuckDB project, README; version 1.11.0 on PyPI (2026-08-07). Checked 2026-10-16. ↩︎

  52. contract — dbt Labs. Checked 2026-10-16. ↩︎

  53. Secure use reference — GitHub Docs. Checked 2026-10-16. ↩︎ ↩︎

  54. OpenID Connect reference — GitHub Docs. Checked 2026-10-17. ↩︎ ↩︎ ↩︎

  55. OpenID Connect — GitHub Docs. Checked 2026-10-16. ↩︎

  56. Using secrets in GitHub Actions — GitHub Docs. Checked 2026-10-16. ↩︎

  57. Events that trigger workflows — GitHub Docs. Checked 2026-10-16. ↩︎

  58. Deployments and environments — GitHub Docs. Checked 2026-10-17. ↩︎ ↩︎

  59. Secrets — GitHub Docs. Checked 2026-10-16. ↩︎

  60. DORA’s software delivery performance metrics — DORA. Checked 2026-10-16. ↩︎ ↩︎

  61. store_failures — dbt Labs. Checked 2026-10-16. ↩︎

  62. Article 10: Data and Data Governance — EU Artificial Intelligence Act explorer (Future of Life Institute; not the official text). Checked 2026-10-16. ↩︎

  63. Regulation (EU) 2026/1744 (Digital Omnibus on AI) — European Parliament and Council, 8 July 2026, OJ L, 2026-07-24 (Article 1, point 40). Checked 2026-10-17. ↩︎

  64. Principles for effective risk data aggregation and risk reporting — Basel Committee on Banking Supervision, 2013-01-09. Checked 2026-10-16. ↩︎

  65. BigQuery pricing — Google Cloud (terms as of October 2026). Checked 2026-10-16. ↩︎

  66. Checks — dbt Labs (dbt v2). Checked 2026-10-16. ↩︎

  67. Open Data Product Standard: releases — Bitol, GitHub (v1.0.0 2025-09-24; v1.1.0 2026-09-08). Checked 2026-10-16. ↩︎

Related Ops

DataSecOps

DataSecOps is a vendor-promoted label for building security into data platform operations: classifying data, enforcing who may see which rows and columns, masking or tokenising sensitive fields, and proving it from access logs.

AnalyticsOps

Operating analytics and BI: semantic layer and metric definitions, BI tools (Power BI, Tableau, Looker), dashboard lifecycle and governance, self-service analytics, da…

In preparation

MLOps

MLOps is the engineering practice of getting trained machine-learning models into production and keeping them correct there: versioned data, code and models, automated training and release pipelines, monitoring for skew and drift, and controlled retraining.

WorkflowOps

Workflow orchestration engines and operations: DAGs, state machines, retries and idempotency, long-running and durable workflows, sagas/compensation, Temporal, Apache …

In preparation

DBOps

Database operations: schema migrations as code (Flyway, Liquibase), backups and point-in-time recovery, replication and failover, indexing and query performance, conne…

In preparation

SecDataOps

SecDataOps is the engineering and governance of security telemetry: onboarding log sources, parsing and normalising them to a schema, enriching, routing and tiering them, and retaining them so that detections, investigations and audits have complete, trustworthy and affordable evidence.

ResearchDataOps

Research data management operations: FAIR principles, data management plans, metadata standards and persistent identifiers (DOI), reproducible research workflows, rese…

In preparation

DevOps

DevOps is a way of organising software delivery so that the people who build a service and the people who run it share goals, tools and feedback, letting changes reach users quickly, safely and repeatedly. It is judged by delivery outcomes, not job titles.

Practise and get certified

From xOps School

Practise DataOps and get certified

The free DataOps track on xOps School has study notes, a 25-question practice quiz with explanations, and a verifiable certificate when you score 70% or more. Sign in with Google to start.

For teams

Instructor-led training

Live batches for your team, with completion certificates and reports for L&D/HR.

Source: master.md (the editorial source of this page) · built 2026-10-17 · diagrams rendered with @mermaid-js/mermaid-cli 11.17.0 (mermaid 11.17.2)