Skip to main content
Glama

A pipeline change can pass every test, finish on time and keep the schema intact while quietly dropping rows, duplicating keys or changing what a column means. More of these changes are now written by AI coding agents, which makes "the job succeeded" an even weaker signal.

pipeverify compares your tables before and after a change and gives a verdict:

Verdict

Meaning

Default CI behaviour

✅ PASS

The data is the same where it should be.

Merge

🟡 REVIEW

Something changed that tests would not catch. A person should look.

Merge allowed, flagged

⛔ BLOCK

The change damages the data.

Fail the check

It works with any tool that produces tables: SQL scripts, Spark jobs, dbt, Airflow tasks or an AI agent's fix. It needs two snapshots, nothing else. It reads dbt models and Snowflake tables directly, runs as a GitHub Action, and AI agents can call it through MCP.

Why

In the PipeMend benchmark (DOI 10.5281/zenodo.23510784), LLM agents repaired 64 data pipeline incidents. Production-style data quality monitors approved every harmful fix the agents made. What caught them was comparing the data itself. pipeverify packages that check so any team can run it on every change.

Related MCP server: nock-mcp

Quick start

No install needed to try it: open the 5-minute notebook in Colab.

pip install pipeverify

# compare two snapshots of one table
pipeverify compare before/orders.parquet after/orders.parquet --key order_id
BLOCK   orders.parquet
        rows 1,200 -> 1,000, matched 1,000
        BLOCK  Table lost 200 rows (16.67%)
        BLOCK  200 key(s) present before are gone after (16.67%)
        BLOCK  Total of 'revenue' moved 16.71% (1,383,131.25 -> 1,152,000.00)

Verdict: BLOCK  This change damages the data. Do not merge as is.

Try the bundled demo, five realistic changes to a retail sales table:

