Filtering error rows

Setup: the imports and the raw_orders asset every example on this page uses
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 covers the default, and this page covers what filter=True changes:

flowchart TD
    F["the decorated function returns"] --> N{"None?"}
    N -- yes --> SKIP["skip: nothing written<br/>every check passes<br/>run succeeds"]
    N -- no --> CS{"column schema matches?"}
    CS -- no --> DRIFT["nothing written<br/>dy_schema__columns fails<br/>run fails, ColumnSchemaError"]
    CS -- yes --> FILTER["Schema.filter"]
    FILTER --> ERR{"a row failed an 'error' rule?"}
    ERR -- no --> CLEAN["every row written<br/>error rows written empty<br/>run succeeds"]
    ERR -- yes --> WRITTEN["error rows written"]
    WRITTEN --> LEFT{"any other rows?"}
    LEFT -- no --> NONE["table not written<br/>run fails, NoValidRowsError"]
    LEFT -- yes --> PARTIAL["the other rows written<br/>'error' checks fail at ERROR, not blocking<br/>run succeeds"]

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
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
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, so you can read them after the run fails.

The Checks tab lists the checks of an asset with filter=True after some rows failed. The selected check is non-blocking, and it shows its rendered constraint, the rule’s expression, and its failure examples.

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).

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

@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 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.

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 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.

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 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). Before the decorated function runs, a run raises 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, 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 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:

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

The lineage view shows the error rows’ node. Its one edge comes from marketing_orders, the asset with filter=True, not from the upstream raw_marketplace_orders. Dagster draws the node with a dashed border because it is an external asset, and it shows the materialization that the step of marketing_orders logged.

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 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. 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.

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:

@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.