# Filtering error rows


Setup: the imports and the `raw_orders` asset every example on this page uses

``` python
import dagster as dg
import polars as pl

import dagster_dataframely as dd
from dagster_dataframely.data import OrdersSchema, ParquetIOManager, storefront_orders


@dg.asset
def raw_orders() -> pl.DataFrame:
    return storefront_orders
```


With `filter=True`, a run writes the rows that failed an `"error"` rule to a separate table, the error rows. It writes every other row to the asset's table, and the run succeeds. [Validation](validation.md) covers the default, and this page covers what `filter=True` changes:

- **The package writes the error rows under `<name>__error_rows`.** `error_rows_key` sets another key.
- **The run succeeds** when some rows fail an `"error"` rule, and fails with [NoValidRowsError](../reference/errors.NoValidRowsError.md#dagster_dataframely.errors.NoValidRowsError) when every row does.
- **No rule's check is blocking**, but a failing `"error"` rule's check still fails at `ERROR`, so the asset shows "Degraded".
- **The materialization** reports the error-rows address, count, sample and the sets of rules the rows failed together.
- **A direct invocation with error rows needs `DAGSTER_DATAFRAMELY_ERROR_ROWS_DIR`.** [Testing an asset](testing-an-asset.md) covers it.


*\[Rich HTML output -- view on the documentation site\]*


The package writes the error rows right after `Schema.filter`, so they are written even when the run then fails. Then it writes every row that passed every `"error"` rule to the table, including the rows that failed only `"warn"` rules. The two tables never share a row, so their row counts add up to the frame's.

| what the function returned | the table | the error rows | failing checks | run |
|----|----|----|----|----|
| columns or dtypes that are not the schema's | not written | not written | the column-schema check | fails, [ColumnSchemaError](../reference/errors.ColumnSchemaError.md#dagster_dataframely.errors.ColumnSchemaError) |
| no row failed an `"error"` rule | every row | written empty | the check of each `"warn"` rule a row failed, at `WARN` | succeeds |
| some rows failed an `"error"` rule | every other row | the rows that failed an `"error"` rule | `"error"` rules at `ERROR`, `"warn"` rules at `WARN`, none blocking | succeeds |
| every row failed an `"error"` rule | not written | every row | the same | fails, [NoValidRowsError](../reference/errors.NoValidRowsError.md#dagster_dataframely.errors.NoValidRowsError) |
| `None`, meaning no source data | skipped | not written | none | succeeds |

**A run where every row fails an `"error"` rule still fails.** It writes no table rather than an empty one, so an empty table never replaces the last-known-good table. It writes every row under `<name>__error_rows` before it raises [NoValidRowsError](../reference/errors.NoValidRowsError.md#dagster_dataframely.errors.NoValidRowsError), so you can read them after the run fails.


<figure class="figure">
<p><img src="../assets/images/error-rows-checks.png" class="img-fluid figure-img" /></p>
<figcaption>The Checks tab lists the checks of an asset with <code>filter=True</code> after some rows failed. The selected check is non-blocking, and it shows its rendered constraint, the rule's expression, and its failure examples.</figcaption>
</figure>


# The asset's IO manager writes the error rows

In a run, `filter=True` is the whole setup. There is no directory to configure and no second asset to declare.

The asset's own IO manager writes the error rows, under the asset's key with `__error_rows` added to the end. It writes the error rows of `analytics/orders` to `analytics/orders__error_rows`. This works with any IO manager that stores by asset key ([ADR-0006](https://github.com/ozanozbeker/dagster-dataframely/blob/main/docs/pre-1.0.md#adr-0006-the-assets-own-io-manager-writes-the-quarantine)).

With the same declaration, each IO manager writes the error rows to a different place:


``` python
@dd.asset(OrdersSchema, filter=True)
def orders(raw_orders: pl.DataFrame) -> pl.DataFrame:
    return raw_orders
```


| IO manager | where it writes the error rows |
|----|----|
| [ParquetIOManager](../reference/data.ParquetIOManager.md#dagster_dataframely.data.ParquetIOManager) | `orders__error_rows.parquet`, in the same directory as `orders.parquet` |
| `DuckDBPolarsIOManager` | the table `orders__error_rows`, in the same schema as `orders` |

The first is a `UPathIOManager` and the second is a `DbIOManager`. Nearly every first-party IO manager subclasses one of the two.

When some rows fail an `"error"` rule, the table's materialization has the error-rows address, the number of error rows, a sample of them, and which sets of rules they failed together. See [What a run produces](what-a-run-produces.md).

**A run with no error rows writes them empty**, so they no longer hold the last run's rows. The empty error rows have the same columns as non-empty ones. So the error rows exist after a clean run too. Read their row count, or check whether the materialization has `dataframely/error_rows_count`.

**A partitioned asset on a database IO manager needs `partition_expr`.** `DbIOManager` raises an error without it, so you already set it for the asset's own table. The error-rows write uses the same value.


# The error rows have one rule column per rule

The error rows have the original columns, then one `String` rule column per rule, with `valid`, `invalid` or `unknown` in each row. A rule column has the check name its rule gets at the default `rule` granularity, such as `dy_rule__amount__min`. At `column` granularity, use the rule column, not a check, to find the rows that failed a rule. A check's failure examples have at most `max_failure_examples` rows per rule, so the rule columns are the only complete record of which rules each error row failed.

The error rows are not in the format `FailureInfo.write_parquet` writes, so `FailureInfo.read_parquet` and `FailureInfo.scan_parquet` cannot read them, for the reason [the out-of-scope record](https://github.com/ozanozbeker/dagster-dataframely/blob/main/docs/out-of-scope/failure-info-format.md) gives. The rule columns also have check names, not the name Dataframely gives each rule, such as `amount|min`.


# Two keywords change the key and the IO manager

`error_rows_key` sets the error-rows key. `error_rows_io_manager_key` names the resource key of the IO manager that writes the error rows in a run. Both need `filter=True`. With `filter=False`, `dd.asset` raises `dg.DagsterInvalidDefinitionError` where you declare the asset. Neither is a setting, so no `DAGSTER_DATAFRAMELY_*` variable sets them.


``` python
from dagster_duckdb_polars import DuckDBPolarsIOManager


@dd.asset(
    OrdersSchema,
    filter=True,
    error_rows_key=["audit", "orders__error_rows"],
    error_rows_io_manager_key="audit",
)
def audited_orders(raw_orders: pl.DataFrame) -> pl.DataFrame:
    return raw_orders


defs = dg.Definitions(
    assets=[
        raw_orders,
        audited_orders,
        dd.build_error_rows_spec(OrdersSchema, audited_orders),
    ],
    resources={
        "io_manager": ParquetIOManager(base_dir="data/warehouse"),
        "audit": DuckDBPolarsIOManager(database="data/warehouse.duckdb"),
    },
)
```


[ParquetIOManager](../reference/data.ParquetIOManager.md#dagster_dataframely.data.ParquetIOManager) writes the table to `audited_orders.parquet`. `DuckDBPolarsIOManager` writes the error rows to the table `orders__error_rows` in the schema `audit`. The error-rows address is `audit/orders__error_rows`.

**`dd.asset` adds `error_rows_io_manager_key` to the asset's `required_resource_keys`.** So loading the definitions raises if no resource has that key. The IO manager receives its own config and the asset's definition metadata, such as `partition_expr`.

**A database IO manager reads the database schema from the metadata first.** `DbIOManager` takes the schema from `metadata["schema"]`, then from its own `schema` config, then from the key prefix. So `error_rows_key` changes the schema only when the asset has no `schema` metadata and the IO manager has no `schema` config. The table name is always the key's last part.

**`error_rows_key` also changes the key a run checks for a collision ([Errors](errors.md)).** Before the decorated function runs, a run raises [ErrorRowsKeyCollisionError](../reference/errors.ErrorRowsKeyCollisionError.md#dagster_dataframely.errors.ErrorRowsKeyCollisionError) if another asset materializes the key `error_rows_key` sets. The run also raises when that key is the asset's own key.

**A direct invocation writes under [`error_rows_dir`](testing-an-asset.md#where-a-call-writes-the-error-rows), at a path built from the error-rows key.** It has no IO manager, so `error_rows_io_manager_key` changes nothing there. Dagster still checks the asset's required resources, so pass a value for that resource key, such as `dg.build_asset_context(resources={"audit": None})`.


# [build_error_rows_spec](../reference/build_error_rows_spec.md#dagster_dataframely.build_error_rows_spec) adds the error rows to the asset graph

The error rows are not in the asset graph, so no asset can depend on them until you add them with [build_error_rows_spec](../reference/build_error_rows_spec.md#dagster_dataframely.build_error_rows_spec):


``` python
defs = dg.Definitions(
    assets=[raw_orders, orders, dd.build_error_rows_spec(OrdersSchema, orders)],
    resources={"io_manager": ParquetIOManager(base_dir="data/warehouse")},
)
```


<figure class="figure">
<p><img src="../assets/images/error-rows-lineage.png" class="img-fluid figure-img" /></p>
<figcaption>The lineage view shows the error rows' node. Its one edge comes from <code>marketing_orders</code>, the asset with <code>filter=True</code>, not from the upstream <code>raw_marketplace_orders</code>. Dagster draws the node with a dashed border because it is an external asset, and it shows the materialization that the step of <code>marketing_orders</code> logged.</figcaption>
</figure>


[build_error_rows_spec](../reference/build_error_rows_spec.md#dagster_dataframely.build_error_rows_spec) returns a `dg.AssetSpec` keyed `orders__error_rows` that depends on `orders` and has its own Columns tab. It has no compute function, because the asset's own step writes the rows. It has the `polars` kind, as the asset does, but none of the asset's other kinds. To add a storage kind such as `duckdb`, call `.merge_attributes(kinds={"duckdb"})` on the spec. The IO manager that wrote the rows also loads them for a downstream asset that names the error rows as an input.

**The error rows' Columns tab has no column constraints.** It lists the schema's columns with their dtypes, descriptions and tags, then one `String` column per rule. The error rows can fail any of the schema's constraints, so a `not null` on a column full of nulls would be false. The primary key is absent too, because rows with a duplicate key are among the error rows.

Pass [build_error_rows_spec](../reference/build_error_rows_spec.md#dagster_dataframely.build_error_rows_spec) the asset definition, and the spec uses its partitions, its asset group, its owners and its tags. So the catalog lists the error rows in the same group as the asset. The spec also uses the error-rows key, the IO manager key and the definition metadata the writer used, so a downstream asset loads the rows from where they were written. `dd.asset` records the first two in the definition metadata, under `dataframely/error_rows_key` and `dataframely/error_rows_io_manager_key`. For an asset you cannot import, pass its asset key instead, and set the partitions with `partitions_def=`. The spec's key is then `<name>__error_rows`, and its IO manager key is Dagster's default, `io_manager`. For an asset whose IO manager has another key, call `.with_io_manager_key()` on the spec. Passing both an asset definition and `partitions_def=` raises `dg.DagsterInvariantViolationError`.

`group_name=` sets the error rows' group. With an asset definition, it overrides the asset's group and does not raise. For example, pass `group_name="error_rows"` on every call to list every asset's error rows in one group. With an asset key and no `group_name=`, the error rows are in the `default` group.

`owners=` and `tags=` follow the same rule. With an asset definition, each replaces the asset's value whole, so `owners=[]` removes the asset's owners and `tags={}` removes its tags. With an asset key, the error rows have the owners and tags these set, and the `polars` kind.

**With the spec, every run that writes the error rows materializes them.** The asset's own step logs the materialization after the IO manager writes the rows. It logs one for each kind of run that writes them: a clean run's empty write, a run where some rows failed, and the write before [NoValidRowsError](../reference/errors.NoValidRowsError.md#dagster_dataframely.errors.NoValidRowsError). The materialization has the IO manager's metadata for the write, such as `path` or `dagster/table_name`, and the row count. The row count is under `dagster/partition_row_count` for a partition, and under `dagster/row_count` otherwise.


``` python
import tempfile

from dagster_dataframely.data import marketplace_orders


@dg.asset(name="raw_orders")
def marketplace_raw() -> pl.DataFrame:
    return marketplace_orders


with tempfile.TemporaryDirectory() as tmp:
    result = dg.materialize(
        [marketplace_raw, orders, dd.build_error_rows_spec(OrdersSchema, orders)],
        resources={"io_manager": ParquetIOManager(base_dir=tmp)},
    )

{
    event.asset_key.to_user_string(): event.materialization.metadata[
        "dagster/row_count"
    ].value
    for event in result.get_asset_materialization_events()
    if event.asset_key != marketplace_raw.key
}
```


    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - RUN_START - Started execution of run for "__ephemeral_asset_job__".
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - ENGINE_EVENT - Executing steps in process (pid: 3990)
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - RESOURCE_INIT_STARTED - Starting initialization of resources [io_manager].
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - RESOURCE_INIT_SUCCESS - Finished initialization of resources [io_manager].
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - LOGS_CAPTURED - Capturing logs for process (pid: 3990).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - raw_orders - STEP_START - Started execution of step "raw_orders".
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - raw_orders - STEP_OUTPUT - Yielded output "result" of type "DataFrame". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - raw_orders - Writing file at: /tmp/tmpys9f_eqc/raw_orders.parquet using ParquetIOManager...
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - raw_orders - ASSET_MATERIALIZATION - Materialized value raw_orders.
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - raw_orders - HANDLED_OUTPUT - Handled output "result" using IO manager "io_manager"
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - raw_orders - STEP_SUCCESS - Finished execution of step "raw_orders" in 11ms.
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_START - Started execution of step "orders".
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - orders - Loading file from: /tmp/tmpys9f_eqc/raw_orders.parquet using ParquetIOManager...
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - LOADED_INPUT - Loaded input "raw_orders" using input manager "io_manager", from output "result" of step "raw_orders"
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_INPUT - Got input "raw_orders" of type "DataFrame". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - orders - Writing file at: /tmp/tmpys9f_eqc/orders__error_rows.parquet using ParquetIOManager...
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_MATERIALIZATION - Materialized value orders__error_rows.
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "result" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - orders - Writing file at: /tmp/tmpys9f_eqc/orders.parquet using ParquetIOManager...
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_MATERIALIZATION - Materialized value orders.
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - HANDLED_OUTPUT - Handled output "result" using IO manager "io_manager"
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_schema__columns" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_schema__columns' on 'orders' passed. Description: 'Every column matches.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__paid_orders_have_amount" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__paid_orders_have_amount' on 'orders' did not pass. Description: '1 row failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__line_numbers_are_dense" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__line_numbers_are_dense' on 'orders' did not pass. Description: '2 rows failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__primary_key" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__primary_key' on 'orders' passed. Description: 'No row failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__order_id__nullability" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__order_id__nullability' on 'orders' passed. Description: 'No row failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__order_id__regex" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__order_id__regex' on 'orders' did not pass. Description: '1 row failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__line_no__nullability" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__line_no__nullability' on 'orders' passed. Description: 'No row failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__line_no__min" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__line_no__min' on 'orders' passed. Description: 'No row failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__email__nullability" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__email__nullability' on 'orders' passed. Description: 'No row failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__email__check__lowercase" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__email__check__lowercase' on 'orders' did not pass. Description: '2 rows failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__email__max_length" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__email__max_length' on 'orders' passed. Description: 'No row failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__amount__nullability" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__amount__nullability' on 'orders' passed. Description: 'No row failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__amount__min" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__amount__min' on 'orders' did not pass. Description: '2 rows failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__quantity__nullability" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__quantity__nullability' on 'orders' passed. Description: 'No row failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__quantity__min" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__quantity__min' on 'orders' passed. Description: 'No row failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__quantity__max" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__quantity__max' on 'orders' did not pass. Description: '1 row failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__priority__is_in" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__priority__is_in' on 'orders' did not pass. Description: '1 row failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__status__nullability" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__status__nullability' on 'orders' passed. Description: 'No row failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__tracking_id__unique" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__tracking_id__unique' on 'orders' passed. Description: 'No row failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__is_gift__nullability" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__is_gift__nullability' on 'orders' passed. Description: 'No row failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__ordered_at__nullability" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__ordered_at__nullability' on 'orders' passed. Description: 'No row failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__tags__max_length" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__tags__max_length' on 'orders' passed. Description: 'No row failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__tags__inner_nullability" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__tags__inner_nullability' on 'orders' passed. Description: 'No row failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_OUTPUT - Yielded output "orders_dy_rule__note__check" of type "Any". (Type check passed).
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - ASSET_CHECK_EVALUATION - Asset check 'dy_rule__note__check' on 'orders' passed. Description: 'No row failed.'
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - orders - STEP_SUCCESS - Finished execution of step "orders" in 167ms.
    /home/runner/work/dagster-dataframely/dagster-dataframely/.venv/lib/python3.12/site-packages/dagster/_core/execution/context_creation_job.py:273: RuntimeWarning: coroutine 'BaseEventLoop.shutdown_asyncgens' was never awaited
      pass
    RuntimeWarning: Enable tracemalloc to get the object allocation traceback
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - ENGINE_EVENT - Finished steps in process (pid: 3990) in 191ms
    2026-10-22 02:11:39 +0000 - dagster - DEBUG - __ephemeral_asset_job__ - 84c752bf-55fd-4ecb-b958-39dba8971103 - 3990 - RUN_SUCCESS - Finished execution of run for "__ephemeral_asset_job__".


    {'orders__error_rows': 8, 'orders': 12}


**`dd.asset` passes `automation_condition` and `freshness_policy` to `dg.asset` unchanged**, and they apply to the asset only.

**An asset downstream of the error rows' spec can automate on the error rows.** `dg.AutomationCondition.eager()` then requests a run after each write of the error rows:


``` python
@dd.asset(OrdersSchema, filter=True)
def checked_orders(raw_orders: pl.DataFrame) -> pl.DataFrame:
    return raw_orders


checked_orders__error_rows = dd.build_error_rows_spec(OrdersSchema, checked_orders)


@dg.asset(
    deps=[checked_orders__error_rows],
    automation_condition=dg.AutomationCondition.eager(),
)
def triage_queue() -> None: ...
```


**A clean run materializes the error rows too**, because it writes them empty. So `triage_queue` runs after every run of `checked_orders` that writes the error rows, not only after a run where rows failed.

**Without the spec, a run launched from a code location records no materialization for the error rows**, so the catalog does not list them as an asset with no definition. A run of `dg.materialize` or `execute_in_process` has no code location to read, so it records the materialization with or without the spec.