git clone https://github.com/akshaysharma1088/pipeverify && cd pipeverify
pip install -e .
python examples/retail/make_demo.py
for s in examples/retail/scenarios/*/; do pipeverify run -c "$s/pipeverify.yml"; done

Scenario

What happened

Verdict

1_safe_fix

Revenue restated for late refunds on one day, as intended

✅ PASS

2_dedupe_drops_rows

Duplicates removed on the wrong columns, real days dropped

⛔ BLOCK

3_join_fanout

A join to a dimension with a duplicate row doubled one store

⛔ BLOCK

4_unit_change

Upstream started sending one region's revenue in cents

⛔ BLOCK

5_null_creep

A renamed source field left region empty for new stores

🟡 REVIEW

Every one of these runs successfully and keeps the same schema.

Sources

before and after can each be:

Source

CLI form

Config form

CSV, Parquet, JSON, Excel (.xlsx) file (CSV and JSON can be .gz or .zst compressed)

data/orders.parquet

path: data/orders.parquet

DuckDB table

duckdb:warehouse.db:gold.orders

{duckdb: warehouse.db, table: gold.orders}

DuckDB query

"duckdb:warehouse.db:SELECT ..."

{duckdb: warehouse.db, query: "SELECT ..."}

dbt model (DuckDB or Snowflake target)

dbt:fct_orders

{dbt: fct_orders, target: prod, manifest: prod/manifest.json}

Snowflake table or query

snowflake:ANALYTICS.GOLD.ORDERS

{snowflake: ANALYTICS.GOLD.ORDERS, where: "order_date >= '2026-10-01'"}

Any SQLAlchemy database (Postgres, BigQuery, ...)

{url: "postgresql://${PGUSER}@host/db", table: gold.orders}

pandas DataFrame

Python API

SQL databases need pip install "pipeverify[sql]" plus the driver for your database. Environment variables in URLs are expanded, so credentials stay out of the config file.

See SQL source examples for Postgres, Snowflake and BigQuery.

dbt: check a pull request against production

# pipeverify.yml, next to dbt_project.yml
tables:
  - name: fct_daily_sales
    before: {dbt: fct_daily_sales, target: prod, manifest: prod-state/manifest.json}
    after: {dbt: fct_daily_sales, target: ci}
    key: [store_id, sale_date]

dbt: <model> finds the model's table in dbt's manifest.json and connects the way profiles.yml says for that target, so there is no second set of credentials to keep. manifest is the manifest of the run you are comparing with (the same file --state and --defer use); leave it out for target/manifest.json. pipeverify does not run dbt and writes nothing. DuckDB and Snowflake targets work today. A runnable project, with a join that fans out while every dbt test stays green, is in examples/dbt_pr.

Snowflake

pip install "pipeverify[snowflake]"
export SNOWFLAKE_ACCOUNT=myorg-myaccount SNOWFLAKE_USER=ci_user SNOWFLAKE_PRIVATE_KEY_PATH=~/.ssh/snowflake.p8
export SNOWFLAKE_ROLE=REPORTER SNOWFLAKE_WAREHOUSE=COMPUTE_WH
pipeverify compare snowflake:PROD.GOLD.ORDERS snowflake:DEV.GOLD.ORDERS --key order_id

It reads the same SNOWFLAKE_* variables as the Snowflake CLI and DataRanger (password, key pair, token or SNOWFLAKE_AUTHENTICATOR), or a named connection from ~/.snowflake/connections.toml with {snowflake: ..., connection: name}. It runs one SELECT per snapshot, tagged pipeverify in query history, and nothing else, so a read-only role is enough. Each snapshot is loaded into memory: for a big table add where: or pass a query, and compare the slice your change touched.

Config

Check many tables at once with pipeverify.yml (pipeverify init writes a starter file):

thresholds:              # optional, overrides the defaults below for every table
  row_change_block_pct: 10
  #skip_id_like_totals: true    # set to false to also check totals of *_id columns
tables:
  - name: gold.daily_sales
    before: snapshots/main/daily_sales.parquet
    after: snapshots/pr/daily_sales.parquet
    key: [store_id, sale_date]
    expect_change: [revenue]   # columns this change is meant to modify
    ignore: [loaded_at]        # columns that always change
  - name: gold.customers
    before: {url: "${WAREHOUSE_URL}", table: prod.customers}
    after: {url: "${WAREHOUSE_URL}", table: pr_123.customers}
    key: [customer_id]
    expect_row_change: true    # this change is meant to add or remove rows
pipeverify run                     # uses ./pipeverify.yml
pipeverify run --fail-on review    # stricter: fail CI on REVIEW too
pipeverify run --format markdown -o report.md
pipeverify run --json-out report.json

Exit code is 1 when the verdict reaches --fail-on (default block), otherwise 0.

What it checks

Check

Catches

Default

Schema

Removed, added or retyped columns

removed: BLOCK, retyped: REVIEW

Row count

Rows lost or gained

REVIEW > 1%, BLOCK ≥ 10%

Missing keys

Rows that existed before and are gone

REVIEW on any, BLOCK ≥ 5%

New keys

Rows that did not exist before

REVIEW

Duplicate keys

Fan-out from joins, double loads

BLOCK on any new duplicate

Null rate

Columns going empty

REVIEW > 1 pt, BLOCK ≥ 10 pts

Changed values

Matched rows whose values changed, per column

REVIEW on any, BLOCK ≥ 10%

Column totals

Unit changes, partial loads, double counting

REVIEW > 0.5%, BLOCK ≥ 5%

Category values

New codes in low-cardinality text columns

REVIEW

Columns listed in expect_change are reported but never raise the verdict. All thresholds can be changed globally or per table; see pipeverify/checks.py for every option.

Source checks

A before/after diff only sees damage to rows that already existed. Errors in new data need evidence the pipeline does not control: the raw source. Add a source block to a table:

tables:
  - name: silver.transactions
    after: snapshots/pr/silver_transactions.parquet
    before: snapshots/main/silver_transactions.parquet   # optional
    key: [txn_id]
    source:
      data: landing/raw_transactions.parquet
      key: [txn_id]
      columns: {amount: amount}      # output column: source column
      checks: [conflict]             # default; also mismatch, missing, orphan
      where: "status = 'POSTED'"     # source rows that should reach the output

Check

Catches

Default

conflict

The source sent several values for one key and the output silently kept one

on, REVIEW

mismatch

An output value the source never sent (use only for pass-through columns)

opt-in

missing

Source keys that never reached the output (set where for intended filters)

opt-in

orphan

Output keys that are not in the source

opt-in

From the command line:

pipeverify compare - silver.parquet -k txn_id --source raw.parquet --source-column amount

In a pilot on the 209 executed fixes in the PipeMend benchmark (25 harmful), production-style monitors caught 16 harmful fixes, and the source conflict check caught the 9 that the monitors approved. Together they caught all 25, at the cost of a REVIEW on 21 of 184 correct fixes, all on incidents where the source data really was ambiguous. This is a small pilot on synthetic data; a larger evaluation is in progress.

GitHub Action

# .github/workflows/pipeverify.yml
on: pull_request
permissions:
  contents: read
  pull-requests: write
jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      # ... build the "after" snapshots for this PR here (for example: dbt build --target ci) ...
      - uses: akshaysharma1088/pipeverify@v0.3.0
        with:
          config: pipeverify.yml
          fail-on: block      # or review, or never
          extras: snowflake   # optional drivers: snowflake, sql
        env:
          SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}   # only if your sources need them

The action posts the report as a PR comment and in the job summary, fails the job at the verdict you choose, and exposes verdict, report and json as outputs.

Use it from an AI agent (MCP)

An agent that edits a query, a dbt model or a load should not be the one to decide its change was safe. pipeverify ships an MCP server so the agent can ask:

uvx pipeverify mcp        # or: pip install pipeverify && pipeverify mcp
{
  "mcpServers": {
    "pipeverify": { "command": "uvx", "args": ["pipeverify", "mcp"] }
  }
}

That block works in Claude Desktop, Claude Code, Cursor, VS Code and any other MCP client (Python 3.10 or newer). The tools:

Tool

What it does

compare_tables

Compare a table before and after a change: files, DuckDB, Snowflake, dbt models or SQL databases

check_against_source

Check an output table against the raw source it was built from, when there is no "before"

run_config

Run the project's pipeverify.yml, the same check CI runs

Every tool is read-only: snapshots are read, the checks run in an in-memory DuckDB, and nothing is written to your files or warehouse. The agent gets pass, review or block with the evidence, and is told to report every review and block finding to you instead of working around it. Database credentials stay on your machine (environment variables or profiles.yml) and are never tool arguments; give the server a read-only role.

Python API

import pipeverify

result = pipeverify.verify(before_df, after_df, key="order_id", expect_change=["status"])
print(result.verdict)              # "pass" | "review" | "block"
for f in result.findings:
    print(f.severity, f.message)

Useful inside an Airflow task, a notebook, or an AI agent's own loop: let the agent propose a fix, run verify, and only apply the fix on PASS.

Optional AI explanation

export ANTHROPIC_API_KEY=...       # or OPENAI_API_KEY with PIPEVERIFY_LLM_MODEL
pip install "pipeverify[ai]"
pipeverify run --explain

This adds a short plain-English explanation of what probably happened and what to check next. It is off by default. Only the findings are sent (check names, counts, percentages, column names); example values from your data are removed first. The verdict never depends on the LLM.

Who uses pipeverify

See ADOPTERS.md. Using it at work or in a project? Tell us how, even in one line. It helps other teams decide whether to try it and shapes what gets built next.

Roadmap

  • Native adapters for Airflow and Spark, and more dbt targets (BigQuery, Postgres, Databricks)

  • Sampling and pushdown for very large tables, so checks run in the warehouse

  • Rules learned from history: what "normal" change looks like for each table

Ideas and pull requests are welcome; see CONTRIBUTING.md and the good first issues. Questions go in Discussions.

Status

Alpha. The checks and verdicts are stable enough to try in CI; options and output formats may still change before 1.0.

Citation

If you use pipeverify in research, please cite the PipeMend benchmark that motivated it (see CITATION.cff).

License

Apache-2.0

Available Tools

3 tools
check_against_sourceA
Read-onlyIdempotent

Check an output table against the raw source it was built from, when there is no "before" to compare.

It catches a transformation that looks clean but decided something it had no basis for: the source
sent two different amounts for one key and the output kept one without saying so, values that no
longer match the source, source rows that never arrived, or output rows with no source at all.

Use this for a new table, a first load, or a dedupe or repair step, where compare_tables has nothing
to compare with. Use compare_tables when you do have the table from before the change; you can run
both for the strongest check.

Behavior: read-only and repeatable. The table and the source are each read once (files are opened
read-only; Snowflake, dbt and SQL sources run one SELECT each, with credentials already configured
on this machine) and loaded into a temporary in-memory DuckDB. Nothing is written anywhere. Usually
takes seconds. Fails with a readable error if a file, table, key or column does not exist.

Returns verdict ("pass", "review" or "block"), headline (one sentence you can show the user), and
tables: one entry with stats and findings, each with check, severity, message, column and metrics.
A "review" here usually means the source itself is ambiguous, so a person must choose; say so.

Inputs: key (required), table (required), source (required), name, +4 more. Returns: tables, seconds, verdict, version, +1 more

compare_tablesA
Read-onlyIdempotent

Compare one table before and after a pipeline change and say whether the change harmed the data.

It looks for the damage that a green job and passing tests miss: rows lost or duplicated (a join that
fans out, a dedupe that drops too much), keys that disappeared, values that changed in columns the
change was not meant to touch, a jump in nulls, a shifted total (a unit or currency change), new or
vanished categories, and schema or type changes.

Use this after you edit a SQL query, dbt model, Spark job or load, or after an agent applies a fix,
and before you report the work as done, merge it or publish the table. Use check_against_source
instead when there is no earlier snapshot but you have the raw input the table was built from, and
run_config when the project already has a pipeverify.yml listing its tables.

Behavior: read-only and repeatable. Both snapshots are read (files are opened read-only; Snowflake,
dbt and SQL sources run one SELECT each, with credentials already configured on this machine, never
passed as arguments) and loaded into a temporary in-memory DuckDB where the checks run. Nothing is
written anywhere. Each snapshot is loaded whole, so for a large warehouse table pass a SELECT or a
'where' filter covering the slice you changed. Usually takes seconds. Fails with a readable error
if a file, table, dbt model, key column or login is wrong.

Returns verdict ("pass", "review" or "block"), headline (one sentence you can show the user), and
tables: one entry with stats (row counts and more) and findings, each with check, severity, message,
column and metrics. On "review" or "block", tell the user every finding and do not apply the change.

Inputs: after (required), before (required), key, name, +4 more. Returns: tables, seconds, verdict, version, +1 more

run_configA
Read-onlyIdempotent

Check every table listed in a project's pipeverify.yml and return one overall verdict.

Use this when the project already has a pipeverify config, which is the same check its CI runs, so
you see what the pull request check will say before you push. Use compare_tables for a one-off
comparison of two snapshots without a config file.

Behavior: read-only and repeatable. Reads the config, then reads each table's snapshots (files,
DuckDB, Snowflake, dbt models or SQL databases, using credentials already configured on this
machine) into a temporary in-memory DuckDB. Nothing is written anywhere. Time grows with the number
and size of the tables. A table that cannot be read is reported as "block" with the reason in its
error field, and the remaining tables are still checked. Fails with a readable error if the config
file is missing, is not valid, or no table matches.

Returns verdict (the worst across tables: "pass", "review" or "block"), headline, and tables: one
entry per table with its own verdict, stats, error and findings. Tell the user every review and
block finding.

Inputs: config, tables. Returns: tables, seconds, verdict, version, +1 more

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.6/5.0

    Scored across 3 tools

    Disambiguation5/5

    The three tools target clearly distinct verification workflows: before/after comparison, output-vs-source checking, and config-wide project checking. The descriptions explicitly cross-reference when to use one instead of another, leaving little room for misselection.

    Naming Consistency5/5

    All tool names use snake_case and begin with a clear verb, following a predictable action-oriented convention. check_against_source is slightly longer but still fits the same verb-first pattern.

    Tool Count5/5

    Three tools is a tight, well-scoped surface for a read-only data verification server. Each tool corresponds to a distinct mode of checking, and none appears redundant or decorative.

    Completeness4/5

    The server covers the main verification paths: one-off before/after checks, source-vs-output checks, and full project config runs. A minor gap is that there is no explicit tool to create, inspect, or validate pipeverify.yml, though run_config reports config errors and users can manage the file outside the server.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      B
      maintenance
      Zero-config data quality monitoring as MCP tools. Profiles a warehouse (Postgres, BigQuery, Snowflake, MySQL, DuckDB), detects anomalies, and gates CI — read-only with the connection resolved server-side, never via the model.
      8
      131 PyPI
      14
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables checking Postgres migration SQL against a table-size estate snapshot and safety policy before apply, returning approve/block verdicts with lock details and remediation guidance, plus tools to explain locks and list policy rules.
      1
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables static, database-free SQL health checks that return rule-based findings and verdicts, propose executable fixes, and verify rewrites are equivalent while comparing execution plans and suggesting indexes. Supports batch/file/git-diff review for CI gating, all at zero-dependency Tier0.
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables AI agents to compare two tables across the same or different databases and report what changed, which columns changed, and where changes concentrate, plus source/table discovery and schema description. Runs four read-only tools that take source names rather than connection strings, returning a stable JSON contract with a risk verdict.
      88 npm
      MIT