Elementary Data: Anomaly Detection and Data Observability for dbt Projects

Elementary Data: Anomaly Detection and Data Observability for dbt Projects

Elementary is an open-source dbt package that adds data observability to your existing dbt project. It turns dbt models into monitored data assets: tracking volume changes, schema changes, freshness, and statistical anomalies in column distributions — without changing your existing dbt tests.

Where dbt's built-in tests (not_null, unique) catch hard failures, Elementary catches soft failures: a table that used to have 100k rows now has 50k, or a column's average shifted 40% overnight. These are the production data quality issues that pass all static tests but break downstream analytics.

Installation

pip install 'elementary-data[snowflake]'  # or bigquery, redshift, databricks

Add to your dbt project's packages.yml:

packages:
  - package: elementary-data/elementary
    version: [">=0.14.0", "<0.15.0"]
dbt deps

Add the Elementary profile to profiles.yml:

# profiles.yml
my_project:
  target: dev
  outputs:
    dev:
      type: snowflake
      # ... your existing connection config

elementary:
  target: dev
  outputs:
    dev:
      type: snowflake
      database: analytics
      schema: elementary
      # ... same connection as above

Initialize Elementary:

dbt run --select elementary

This creates the elementary schema with metadata tables that Elementary writes to.

Configuring Elementary Tests

Elementary tests are dbt singular tests added to your schema.yml files:

# models/staging/schema.yml
version: 2

models:
  - name: stg_orders
    tests:
      - elementary.volume_anomalies:
          timestamp_column: created_at
          sensitivity: 3          # standard deviations from baseline
          backfill_days: 14       # days of history to build baseline
      - elementary.freshness_anomalies:
          timestamp_column: updated_at
          max_time_since_last_update_allowed: "1 hour"

    columns:
      - name: order_id
        tests:
          - not_null
          - unique
          - elementary.column_anomalies:
              column_anomalies:
                - null_count
                - null_rate

      - name: amount
        tests:
          - elementary.column_anomalies:
              column_anomalies:
                - avg
                - stddev
                - max
                - min
                - zero_count
                - zero_rate
              sensitivity: 3

      - name: status
        tests:
          - elementary.column_anomalies:
              column_anomalies:
                - null_rate
                - count_distinct

  - name: orders_by_customer
    tests:
      - elementary.volume_anomalies:
          timestamp_column: last_order_date
          time_bucket:
            period: day
            count: 1
      - elementary.dimension_anomalies:
          timestamp_column: last_order_date
          dimensions:
            - status   # detect shifts in order status distribution

Available Test Types

Volume Anomalies

Detects unusual row count changes:

tests:
  - elementary.volume_anomalies:
      timestamp_column: created_at
      sensitivity: 3         # 3 standard deviations (default)
      backfill_days: 30      # training window
      time_bucket:
        period: hour         # or day, week
        count: 1
      seasonality: day_of_week  # account for Mon-Sun patterns
      exclude_final_results: false

Use day_of_week seasonality for tables where Monday volume differs from Friday volume.

Freshness Anomalies

Alerts when data stops updating:

tests:
  - elementary.freshness_anomalies:
      timestamp_column: updated_at
      max_time_since_last_update_allowed: "6 hours"
      where_expression: "status = 'active'"  # only check active records

Column Anomalies

Statistical monitoring per column:

- name: revenue
  tests:
    - elementary.column_anomalies:
        column_anomalies:
          - avg                # mean value
          - stddev             # standard deviation
          - max                # maximum value
          - min                # minimum value
          - null_count         # number of nulls
          - null_rate          # fraction of nulls
          - zero_count         # number of zeros
          - zero_rate          # fraction of zeros
          - count_distinct     # cardinality
          - percentile_25      # 25th percentile
          - percentile_75      # 75th percentile
          - percentile_95      # 95th percentile
        sensitivity: 3
        backfill_days: 14

Dimension Anomalies

Detects shifts in categorical column distributions:

- elementary.dimension_anomalies:
    timestamp_column: created_at
    dimensions:
      - country
      - payment_method
    sensitivity: 3
    # alerts when US/EU split changes by >3 stddev from baseline

Schema Changes

Track schema changes automatically:

models:
  - name: stg_orders
    tests:
      - elementary.schema_changes:
          # alerts on: added columns, removed columns, type changes

Global Configuration

Set defaults in dbt_project.yml:

# dbt_project.yml
vars:
  elementary:
    # Training window for anomaly detection
    backfill_days: 14
    # Alert sensitivity (standard deviations)
    anomaly_sensitivity: 3
    # Minimum training set size before alerting
    minimum_training_set_size: 7
    # Slack webhook for alerts
    slack_webhook: "{{ env_var('ELEMENTARY_SLACK_WEBHOOK', '') }}"
    # Send alerts on test failures only, or all runs
    send_test_results_on_failure: true

Running Elementary

# Run dbt + Elementary tests
dbt test --select elementary

# Run all tests including Elementary
dbt build

