Skip to content
>GLB_
Go back

Connecting Clinical and Operational Data to Tuva with dbt

>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-partTypical sources
Claims837/835/834-derived claims, eligibility, pharmacy
ClinicalEHR, ADT, FHIR, labs
Provider AttributionAttribution 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:

  1. Leave Tuva untouched.
  2. Build your own models in the same dbt project, downstream of Tuva Core.
  3. 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

References


Share this post:

Related Posts


Previous Post
Snowflake SSO from the terminal: CLI, dbt Core and DBeaver
Next Post
Tuva column extensions: carry a custom column from eligibility to core