// data engineering · tuva
TL;DR
Prefix a custom column with x_ in your connector’s eligibility model. It arrives in core__eligibility with no YAML and no edits to Tuva’s models. One dbt var (passthrough.strip) removes the prefix in the final Core output.
The problem
I needed contract_type in core__eligibility. The field exists in the source data, but it is not part of Tuva’s standard eligibility contract.
Tuva’s Core models use explicit column lists. Without pass-through support, custom fields are preserved in input_layer but dropped in downstream core models that use explicit selections. The column is present in your data, then missing where you need it.
What I tried
These are the options I looked at before finding the supported one.
Option 1: edit core__eligibility.sql
It works, but you are now maintaining a fork. Every package upgrade means re-applying the patch, and it puts business logic inside a package-owned model.
Option 2: rejoin downstream
Build a separate model that joins core.eligibility back to the source on member_id and data_source to recover contract_type. It also works, but it means more custom SQL in host projects, more tables/views to manage, and higher maintenance and compute costs.
Option 3: column extensions
Tuva has a documented mechanism for this case. A connector adds organization-specific columns to a supported Input Layer Model and retains them in the same-named Core table. The docs call it column extensions, and it is the only one of the three that needs no patching and no rejoin.
What actually worked
How the mechanism works
The path is deliberately narrow. Column extensions do not propagate arbitrary columns through the Tuva DAG. The flow is:
connector Input Layer Model (eligibility)
-> Tuva Input Layer Wrapper (input_layer__eligibility)
-> intermediate + normalized (int_eligibility_casting, normalized__eligibility)
-> Core Model (core__eligibility)
Three rules define the contract:
- Prefix-based detection. A column is an extension column if its name starts with the configured prefix (default
x_). Two vars control this:passthrough.prefix(default'x_') andpassthrough.strip(defaultfalse). - Introspection, not declaration. The macro
select_extension_columns(...)inspects the upstream relation’s columns. Nothing is listed in YAML. Matching is case-insensitive, and the macro validates collisions against Tuva’s fixed columns. - Each layer appends them itself. Core staging/final models call
select_extension_columns(...)to append extension columns dynamically. The column does not travel on its own. Each layer’s model has the macro call in its SQL.
In this repo, the call sites for eligibility are:
| Model | Call |
|---|---|
int_eligibility_casting | select_extension_columns(ref('input_layer__eligibility'), alias='elig', strip_prefix=false) |
normalized__eligibility | same pattern over int_eligibility_casting |
core__eligibility | select_extension_columns(ref('normalized__eligibility')) |
The intermediate and normalized relations retain the extension prefix and are not the supported destination. Only the Core table is the supported place to read the column.
Step 0: check the Tuva version
The contract described here is Tuva Core v1.0.0, and earlier Core versions use legacy extension behavior. Pass-through itself was first introduced in v0.17.0. Confirm what your connector resolves to:
# packages.yml
packages:
- package: tuva-health/the_tuva_project
version: [">=1.0.0", "<2.0.0"] # example range, verify against the guide
If you started from a Connector Template revision whose package range ends below 1.0, align that dependency first.
Step 1: add the column in your Input Layer Model
This is the connector-owned eligibility model, the one input_layer__eligibility references. Add the field with the prefix:
-- models/input_layer/eligibility.sql (connector project)
select
person_id
, member_id
, enrollment_start_date
, enrollment_end_date
, payer
, plan
-- ... rest of the Tuva eligibility contract ...
, contract_type as x_contract_type -- extension column
, data_source
from {{ source('raw', 'eligibility_feed') }}
That is the only change to your code.
Step 2: build and verify
dbt build --select +core__eligibility
select x_contract_type, count(*) as members
from core.eligibility
group by 1
order by 2 desc;
If x_contract_type is in core.eligibility, the path works end to end.
Step 3 (optional): drop the prefix in Core
To get contract_type instead of x_contract_type:
# dbt_project.yml
vars:
passthrough:
prefix: 'x_'
strip: true
strip only affects the final Core output. The intermediate layers are expected to keep the prefix. Read strip as “rename at the last hop”. It does not rename the column everywhere.
Gotchas
-
Non-default prefix.
select_extension_columns()only emits columns matching the configured prefix. If a project sets a different prefix such asext_,x_columns get filtered out of the model output. Tuva’s own unit tests had to be made hermetic for this reason. If you write unit tests on Core models, pin the config inside each test:overrides: vars: passthrough: prefix: 'x_' strip: false -
Supported tables only. The mechanism works for supported Input Layer to Core pairs. Derived outputs such as
member_monthare excluded.eligibilityis a supported pair, and the Tuva blog usesx_member_tieroneligibilityas an example. -
Same grain. The column must live at the grain of the target table. Use a column extension when the value belongs at the same grain as that table but is not part of Tuva’s standard contract.
contract_typeper enrollment span fits. A per-claim attribute does not belong oneligibility. -
Collisions. The macro validates extension names against fixed columns. If a build fails there, rename your column instead of fighting the check.
What this does not do: filtering rows
Column extensions expose a column. They do not filter rows. If you need core__eligibility restricted to one contract_type, that is business logic, and it belongs in a downstream model in your project:
-- models/marts/eligibility_by_contract.sql (downstream project)
select *
from {{ ref('core__eligibility') }}
where x_contract_type = 'medicare_advantage' -- illustrative value
Editing core__eligibility.sql means forking the package. It is the same problem as Option 1.
Key takeaways
- Name the column
x_<name>in the connector’s Input Layer Model and nothing else needs to change. No YAML, no fork, no rejoin. - The mechanism is a documented contract in Tuva Core 1.0, backed by unit tests and an integration contract test. It is not a hack.
- Extension columns terminate at the Core table. Intermediate and normalized layers keep the prefix by design.
passthrough.strip: truerenames only in the final Core output.- Passthrough exposes columns. Row filtering goes in a downstream model.
References
- Tuva docs, Column Extensions guide: https://thetuvaproject.com/guides/column-extensions
- Tuva blog, Customizing the Tuva Data Model (v0.17.0 pass-through): https://thetuvaproject.com/blog/customizing-the-tuva-data-model
tuva-core,integration_tests/dbt_project.yml(passthroughvars): https://github.com/tuva-health/tuva-core/blob/main/integration_tests/dbt_project.ymltuva-corePR #1350, hermetic extension-column unit tests: https://github.com/tuva-health/tuva-core/pull/1350
16 Oct 2026 · #tuva #dbt #dataengineering #healthcaredata
GLB_