---
updatedAt: 2026-09-28T22:19:35.000Z
agentTools:
  projectIndex: https://docs.getmontecarlo.com/llms.txt
---

# Comparison Monitor

Compare metrics across two data sources and alert when differences exceed thresholds.

## Overview

Compare the same metric across two tables — source and target — and get alerted when they diverge beyond a threshold. Useful for migration validation, cross-environment parity checks (staging vs production), or any scenario where two datasets should agree on row counts, null rates, or custom expressions.

> 📘 **Reference scope**
>
> This page covers MaC YAML configuration. For an introduction to comparison monitors, see [Comparison Monitors](https://docs.getmontecarlo.com/docs/comparison-rules).

MaC key: `metric_comparison`. Replaces the legacy `comparison_rule` (blocked from new creation).

## Quick Start

```yaml
montecarlo:
  metric_comparison:
    - name: orders_row_count_parity
      description: Verify row count matches between staging and production orders
      source:
        warehouse: staging-snowflake
        table: staging:core.fct_orders
      target:
        warehouse: prod-snowflake
        table: analytics:core.fct_orders
      alert_conditions:
        - metric: ROW_COUNT
          type: comparison_delta
          operator: GT
          threshold_value: 100
      schedule:
        type: fixed
        interval_minutes: 1440
      domains:
        - my-domain
```

Create interactively with `create_or_update_comparison_monitor(dry_run=True)` via the [Monte Carlo MCP server](https://docs.getmontecarlo.com/docs/agent-toolkit).

## Configuration

<Cards columns={1}>

<Card title="description — what this monitor checks">

`string` · required

Displayed in the Monte Carlo UI and in incident notifications. Max 512 characters.

```yaml
description: Verify staging and prod orders tables have matching row counts
```

</Card>

<Card title="source — source-side data configuration">

`object` · required

Defines one side of the comparison — the table or SQL query, warehouse connection, and optional segmentation. The `warehouse` field inside `source` is always required; the top-level `warehouse` does not substitute for it.

```yaml
source:
  warehouse: staging-snowflake
  table: staging:core.fct_orders
  where_condition: "status = 'ACTIVE'"
```

**Properties**

---

**`warehouse`** — warehouse for this side of the comparison

`string` · required

Warehouse UUID or name. Must be specified on each side independently — the top-level `warehouse` does not substitute for it.

```yaml
warehouse: staging-snowflake
```

---

**`table`** — fully qualified table name

`string` · optional · one of `table` or `sql` required

Format: `database:schema.table`. Mutually exclusive with `sql`.

```yaml
table: staging:core.fct_orders
```

---

**`sql`** — custom SQL query as the data source

`string` · optional · one of `table` or `sql` required

Custom SQL query. Mutually exclusive with `table`.

```yaml
sql: "SELECT * FROM staging.core.fct_orders WHERE created_at > DATEADD(day, -7, CURRENT_DATE)"
```

---

**`transforms`** — AI-powered field transforms

`array of objects` · optional

Applied before metric computation. Same structure as [metric monitor transforms](https://docs.getmontecarlo.com/docs/metric#data_sourcetransforms).

```yaml
transforms:
  - field: revenue
    expression: "amount_cents / 100.0"
```

---

**`where_condition`** — SQL WHERE filter

`string` · optional

SQL WHERE clause (without `WHERE`) to filter rows before comparison.

```yaml
where_condition: "status = 'ACTIVE'"
```

---

**`segment_fields`** — columns to segment comparison by

`array of strings` · optional

Column names to segment the comparison by. Maximum 1 field.

```yaml
segment_fields:
  - region
```

---

**`segment_sql`** — SQL expressions for segmentation

`array of strings` · optional

SQL expressions for segmentation when column names alone are insufficient. Maximum 1 expression.

```yaml
segment_sql:
  - "DATE_TRUNC('month', created_at)"
```

---

**`connection_name`** — named connection override

`string` · optional

Named connection to use when executing queries on this side.

```yaml
connection_name: staging-read-only
```

---

**`timeout`** — query execution timeout

`integer` · optional

Query execution timeout in seconds.

```yaml
timeout: 300
```

</Card>

<Card title="target — target-side data configuration">

`object` · required

Same structure as `source`. Defines the other side of the comparison. The `warehouse` field inside `target` is always required; the top-level `warehouse` does not substitute for it.

```yaml
target:
  warehouse: prod-snowflake
  table: analytics:core.fct_orders
```

</Card>

<Card title="alert_conditions — comparison alert conditions">

`array of objects` · required

Each entry defines which metric to compare and the threshold that triggers an alert. Supported alert condition types: `comparison_delta`, `noop`.

**Available operators:** `GT` · `NOOP` only. Other operators (LT, GTE, AUTO, etc.) are not supported.

**Threshold type:** `comparison_delta` alerts when the difference between source and target exceeds `threshold_value`. Set `is_threshold_relative: true` to treat `threshold_value` as a percentage difference rather than an absolute count.

```yaml
alert_conditions:
  - metric: ROW_COUNT
    type: comparison_delta
    operator: GT
    threshold_value: 100
```

**Properties**

---

**`metric`** — built-in metric name

`string` · optional · one of `metric` or `custom_metric` required

Built-in metric name (e.g., `ROW_COUNT`, `NULL_COUNT`, `NUMERIC_MEAN`). Mutually exclusive with `custom_metric`. See [Available Metrics](https://docs.getmontecarlo.com/docs/available-metrics).

```yaml
metric: ROW_COUNT
```

---

**`custom_metric`** — custom SQL-based metric

`object` · optional · one of `metric` or `custom_metric` required

Custom SQL-based metric with separate expressions for source and target. Mutually exclusive with `metric`.

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `uuid` | string | no | UUID of an existing custom metric to reuse |
| `display_name` | string | yes | Name for the metric |
| `source_sql_expression` | string | yes | SQL expression for the source side. Must return a single numeric value. |
| `target_sql_expression` | string | yes | SQL expression for the target side. Must return a single numeric value. |

<br />

```yaml
custom_metric:
  display_name: Total Revenue
  source_sql_expression: "SUM(amount_cents) / 100.0"
  target_sql_expression: "SUM(revenue_usd)"
```

---

**`source_field`** — column name on the source side

`string` · optional

Required when comparing field-level metrics and columns have different names on each side.

```yaml
source_field: EMAIL_ADDR
```

---

**`target_field`** — column name on the target side

`string` · optional

Required when comparing field-level metrics and columns have different names on each side.

```yaml
target_field: EMAIL
```

---

**`type`** — condition type

`enum` · optional · default: `comparison_delta`

**Accepted values:** `comparison_delta` · `noop`

Use `noop` to collect data without alerting.

```yaml
type: comparison_delta
```

---

**`operator`** — comparison operator

`enum` · optional · default: `GT`

**Accepted values:** `GT` · `NOOP`

Only `GT` is supported for `comparison_delta` conditions.

```yaml
operator: GT
```

---

**`threshold_value`** — maximum allowed delta

`number` · required, for `comparison_delta` conditions

Maximum allowed delta between source and target.

```yaml
threshold_value: 100
```

---

**`is_threshold_relative`** — treat threshold as percentage

`boolean` · optional · default: `false`

When `true`, `threshold_value` is treated as a percentage (0-100) rather than an absolute value.

```yaml
is_threshold_relative: true
```

</Card>

<Card title="schedule — execution schedule">

`object` · optional · default: system-managed schedule

Controls when the monitor runs. Supported modes: `fixed`, `dynamic`, `manual`. Crontab (`interval_crontab`) is supported. Omitting `schedule` means Monte Carlo runs the monitor on the default collection cycle.

```yaml
schedule:
  type: fixed
  interval_minutes: 1440
```

**Properties**

---

**`type`** — schedule type

`enum` · optional · default: `fixed`

**Accepted values:** `fixed` · `dynamic` · `manual`

```yaml
type: fixed
```

---

**`interval_minutes`** — run interval

`integer` · optional

Run interval for `fixed` schedules.

```yaml
interval_minutes: 1440
```

---

**`interval_crontab`** — crontab expressions

`array of strings` · optional

Crontab expressions for scheduling.

```yaml
interval_crontab:
  - "0 6 * * *"
```

---

**`interval_crontab_day_operator`** — crontab day combination

`enum` · optional

**Accepted values:** `AND` · `OR`

How to combine day-of-week and day-of-month in crontab.

```yaml
interval_crontab_day_operator: AND
```

---

**`start_time`** — schedule start time

`string` · optional

ISO 8601 format.

```yaml
start_time: "2024-01-01T06:00:00Z"
```

---

**`timezone`** — schedule timezone

`string` · optional

```yaml
timezone: America/New_York
```

---

**`dynamic_schedule_tables`** — tables that trigger the monitor

`array of strings` · required, when `type` is `dynamic` (unless `dynamic_schedule_jobs` is set)

Tables whose update events trigger this monitor.

```yaml
dynamic_schedule_tables:
  - analytics:core.fct_orders
```

---

**`dynamic_schedule_jobs`** — jobs that trigger the monitor

`array of objects` · optional

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `job_type` | string | yes | `AdfJob` · `AirflowDag` · `DatabricksJob` · `DbtJob` |
| `job_name` | string | yes | Name of the job |
| `project_name` | string | yes | Project or workspace containing the job |
| `task_name` | string | no | Specific task within the job |
| `mcon` | string | no | MCON identifier for the job |

<br />

```yaml
dynamic_schedule_jobs:
  - job_type: DbtJob
    job_name: nightly_build
    project_name: analytics
```

---

**`min_interval_minutes`** — minimum interval between dynamic runs

`integer` · optional

Minimum interval between runs for `dynamic` schedules.

```yaml
min_interval_minutes: 60
```

</Card>

<Card title="domains — domain for this monitor">

`array of strings (exactly one entry)` · required when creating a monitor

Required on all accounts, regardless of when the account was created. Monitors that already exist without a domain can still be updated without adding one.

Set `default_domain` in `montecarlo.yml` to avoid repeating it on every monitor.

```yaml
domains:
  - my-domain
```

</Card>

<Card title="name — unique identifier within the namespace">

`string` · required

Required for monitors created after Jan 29, 2024 (existing monitors keep working). Changing the name creates a new monitor and deletes the old one — incident history does not transfer.

```yaml
name: orders_row_count_parity
```

</Card>

<Card title="warehouse — top-level warehouse">

`string` · optional

Warehouse UUID or name. Does **not** substitute for `warehouse` inside `source` and `target` — those are always required. Overrides `default_resource` from `montecarlo.yml`.

```yaml
warehouse: my-snowflake
```

</Card>

<Card title="notes — internal notes">

`string` · optional

Visible in the Monte Carlo UI. Not included in notifications.

```yaml
notes: Migration parity check. Reviewed weekly during cutover.
```

</Card>

<Card title="notify_run_failure — notify on query failure">

`boolean` · optional

Notify when the monitor query itself fails.

```yaml
notify_run_failure: true
```

</Card>

<Card title="audiences — notification channels">

`array of strings or mappings` · optional

Audience names linking this monitor to channels defined in Notifications as Code.

An entry can instead be a mapping that narrows an audience to specific triage priorities, so it is only notified about alerts scored at those priorities. Some audience must cover `NOT_TRIAGED`. See [Routing alerts by triage priority](https://docs.getmontecarlo.com/docs/notifications-v2#routing-alerts-by-triage-priority).

```yaml
audiences:
  - migration-alerts
  - data-engineering
  - audience: oncall
    triage_priority: [HIGH]
```

</Card>

<Card title="failure_audiences — run-failure notification channels">

`array of strings` · optional

Separate audiences for run-failure notifications. Falls back to `audiences` if not set.

```yaml
failure_audiences:
  - oncall-alerts
```

</Card>

<Card title="alert_grouping — control how breaches are grouped into alerts">
`object` · optional · default: no grouping (a new alert is created every time the monitor breaches)

Groups subsequent breaches into the currently open alert rather than creating a new one, while the alert remains open and unresolved.

**`mode`**
**Accepted values:** `group_into_open_alert`

<br />

```yaml
alert_grouping:
  mode: group_into_open_alert
```
</Card>

<Card title="priority — incident priority level">

`enum` · optional

**Accepted values:** `P1` · `P2` · `P3` · `P4` · `P5`

```yaml
priority: P2
```

</Card>

<Card title="tags — key-value pairs for organizing monitors">

`array of objects` · optional

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `name` | string | yes | Tag key |
| `value` | string | no | Tag value |

<br />

```yaml
tags:
  - name: team
    value: analytics
  - name: environment
    value: production
```

</Card>

<Card title="data_quality_dimension — data quality category">

`enum` · optional

**Accepted values:** `ACCURACY` · `COMPLETENESS` · `CONSISTENCY` · `TIMELINESS` · `UNIQUENESS` · `VALIDITY`

```yaml
data_quality_dimension: CONSISTENCY
```

</Card>

<Card title="is_draft — create as draft without activating">

`boolean` · optional · default: `false`

Creates the monitor in a paused state. Omitting this on a later update resets to `false` (active) due to PUT semantics — always include it if you want the monitor to stay in draft.

```yaml
is_draft: true
```

</Card>

<Card title="uuid — update an existing monitor">

`string` · optional

Include the UUID of an existing monitor to update it instead of creating a new one.

```yaml
uuid: 0dae7702-0950-45c7-909c-7e183bddca19
```

</Card>

<Card title="auto_triage — override auto-triage for this monitor">

`boolean` · optional

Force auto-triage on (`true`) or off (`false`) for this monitor, overriding the account or domain default. Omit to inherit. See [the Triage Agent](https://docs.getmontecarlo.com/docs/triage-agent#per-monitor-override).

```yaml
auto_triage: false
```

</Card>

</Cards>

<Accordion title="Deprecated fields">

| Field | Use instead |
|-------|-------------|
| `resource` | `warehouse` |
| `domain` | `domains` |
| `domain_uuids` | `domains` |
| `labels` | `audiences` |
| `notify_rule_run_failure` | `notify_run_failure` |

</Accordion>

> 📘 **API-only fields**
>
> Some fields visible in the API or JSON Schema (`skip_reset`, `fail_on_reset`) are present in the schema but silently stripped during MaC YAML processing. They have no effect and should not be included.

### Available Metrics

**Table-level (no&#x20;**`source_field`**&#x20;/&#x20;**`target_field`**):** `ROW_COUNT`

**Field-level (require&#x20;**`source_field`**&#x20;and&#x20;**`target_field`**):**

* Uniqueness: `UNIQUE_COUNT`, `DUPLICATE_COUNT`, `APPROX_DISTINCT_COUNT`
* Null/empty: `NULL_COUNT`, `NON_NULL_COUNT`, `EMPTY_STRING_COUNT`, `TEXT_ALL_SPACES_COUNT`, `NAN_COUNT`, `TEXT_NULL_KEYWORD_COUNT`
* Numeric: `NUMERIC_MEAN`, `NUMERIC_MEDIAN`, `NUMERIC_MIN`, `NUMERIC_MAX`, `NUMERIC_STDDEV`, `SUM`, `ZERO_COUNT`, `NEGATIVE_COUNT`
* Percentiles: `PERCENTILE_20`, `PERCENTILE_40`, `PERCENTILE_60`, `PERCENTILE_80`
* Text stats: `TEXT_MAX_LENGTH`, `TEXT_MIN_LENGTH`, `TEXT_MEAN_LENGTH`, `TEXT_STD_LENGTH`
* Text format: `TEXT_NOT_INT_COUNT`, `TEXT_NOT_NUMBER_COUNT`, `TEXT_NOT_UUID_COUNT`, `TEXT_NOT_SSN_COUNT`, `TEXT_NOT_US_PHONE_COUNT`, `TEXT_NOT_US_STATE_CODE_COUNT`, `TEXT_NOT_US_ZIP_CODE_COUNT`, `TEXT_NOT_EMAIL_ADDRESS_COUNT`, `TEXT_NOT_TIMESTAMP_COUNT`
* Boolean: `TRUE_COUNT`, `FALSE_COUNT`
* Timestamp: `FUTURE_TIMESTAMP_COUNT`, `PAST_TIMESTAMP_COUNT`, `UNIX_ZERO_COUNT`

## Examples

### Row count parity with absolute threshold

Alerts when the row count difference between source and target exceeds 100 rows.

```yaml
montecarlo:
  metric_comparison:
    - name: orders_row_count_parity
      description: Verify staging and prod orders tables have matching row counts
      source:
        warehouse: staging-snowflake
        table: staging:core.fct_orders
      target:
        warehouse: prod-snowflake
        table: analytics:core.fct_orders
      alert_conditions:
        - metric: ROW_COUNT
          type: comparison_delta
          operator: GT
          threshold_value: 100
      schedule:
        type: fixed
        interval_minutes: 1440
      audiences:
        - migration-alerts
      priority: P2
      data_quality_dimension: CONSISTENCY
      domains:
        - my-domain
```

### Field-level comparison with different column names

Compares null counts between two tables where the column is named differently on each side.

```yaml
montecarlo:
  metric_comparison:
    - name: email_nulls_comparison
      description: Compare email null counts between legacy and new CRM
      source:
        warehouse: prod-snowflake
        table: legacy:crm.contacts
      target:
        warehouse: prod-snowflake
        table: new:crm.customers
      alert_conditions:
        - metric: NULL_COUNT
          source_field: EMAIL_ADDR
          target_field: EMAIL
          type: comparison_delta
          operator: GT
          threshold_value: 50
      schedule:
        type: fixed
        interval_minutes: 720
      priority: P3
      domains:
        - my-domain
```

### Custom metric comparison

Uses custom SQL expressions to compare a derived metric across both sides.

```yaml
montecarlo:
  metric_comparison:
    - name: revenue_match
      description: Verify total revenue matches between source system and warehouse
      source:
        warehouse: source-postgres
        table: public:billing.invoices
      target:
        warehouse: prod-snowflake
        table: analytics:finance.fct_invoices
      alert_conditions:
        - custom_metric:
            display_name: Total Revenue
            source_sql_expression: "SUM(amount_cents) / 100.0"
            target_sql_expression: "SUM(revenue_usd)"
          type: comparison_delta
          operator: GT
          threshold_value: 0.01
          is_threshold_relative: true
      schedule:
        type: fixed
        interval_minutes: 1440
      audiences:
        - finance-data-quality
      priority: P1
      domains:
        - my-domain
```

## Troubleshooting

### Metrics and fields

* **Passing&#x20;**`source_field`**&#x20;or&#x20;**`target_field`**&#x20;with&#x20;**`ROW_COUNT`**.** `ROW_COUNT` is a table-level metric. Including field references causes a validation error. This is the single most common mistake with comparison monitors.
* **Using&#x20;**`custom_metric`**&#x20;together with&#x20;**`metric`**&#x20;or field references.** These are mutually exclusive. When using `custom_metric`, omit `metric`, `source_field`, and `target_field` entirely.

### Source and target

* **Forgetting&#x20;**`warehouse`**&#x20;inside&#x20;**`source`**&#x20;/&#x20;**`target`**.** The `warehouse` field is required on each side. The top-level `warehouse` field does not substitute for it.

### Alert conditions

* **Omitting&#x20;**`threshold_value`**&#x20;for&#x20;**`comparison_delta`**&#x20;conditions.** The threshold is required — without it, the backend cannot evaluate the comparison.

### Tags and updates

* **Wrong&#x20;**`tags`**&#x20;format.** Tags must be objects with `name` and optional `value` keys. Writing `tags: ["my-tag"]` or `tags: - my-tag` fails validation.
* **Forgetting PUT semantics on updates.** When updating a monitor by including `uuid`, every field you omit reverts to its default — it is not left unchanged. Always specify the complete desired configuration.

<br />