>GLB_
// tools · tuva
Tuva is a dbt package. It does not ingest anything by itself: you map your sources to its Input Layer, and the rest of the package runs on top of that. The question I kept hitting was where clinical data and operational data each plug in, and where Tuva stops.
This post covers that boundary.
The Input Layer is the contract
The Input Layer is the contract between your source data and the Tuva package. It has three sub-parts:
| Sub-part | Typical sources |
|---|---|
| Claims | 837/835/834-derived claims, eligibility, pharmacy |
| Clinical | EHR, ADT, FHIR, labs |
| Provider Attribution | Attribution inputs |
Each sub-part is enabled independently in dbt_project.yml:
flags:
require_ref_searches_node_package_before_root: true
vars:
claims_enabled: true
clinical_enabled: true
provider_attribution_enabled: true
If you enable a sub-part you must create all of its models. If you have no data for a table, create it with null columns, or as an entirely null table.
The Input Layer models live in your dbt project. When you import Tuva as a package, tuva/models/input_layer refs the models you defined in the root project.
# packages.yml
packages:
- package: tuva-health/the_tuva_project
version: "<current-release>"
Pin an immutable release from GitHub Releases for production. The main branch is not a release.
Clinical data: write a connector
A connector is a dbt project with SQL that transforms a raw source into the Input Layer. For clinical data, that means mapping the EHR source into the clinical tables:
patient, encounter, condition, procedure, observation, lab_result, medication, immunization, appointment, plus location and practitioner.
Structure that keeps the mapping reviewable:
models/
staging/ # 1:1 with raw tables, renames and casts only
input_layer/ # one model per Tuva Input Layer table
A minimal shape for one mapping (column list is illustrative; take the exact spec from tuva/models/input_layer):
-- models/input_layer/input_layer__encounter.sql
select
cast(enc.id as varchar) as encounter_id,
cast(enc.patient_id as varchar) as patient_id,
enc.type_code as encounter_type,
enc.start_ts::date as encounter_start_date,
enc.end_ts::date as encounter_end_date,
'my_ehr' as data_source
from {{ ref('stg_ehr__encounters') }} enc
Tuva ships a library of pre-built connectors. The Healthie EHR connector is a good reference for the staging-to-Input-Layer pattern on Snowflake.
Claims and eligibility
Payer-side data goes through the Claims sub-part: eligibility, medical_claim, pharmacy_claim. Same approach: stage, then map one model per Input Layer table.
When both claims and clinical are enabled, Tuva builds a person ID crosswalk so the two identity spaces can be linked. Validate that crosswalk against your own master patient index before trusting any cross-domain metric.
Operational data: Tuva does not model it
Logistics, expense, workflow, or any internal operational dataset has no Input Layer table. Forcing it into one is the wrong move.
The pattern that works:
- Leave Tuva untouched.
- Build your own models in the same dbt project, downstream of Tuva Core.
- Join on stable keys:
patient_id/person_id,encounter_id, NPI, and dates.
-- models/marts/ops__encounter_cost_context.sql
select
e.encounter_id,
e.encounter_start_date,
o.operational_metric,
o.cost_center
from {{ ref('core__encounter') }} e
left join {{ ref('stg_ops__events') }} o
on o.encounter_id = e.encounter_id
Adjust the ref to the Core model names in your Tuva version.
Extension columns have limits
Tuva supports extension columns, but they only flow between the 14 same-named Input Layer and Core tables: appointment, condition, eligibility, encounter, immunization, lab_result, location, medical_claim, medication, observation, patient, pharmacy_claim, practitioner, procedure.
They do not propagate into derived outputs such as cost, member month, person ID crosswalk, or utilization. If you need an operational attribute in a mart, join it in a model of your own after Tuva runs.
Checklist
- Pin a released Tuva version, not
main - Enable only the sub-parts you have data for
- Create every model of an enabled sub-part, using null columns where needed
- Validate the person ID crosswalk against your own identity source
- Keep operational joins in downstream models, not in the Input Layer
- Run
dbt buildand review Tuva’s data quality tests before using any mart