Module 1c — Silently ignored config#
Gate: dbt parse.
This module has the most important lesson in the lab, and it is not the one you expect. Do not skip to the fix.
The setup#
models/marts/dim_potions.sql opens like this:
{#
Small dimension -- 120 potions -- so this is a view rather than a table. No
point paying for storage and a rebuild on something this size.
#}
{{
config(
materialised = 'view'
)
}}
British spelling. dbt Core does not recognise materialised as a config key, so
it drops it silently and falls back to the project default:
Run it and watch dbt Core describe what it is doing:
1 of 1 OK created sql table model ... dim_potions
Finished running 1 table model in 5.06s
Done. PASS=1 WARN=0 ERROR=0
"1 table model." The author asked for a view. Nothing warned anybody. Check the warehouse:
select table_name, table_type
from information_schema.tables
where table_schema = 'YOUR_SCHEMA' and table_name = 'DIM_POTIONS';
This is the class of bug where someone says "why is this stale?" or "why is this costing us so much?" and nobody can see why, because the code says view and the warehouse says table.
Now the one that corrupts data#
fct_payment_events is an incremental model:
{{
config(
materialized = 'incremental',
unique_keys = 'payment_id',
incremental_strategy = 'merge'
)
}}
...
{% if is_incremental() %}
where paid_at >= (select max(paid_at) from {{ this }})
{% endif %}
unique_keys, plural. The window uses >= on purpose — a payment can land with
a paid_at slightly behind the previous high-water mark, so the boundary period
gets re-read every run. unique_key is what makes that safe: the merge matches
on it and updates rather than inserts.
With the key silently dropped, there is nothing to match on, so the merge appends. Watch it happen:
dbt run --select fct_payment_events --full-refresh
dbt run --select fct_payment_events
dbt run --select fct_payment_events
After each run:
select count(*) as total_rows,
count(distinct payment_id) as distinct_ids,
count(*) - count(distinct payment_id) as duplicates
from {{ your_schema }}.fct_payment_events;
One more duplicate every single night, forever. dbt Core reports PASS every
time.
Why no test caught it
There is no unique test on fct_payment_events.payment_id. That is
deliberate, and it is the realistic part — the absence of the test is why
nobody noticed. Adding one belongs in the fix, not just the config change.
What Core v2 says#
[error] [UnusedConfigKey (dbt1060)]: Ignored unexpected key `"materialised"`.
YAML path: `materialised`.
[error] [UnusedConfigKey (dbt1060)]: Ignored unexpected key `"unique_keys"`.
YAML path: `unique_keys`.
Both, at parse, before anything runs. It is not clever analysis — it is simply refusing to ignore a key it does not recognise.
The class is wider than these two. Probing a config block with five plausible typos, Core v2 flags all of them and dbt Core flags none:
| Typo'd key | dbt Core | Core v2 |
|---|---|---|
materialised |
silent | dbt1060 |
unique_keys |
silent | dbt1060 |
incremental_strategey |
silent | dbt1060 |
on_schema_changed |
silent | dbt1060 |
tag (for tags) |
silent | dbt1060 |
Do not let Autofix near this#
Here is the lesson. Run the dry run:
Autofix cannot tell a typo from a deliberate custom key, so it assumes you meant
it and relocates it to +meta, which is where arbitrary keys legally live:
{{ config(
materialized='incremental',
incremental_strategy='merge',
meta={'unique_keys': 'payment_id'}
) }}
Apply it and check:
Zero errors. The gate is green.
Now run the model again:
The bug is untouched. dim_potions is still a table. The incremental still has
no unique key. Autofix did exactly what it was designed to do, the parse gate
went green, and the data is still being corrupted every night — except now
the typo sits under meta, where it looks deliberate, so nobody will ever
question it again.
A clean Autofix run is not a clean project
This is the strongest reason to read --dry-run output line by line rather
than piping Autofix into a commit. Anything described as "moved custom
config X to meta" is Autofix telling you it does not know what X is. That is
a question for you, not a fix.
The July 2026 Autofix regression that mis-classified package versions is the famous example, but this is the everyday one.
The actual fix#
Spell the keys correctly:
{{
config(
materialized = 'incremental',
unique_key = 'payment_id',
incremental_strategy = 'merge'
)
}}
Then add the test whose absence let this run for months:
Fixing the config does not fix the data
The duplicates already in the table stay there. unique_key only governs
future merges, and the new unique test will now fail against the mess the
typo already made:
dbt build --select fct_payment_events # test FAILS
dbt build --select fct_payment_events --full-refresh # clean
Budget a full refresh as part of the remediation. This is generally true of silent-config bugs: by the time you find one, you own a backfill as well as a code change.
Verify:
dbt parse # Core v2, clean
dbt run --select dim_potions # "1 view model"
dbt run --select fct_payment_events # idempotent now
Not every unrecognized key is a typo#
materialised and unique_keys are both misspellings of real config keys, and
the fix for both is to spell them correctly. But dbt does not have a way to
tell "typo" from "deliberate custom key" apart — which is why Core v2 flags
both with the same dbt1060, and why Autofix relocates both to meta without
asking.
fct_regulated_potion_sales shows the other case:
compliance_owner is not a typo of anything. Compliance actually wants to know
who to call about a given regulator-facing table, and the house audit_table
materialization reads it and stamps it into every ledger row:
{%- set compliance_owner = config.get('compliance_owner', 'unassigned') -%}
...
insert into {{ ledger_relation }} (..., compliance_owner)
select ..., '{{ compliance_owner }}'
This is exactly the shape CustomKeyInConfigDeprecation exists for: a custom
config key dbt does not recognize as part of the official spec, used in a
config block in a SQL file (it applies the same way to YAML config: blocks).
Run it with dbt Core:
[WARNING][CustomKeyInConfigDeprecation]: Deprecated functionality
Custom config key `compliance_owner` found. Custom config keys should be
nested under the `meta` config.
Unlike materialised and unique_keys, dbt Core does not discard this
value — compliance_owner still works, the ledger still gets stamped, nothing
is silently broken. This is purely a "get ahead of the v2 upgrade" warning: on
Core v2, an unrecognized top-level key is a hard parse error regardless of
whether it was a typo or on purpose.
The fix is the one PropertyMovedToConfigDeprecation already trained you for
— nest it under meta, do not rename it:
And because a macro actually reads this key, the macro has to move with it —
Autofix rewrites the model's config, but it will not touch
macros/materializations/audit_table.sql for you:
Skip that second edit and the fix only looks complete: dbt parse goes
green, but the materialization silently falls back to 'unassigned' for
every regulator-facing table, and Compliance loses the one thing this
whole mechanism exists to give them.
Why Autofix is safe to trust here, and wasn't above
For compliance_owner, "move it to meta" is genuinely correct — there is
no typo underneath it to paper over. That is the difference between this
key and materialised/unique_keys: the risk was never in where
Autofix put the key, it was in Autofix being unable to tell the two cases
apart. Read the dry run either way.
Takeaways#
- dbt Core silently drops config keys it does not recognise. Intent and reality diverge with no signal at all.
- Consequences run from "wrong object type and a surprising bill" to "one duplicate row per night, forever, reported as PASS".
- Core v2 catches the whole class at parse with
dbt1060. - Autofix will move a typo'd key to
meta, turning the error green while leaving the bug in place and making it look intentional. Read the dry run. - Fix the key, add the test whose absence hid it, and plan the backfill.
- Not every unrecognized key is a mistake.
CustomKeyInConfigDeprecationwarns about deliberate custom keys too — same message, samemetafix asPropertyMovedToConfigDeprecation, but this time the fix is "move it," not "spell it correctly." If a macro reads the key directly, moving it undermetameans updating the macro too, or the fix only looks complete.
Solution branch: solution/01-deprecations