Install
- Claude Code
- Codex
- Cursor
Save the file as
.claude/skills/monitors-as-code/SKILL.md in your dbt repository. Claude Code loads it when a task mentions monitors; /monitors-as-code invokes it directly.Save the file as
.agents/skills/monitors-as-code/SKILL.md in your dbt repository so Codex loads it on demand. As a fallback, paste the body into AGENTS.md under a Sidecar monitors heading.Save the file as
.cursor/rules/monitors-as-code.mdc, replacing the frontmatter with:---
description: Writing Sidecar monitor definitions in dbt YAML
globs: ["models/**/*.yml", "sidecar/monitors.yml"]
alwaysApply: false
---
SKILL.md
---
name: monitors-as-code
description: Write and edit Sidecar monitor definitions in a dbt project, in a model's config.meta.sidecar block or in sidecar/monitors.yml. Use when asked to add, change, rename, or remove a Sidecar monitor, data quality check, volume or freshness check, or alert on a dbt model.
---
# Sidecar monitors as code
Sidecar creates monitors from definitions in the dbt project. Two locations, one schema:
1. Model YAML under `config.meta.sidecar` (never top-level `meta`). Sidecar sees changes after the production dbt run and its next catalog refresh.
2. `sidecar/monitors.yml` at the repo root. Sidecar reads the default branch on its next sync, so merged changes are picked up without a dbt run. Use it for many models at once or for package models whose YAML is not in the repo.
## Workflow
1. Search both locations for existing definitions on the model (`grep -n sidecar models/ sidecar/monitors.yml`).
2. Keep existing keys and all unrelated YAML untouched. Edit the block that already defines the monitor rather than adding a second one.
3. Verify the model and column names against the model SQL or its YAML columns before writing SQL.
4. Validate the result parses as YAML and sits under `config → meta → sidecar` for model blocks.
## Identity
A monitor is identified by `<model>::<key>`. Changing `name` keeps the monitor. Changing the key normally creates a new monitor and retires the old one, with one exception: Sidecar detects a rename when the new key is on the same model and, for volume and freshness, binds the same table, or, for custom, has unchanged resolved SQL and is the only candidate. Never define the same key for a model in both locations; both are blocked.
## Model block
```yaml
models:
- name: fct_orders
config:
meta:
sidecar:
version: 1
defaults: # optional, applies to all monitors below
schedule: { cron: "0 * * * *" }
monitors:
orders_volume: # key: ^[a-z][a-z0-9_]{0,63}$
type: volume
sensitivity: medium
orders_freshness:
type: freshness
freshness_threshold: { hours: 6 }
orders_negative_revenue:
type: custom
name: Orders with negative revenue
sql: |
SELECT order_id FROM [[ this ]] WHERE net_revenue < 0
fail_when: { operator: gt, value: 0 }
investigation_instructions: Check recent pricing and refund changes.
```
## Repository file
```yaml
version: 1
defaults:
schedule: { cron: "0 */6 * * *" }
models:
- name: fct_orders # or unique_id: model.<project>.fct_orders
defaults:
schedule: { cron: "30 * * * *" }
monitors:
orders_volume: { type: volume, sensitivity: high }
```
Defaults apply file → entry → monitor. Use `unique_id` for ambiguous or package models.
## Types
**custom**: `sql` (required) is one SELECT returning the offending rows; Sidecar counts them. `fail_when` (required): `operator` in `gt gte lt lte eq neq`, integer `value`; fails when `count <operator> value`. Enforced: a single statement, no DDL or DML, no Jinja or dbt macros, warehouse SQL dialect, and any referenced model must be in the warehouse the Sidecar source runs custom SQL on. Recommended: reference tables as `[[ this ]]` or `[[ ref('model') ]]` rather than literal names, and omit the trailing semicolon.
**volume**: `sensitivity` in `low medium high` (default `medium`). Model must be materialized as a table.
**freshness**: `freshness_threshold` (required) with `days` and/or `hours`, at least one above zero. `freshness_mode`: `row_count_change` (default) or `last_updated` (not on Redshift). Table materialization required.
## Common fields
- `name`: display name; defaults to the key.
- `schedule`: `{cron: "..."}` (15 min minimum), `{mode: interval, every_hours: 1|2|3|4|6|8|12, days_of_week: [...]}` (Monday = 0), or `{mode: specific_hours, hours: [...], days_of_week: [...]}`. Integers only; `true` and `1.0` are rejected. Every monitor needs a schedule from itself, its defaults, or the Sidecar source.
- `notification_destinations`: a list. Omit for source defaults.
```yaml
notification_destinations:
- { type: slack, channel_id: C0123456789, channel_name: data-alerts }
- { type: email, email: data-team@yourcompany.com } # company domain only
```
- `investigation_instructions`: short runbook for the Reliability agent on failure.
No other fields. Unknown fields block the definition. There is no `enabled`; pausing happens in the Sidecar UI.
## Edits
- Threshold, SQL, schedule, name: edit in place; history is kept.
- Remove: delete the block; the monitor is retired, runs are kept.
- Type or table change: blocked. Delete and add under a new key.
## Checklist
Keys valid and unique per model. `version: 1` present. References are `[[ this ]]` / `[[ ref() ]]` and columns exist. Each monitor resolves a schedule. Destinations are a list. YAML parses.
After merge the user runs **Observability → Monitors → Sync from dbt**. A blocked definition's error code names the rule above that was broken; errors such as `table_not_found` or `adoption_required` come from the Sidecar side, not the YAML.