> For the complete documentation index, see [llms.txt](https://v2.dataos.info/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://v2.dataos.info/build/v1/productize/assertions.md).

# Assertions

An assertion is the blocking layer of the contract. It runs every time a model materializes, and if it finds bad data, Vulcan stops the run. Nothing bad reaches a consumer. Where a [DQ check](/build/v1/productize/data-quality.md) watches data and warns, an assertion blocks it.

Two terms travel together here, and they don't mean the same thing:

* **Audit** - the validation rule itself: a named SQL query in `audits/*.sql` that returns rows when data is bad.
* **Assertion** - the attachment of a rule to a model: a declaration inside the `MODEL(...)` block saying the model must pass that rule.

In short: audits define, assertions attach. A model's `assertions(...)` block can attach built-in functions (no SQL needed) or named audits you wrote yourself. Both block the run when they fail. `audits(...)` is an accepted alias for `assertions(...)` on the `MODEL` block. Both work identically; this guide uses `assertions`.

## How it runs

An assertion passes when its query returns zero rows. Any row it returns counts as a failing row. For every model run:

1. Vulcan evaluates the model SQL and inserts the data.
2. It runs each attached assertion against the new data.
3. If any assertion returns rows, execution stops and Vulcan reports the failure.
4. If every assertion returns zero rows, Vulcan promotes the model.

## Inline assertions (start here)

The fastest way to add a check is a built-in function in the `MODEL` block's `assertions` property. You don't need a SQL file:

```sql
MODEL (
  name silver.fct_daily_sales,
  kind FULL,
  grains (order_date),
  assertions (
    not_null(columns := (order_date, total_revenue)),
    unique_values(columns := (order_date)),
    accepted_range(column := total_revenue, min_v := 0)
  )
);
```

Common built-in functions:

| Category      | Functions                                                                                                 |
| ------------- | --------------------------------------------------------------------------------------------------------- |
| Null and rows | `not_null`, `at_least_one`, `not_null_proportion`, `number_of_rows`                                       |
| Uniqueness    | `unique_values`, `unique_combination_of_columns`                                                          |
| Value         | `accepted_values`, `not_accepted_values`, `accepted_range`, `not_constant`, `forall`, `sequential_values` |
| String        | `not_empty_string`, `string_length_between`, `valid_uuid`, `valid_email`, `valid_url`                     |
| Statistical   | `mean_in_range`, `z_score`                                                                                |

`forall(criteria := (price > 0, LENGTH(name) > 0))` is the escape hatch: it accepts any boolean SQL expression.

## Custom audits

When a built-in function can't express the rule, write a custom audit: a `.sql` file in `audits/` that returns the offending rows. `orders-analytics` has 3, each attached to a model as an assertion, which is what makes it blocking. This one checks that the derived metrics in `silver.fct_daily_sales` are internally consistent:

```sql
AUDIT (
  name daily_sales_metric_consistency
);

SELECT order_date, region_id, total_orders, total_revenue, avg_order_value
FROM @this_model
WHERE total_orders <= 0
   OR total_revenue < 0
   OR shipment_rate < 0
   OR shipment_rate > 1
   OR ROUND(total_revenue / NULLIF(total_orders, 0), 2) <> avg_order_value;
```

`@this_model` refers to the model the audit is attached to. For incremental models, Vulcan adds an automatic time-range filter, so the audit only checks the interval that just ran.

Attach the audit by naming it in the model's `assertions` block, alongside any built-in functions:

```sql
MODEL (
  name silver.fct_daily_sales,
  assertions (
    not_null(columns := (order_date, region_id)),
    daily_sales_metric_consistency()
  )
);
```

A custom audit is reusable: define it once, attach it to any model. The `orders-analytics` audits also validate recency, frequency, and monetary (RFM) scores, and check referential integrity against the order-status seed.

For parameterized audits, global assertions via `model_defaults`, and more patterns, see [References → Vulcan → Quality → Assertions](https://v2.dataos.info/references/resources/vulcan/quality/assertions).

## Plan versus run

| Command       | On failure                                                                                                                                                            |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `vulcan plan` | Production is untouched. The bad data stays in the isolated dev table.                                                                                                |
| `vulcan run`  | The model writes to the production table first, then the assertion runs. On failure, the run halts: the bad rows can land, but downstream models won't build on them. |

Use `vulcan plan` for production changes: it catches the failure before bad data lands. Run audits manually with `vulcan audit --start <date> --end <date>`, adding `--verbose` to see the failing query and rows.

An assertion is your block against bad data reaching a consumer. To verify the model's logic is right in the first place, before any warehouse run happens, write a [test](/build/v1/productize/unit-test.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://v2.dataos.info/build/v1/productize/assertions.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