# Generate Elementary report
edr report

# Send Slack alerts for failures
edr send-report --slack-webhook $SLACK_WEBHOOK

# CLI monitoring
edr monitor

Slack Alerts

Configure Slack alerts in profiles.yml or via env vars:

# Elementary config in profiles.yml
elementary:
  outputs:
    dev:
      type: snowflake
      # ... connection
      elementary_config:
        slack_webhook: "https://hooks.slack.com/services/..."
        slack_channel_name: "#data-alerts"
        slack_token: "xoxb-..."   # for bot token auth
        send_test_results_on_failure: true
        timezone: "America/New_York"

Or via CLI:

edr send-report \
  --slack-webhook $SLACK_WEBHOOK \
  --slack-channel "#data-quality" \
  --days-back 1

Alert format in Slack:

🚨 Elementary Alert
Test: volume_anomalies
Model: stg_orders
Status: FAIL
Details: Row count dropped 45% (expected: 98,000-102,000, actual: 54,231)
Time window: 2024-01-15 08:00 - 09:00 UTC

The Elementary Dashboard

Elementary generates a local HTML report:

edr report --days-back 7
# Opens browser with interactive dashboard

The dashboard shows:

  • Test results timeline
  • Anomaly trends per model
  • Schema changes log
  • Lineage graph with test coverage

For a hosted dashboard, use Elementary Cloud (free tier available).

Integrating with CI

# .github/workflows/dbt-elementary.yml
name: dbt + Elementary

on:
  schedule:
    - cron: '0 */6 * * *'   # every 6 hours
  push:
    branches: [main]
    paths: ['models/**', 'tests/**']

jobs:
  dbt-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: |
          pip install dbt-snowflake elementary-data[snowflake]
          dbt deps

      - name: Run dbt + tests
        run: dbt build --target prod
        env:
          DBT_SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
          DBT_SNOWFLAKE_PASSWORD: ${{ secrets.SNOWFLAKE_PASSWORD }}

      - name: Send Elementary report
        if: always()   # run even if dbt fails
        run: |
          edr send-report \
            --slack-webhook ${{ secrets.ELEMENTARY_SLACK_WEBHOOK }} \
            --days-back 1
        env:
          ELEMENTARY_SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
          ELEMENTARY_SNOWFLAKE_PASSWORD: ${{ secrets.SNOWFLAKE_PASSWORD }}

Tuning Anomaly Sensitivity

The default sensitivity of 3 standard deviations catches major anomalies but may miss gradual drift. Tuning options:

Lower sensitivity (more alerts, more noise):

sensitivity: 2   # alerts at 2 stddev — good for critical payment tables

Higher sensitivity (fewer alerts, misses subtle issues):

sensitivity: 4   # alerts at 4 stddev — for noisy tables with natural variance

Seasonality adjustment:

- elementary.volume_anomalies:
    seasonality: day_of_week   # compares Monday to previous Mondays
    timestamp_column: created_at

Time-windowed baselines:

- elementary.volume_anomalies:
    backfill_days: 30    # use 30 days of history for baseline (default: 14)
    min_training_set_size: 14   # don't alert until 14 data points

Elementary vs Monte Carlo vs Soda

Feature Elementary Monte Carlo Soda
Open source Yes (core) No Yes (core)
dbt integration Native Via metadata Via scans
Anomaly detection Statistical ML-based Threshold-based
Schema monitoring Yes Yes Yes
Lineage Via dbt Advanced No
Alerting Slack, webhook Slack, PagerDuty, etc. Slack, webhook
Cost Free (OSS) $$$$ Free (OSS)
Setup complexity Low (dbt package) High Medium

Elementary's advantage: zero additional infrastructure. It runs inside your dbt project using your existing warehouse connection and writes metadata to a schema you already control.

Common Pitfalls

Not enough training data: Elementary needs at least 7-14 data points to build a reliable baseline. On tables that refresh daily, this means 1-2 weeks before alerts are meaningful. Set minimum_training_set_size: 14 to avoid false positives in the first week.

Timezone issues: Elementary timestamps use UTC by default. If your business metrics are day-based in a different timezone, configure timezone to avoid volume anomalies at midnight UTC.

Alert fatigue: Too many models monitored with low sensitivity = too many Slack messages = team ignores alerts. Start with volume_anomalies on your most critical 5-10 models, then expand coverage as you tune sensitivity.

Backfilling historical data: When you first add Elementary to an existing project, run edr run --backfill-period 30 to populate 30 days of training data before alerts go live.

Summary

Elementary adds the anomaly detection layer that static dbt tests miss. Volume anomalies catch table truncations and pipeline failures. Column anomalies catch data quality regressions that pass not_null and unique checks. Schema change monitoring catches accidental upstream schema changes. It's a dbt package — one pip install, one dbt deps, and it writes to your existing warehouse. The result is a monitored data stack with Slack alerts before your stakeholders notice something broke.

Read more

Start now free