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, databricksAdd to your dbt project's packages.yml:
packages:
- package: elementary-data/elementary
version: [">=0.14.0", "<0.15.0"]dbt depsAdd 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 aboveInitialize Elementary:
dbt run --select elementaryThis 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 distributionAvailable 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: falseUse 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 recordsColumn 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: 14Dimension 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 baselineSchema Changes
Track schema changes automatically:
models:
- name: stg_orders
tests:
- elementary.schema_changes:
# alerts on: added columns, removed columns, type changesGlobal 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: trueRunning 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 monitorSlack 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 1Alert 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 UTCThe Elementary Dashboard
Elementary generates a local HTML report:
edr report --days-back 7
# Opens browser with interactive dashboardThe 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 tablesHigher sensitivity (fewer alerts, misses subtle issues):
sensitivity: 4 # alerts at 4 stddev — for noisy tables with natural varianceSeasonality adjustment:
- elementary.volume_anomalies:
seasonality: day_of_week # compares Monday to previous Mondays
timestamp_column: created_atTime-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 pointsElementary 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.