MLOps Pipelines: Train, Test and Ship Models Automatically
Deployment
MLOps
CI/CD
What MLOps really means, the three maturity levels, an animated pipeline run, experiment tracking and a CI/CD workflow with a quality gate.
Author
MLStation
Published
October 1, 2026
Training PayGuard once by hand is easy. Retraining it every month, testing it, comparing it with the live model and shipping it without mistakes is hard. MLOps applies DevOps habits (automation, testing, monitoring) to machine learning, with one extra twist: the data changes too, so models must be retrained and re-validated, not just re-deployed (Google Cloud).
TipWhat you will learn
The three MLOps maturity levels and what each one automates
The stages of a training pipeline, shown as an animation
How data validation stops bad data before it reaches a model
How experiment tracking and a quality gate decide what ships
Google describes three levels: level 0, manual process; level 1, ML pipeline automation (continuous training); and level 2, CI/CD pipeline automation (the pipeline code itself is tested and deployed automatically) (Google Cloud).
Table 1: MLOps maturity levels
Level
Who retrains
How often
Typical risk
0 Manual
A data scientist runs a notebook
A few times a year
Stale model, steps forgotten
1 Pipeline
A scheduled or triggered pipeline
Weekly or monthly
Pipeline code changes are untested
2 CI/CD
Pipeline code is tested and released like software
On every change
Higher setup effort
Why does it matter? The simulation below follows PayGuard for a year while fraud patterns slowly change. The manual team retrains twice; the pipeline retrains every month.
Show the code
import numpy as np, plotly.graph_objects as gofrom ms_charts import COLORS, PLOTLY_CONFIGrng = np.random.default_rng(3)days = np.arange(365)def quality(retrain_days): q, last = [], 0for d in days:if d in retrain_days: last = d q.append(0.93-0.00045* (d - last) -0.0000012* (d - last) **2+ rng.normal(0, 0.004))return np.array(q)manual = quality({0, 180})auto = quality(set(range(0, 365, 30)))fig = go.Figure()fig.add_scatter(x=days, y=manual, name="Level 0: manual, twice a year", line=dict(color=COLORS["rose"], width=2.5))fig.add_scatter(x=days, y=auto, name="Level 1: pipeline, monthly", line=dict(color=COLORS["teal"], width=2.5))fig.update_layout(height=380, xaxis_title="Day", yaxis_title="AUC on live data", legend=dict(orientation="h", y=1.12))fig.show(config=PLOTLY_CONFIG)
Figure 1: Simulated model quality over a year. Automated monthly retraining keeps the model close to its best.
Averaged over the year, the automated model scores 0.924 AUC against 0.876 for the manual one. In fraud detection, that gap is money.
Anatomy of a training pipeline
A pipeline is a chain of steps where each step must pass before the next one starts.
flowchart TD
A[Ingest<br/>new payments] --> B{Validate<br/>data}
B -- fail --> X[Stop and alert]
B -- pass --> C[Build<br/>features] --> D[Train] --> E[Evaluate]
E --> F{Quality<br/>gate}
F -- worse than live --> Y[Keep current model]
F -- better --> G[Register<br/>new version] --> H[Deploy<br/>canary]
Press play to run the pipeline. Then tick “inject a bad data batch” and run it again: validation catches the problem before any training happens.
import {ms, msStyle} from"/assets/js/ms-plot.js"viewof bad = Inputs.toggle({label:"Inject a bad data batch",value:false})viewof runs = Inputs.button("Run pipeline")
steps = ["Ingest","Validate","Features","Train","Evaluate","Gate","Register","Deploy"]progress = { runs; bad;for (let t =0; t <= steps.length*12; t++) { yield t;await Promises.delay(40); }}{const W =780, bw =84, gap = (W -20- bw * steps.length) / (steps.length-1);const failAt = bad ?1:-1;const cur =Math.floor(progress /12);const state = i => failAt >=0&& i > failAt ?"skip": i < cur ? (i === failAt ?"fail":"done") : i === cur ? (i === failAt ?"fail":"run") :"wait";const col = {done: ms.teal,run: ms.amber,fail: ms.rose,wait:"currentColor",skip:"currentColor"};const msg = failAt >=0&& cur > failAt ?"Stopped at validation: 12% of rows have a negative amount and 'hour' is missing. No model was trained. Alert sent.": cur >= steps.length?"Pipeline passed. Version 14 registered and sent to a 5% canary.":`Running: ${steps[Math.min(cur, steps.length-1)]}`;return htl.html`<div>${htl.svg`<svg viewBox="0 0 ${W} 110" style="width:100%;max-width:${W}px;font-family:Inter,system-ui,sans-serif">${steps.map((s, i) => { const st =state(i), x =10+ i * (bw + gap);return htl.svg`<g>${i < steps.length-1? htl.svg`<line x1="${x + bw}" y1="45" x2="${x + bw + gap}" y2="45" stroke="currentColor" stroke-opacity="0.3" stroke-width="2"/>`:""} <rect x="${x}" y="20" width="${bw}" height="50" rx="10" fill="${col[st]}" fill-opacity="${st ==="wait"|| st ==="skip"?0.07:0.85}" stroke="currentColor" stroke-opacity="0.2"/> <text x="${x + bw /2}" y="50" text-anchor="middle" font-size="12.5" font-weight="600" fill="${st ==="wait"|| st ==="skip"?"currentColor":"white"}" opacity="${st ==="skip"?0.35:1}">${s}</text> <text x="${x + bw /2}" y="92" text-anchor="middle" font-size="11" fill="currentColor" opacity="0.7">${st ==="done"?"passed": st ==="run"?"running": st ==="fail"?"failed": st ==="skip"?"skipped":""}</text></g>`; })} </svg>`}<div class="ms-readout"><span>Status</span><b style="font-size:1rem">${msg}</b></div></div>`;}
Data validation in practice
Most pipeline failures start with data, not code. A few simple checks catch the common ones. Here is a real check on a simulated batch where something went wrong upstream.
Two rules fail, so the pipeline stops. Libraries such as Great Expectations, Pandera and TFX Data Validation turn these checks into reusable, documented suites.
Experiment tracking: which run wins?
During tuning, a team may train dozens of versions. Tracking tools (MLflow, Weights & Biases) store each run’s settings and scores, so the best one is easy to find and reproduce. Below are 40 simulated tuning runs.
Figure 2: Forty simulated tuning runs. Each dot is one training run; the star is the best by validation AUC.
The quality gate
A new model is promoted only if it is at least as good as the live model on the same held-out data, and not worse on any important slice (for example, small merchants or one country).
Here \varepsilon is a small tolerance (say 0.002) and \delta protects slices (say 0.01). Writing the gate as code removes arguments and gut feelings from the release decision.
CI/CD for ML
At level 2, a push to the repository triggers tests, a training run and the gate. This GitHub Actions workflow is a compact starting point.
.github/workflows/train.yml
name: train-and-gateon:push:{branches:[main]}schedule:[{cron:"0 3 1 * *"}] # also monthly, 03:00 on the 1stjobs:pipeline:runs-on: ubuntu-lateststeps:-uses: actions/checkout@v4-uses: actions/setup-python@v5with:{python-version:"3.11"}-run: pip install -r requirements.txt-run: pytest tests/ # unit tests for feature code-run: python pipeline/validate.py # data checks, exits 1 on failure-run: python pipeline/train.py # logs run to MLflow-run: python pipeline/gate.py # compares with live model-run: python pipeline/register.py # only reached if gate passes
Common pitfalls
Pitfall
Fix
Training notebook and pipeline drift apart
Move code into a package; notebooks import it
No fixed random seeds or data snapshots
Log seed, data version and code commit with every run
Gate only checks the overall metric
Add slice checks and a latency check
Pipeline fails silently
Alert on failure and on “no run in 35 days”
Summary
MLOps automates training, testing and release, because data keeps changing.
Level 0 is manual, level 1 automates training, level 2 also tests and ships the pipeline itself.
Validate data first, track every experiment, and let a quality gate decide what ships.