Airflow DAG for processing OMOP EHR deliveries in Cloud Composer.
The DAG id is ehr-pipeline.
For each site delivery, the pipeline:
- Finds the most recent delivery folder in the site's GCS bucket.
- Exports Connect reference data for the site.
- Converts source files to Parquet.
- Validates and normalizes OMOP files.
- Upgrades delivered CDM versions when required.
- Rewrites natural-key columns (PK + FK) so values are globally unique across sites.
- Filters participants using Connect status data.
- Runs vocabulary harmonization for supported clinical tables.
- Generates derived tables.
- Loads the processed data to BigQuery.
- Runs DQD, Achilles, PASS, and delivery reporting.
This repository contains the Composer DAG and task helpers. The heavy processing is executed by external services:
OMOP_PROCESSOR_ENDPOINThandles file conversion, validation, normalization, CDM upgrades, vocabulary work, BigQuery preparation, and related OMOP operations.OMOP_ANALYZER_ENDPOINThandles DQD, Achilles, Atlas table creation, and report generation.
The DAG coordinates site discovery, task ordering, retries, and pipeline logging.
- Google Cloud Composer
- BigQuery datasets for CDM and analytics results
- Per-site GCS buckets with deliveries stored as top-level
YYYY-MM-DDfolders - Reachable processor and analyzer services
- Composer worker permissions to:
- read and write the configured GCS buckets
- invoke the processor and analyzer services
- create, truncate, and load BigQuery tables
The DAG reads site configuration from:
/home/airflow/gcs/dags/dependencies/ehr/config/site_config.yml
Example:
site:
site_name:
display_name: "Site Display Name"
gcs_bucket: "my-site-bucket"
file_delivery_format: ".csv"
project_id: "gcp-project-id"
cdm_bq_dataset: "omop_cdm"
analytics_bq_dataset: "omop_analytics"
cdm_version: "5.3"
date_format: "%Y-%m-%d"
datetime_format: "%Y-%m-%d %H:%M:%S"
overwrite_site_vocab_with_standard: true
site_connect_id: 123456789
post_processing: 'remove_text-to-concept_measurements, some_other_task'Fields used by the DAG:
-
display_name: human-readable site name used in logs and reports -
gcs_bucket: bucket name only, withoutgs:// -
file_delivery_format: expected source file format, typically.csvor.csv.gz -
project_id: GCP project for BigQuery operations -
cdm_bq_dataset: target OMOP dataset -
analytics_bq_dataset: target analytics dataset for DQD, Achilles, PASS, and Atlas outputs -
cdm_version: delivered CDM version -
date_formatanddatetime_format: source formatting hints for normalization -
overwrite_site_vocab_with_standard: whentrue, standard vocabulary is loaded and site vocab tables are skipped -
site_connect_id: Connect identifier used for site-level participant export -
post_processing: optional comma-delimited string of post-processing task names to run after vocabulary harmonization, written on a single line. Each name must correspond to a SQL file atreference/sql/post_processing/<task_name>.sqlin the OMOP file processor repo. Tasks for a given site run in series in the order they appear in the string; different sites' tasks run in parallel.post_processing: 'remove_text-to-concept_measurements, some_other_task'
Omit the field or set it to an empty string to skip post-processing for the site.
Defined in dags/dependencies/ehr/constants.py:
OMOP_PROCESSOR_ENDPOINTOMOP_ANALYZER_ENDPOINTOMOP_TARGET_VOCAB_VERSIONOMOP_TARGET_CDM_VERSIONOMOP_VOCAB_GCS_PATHCONNECT_DATASET_ID
Read in dags/dependencies/ehr/storage_backend.py:
STORAGE_BACKEND—gcs(default) orlocal. The local backend is intended for development against locally mounted files; production should leave this unset.
The main flow in dags/ehr_pipeline.py is:
check_api_healthid_sites_to_processget_unprocessed_filesprocess_deliverytask group, per file in order:convert_file→validate_file→normalize_file→cdm_upgrade→ per-sitepopulate_cdm_source_file→ per-fileglobalize_natural_keysfilteringtask group: per-siteretrieve_connect_data→ per-filefilter_participantsvocab_harmonizationtask grouppost_processingtask groupgenerate_derived_tablestask groupload_to_bigquerytask groupcleanupreportingtask group: per-(site, artifact_type)generate_report_artifact→ per-siteconsolidate_reportanalysistask group: per-sitedqd,achilles, andpass_analysisrun in parallel; once all three finish, per-siteatlas_results_tablesandgenerate_delivery_reportrunmark_delivery_completelog_done
The vocab_harmonization task group runs these steps in order:
harmonize_vocab_source_targetharmonize_vocab_target_remapharmonize_vocab_source_concept_backfillharmonize_vocab_domain_checkharmonize_vocab_omop_etlharmonize_vocab_consolidateharmonize_vocab_discover_tablesflatten_table_configsharmonize_vocab_deduplicate_table
Only tables listed in VOCAB_HARMONIZED_TABLES are processed by these steps.
Two additional file-processor harmonization steps — harmonize_vocab_target_replacement and harmonize_vocab_secondary_concept_backfill — are defined as Airflow tasks but are intentionally excluded from the DAG's >> chain (see the comment near the vocab_harmonization task group). They remain callable on the file processor as endpoints but are not invoked by the current DAG.
The post_processing task group sits between vocabulary harmonization and derived-table generation, so derived tables reflect the final post-processed state of the harmonized clinical tables.
Per-site serial, cross-site parallel. The task group maps one Airflow instance per site that has at least one post-processing task configured. Inside each instance, the configured tasks run sequentially in the order they appear in site_config.yml — the file processor's /post_processing endpoint is invoked once per task, and the next task does not start until the previous one returns. This matters because post-processing tasks share the same parquet artifacts; later tasks may depend on the state produced by earlier ones. Different sites' chains run independently in parallel.
If any task in a site's chain fails, the remaining tasks for that site are not attempted. Empty or whitespace-only entries in the comma-delimited string are filtered out by SiteConfig.post_processing, so stubs like post_processing: '' collapse to no work. When no site has any tasks configured, the mapped task expands to zero instances and the DAG flows straight through.
Each task name in the site config must map to a SQL file at reference/sql/post_processing/<task_name>.sql in the OMOP file processor repo. The processor's /post_processing endpoint returns 400 if the script is missing or if the script attempts to write to a vocabulary file; the DAG task fails fast in either case.
Derived tables are generated after post-processing and before BigQuery loading.
The current derived tables are defined by DERIVED_DATA_TABLES in dags/dependencies/ehr/constants.py.
- Upload
dags/to the Composer DAGs bucket, preserving paths. - Ensure
site_config.ymlis present at the configured path in the DAGs bucket. - Set the required environment variables in Composer.
- Grant Composer access to the configured GCS buckets, BigQuery datasets, and service endpoints.
- Trigger
ehr-pipelinefrom Airflow.
- A site is processed when its latest delivery has no log row or its most recent log row is in an error state.
load_harmonized_tablesskips cleanly when no harmonized clinical tables were produced.load_remainingskips:- vocabulary tables, but only when
overwrite_site_vocab_with_standardistrue(i.e., when the DAG is loading the standard target vocabulary instead of site-delivered vocab) - tables listed in
VOCAB_HARMONIZED_TABLES, since those are loaded earlier fromartifacts/omop_etl/ cdm_source, which is loaded duringcleanup
- vocabulary tables, but only when
log_doneinspects task states and fails the DAG if any upstream task failed, even if a mapped task failure was otherwise easy to miss.
dags/
ehr_pipeline.py
dependencies/
ehr/
analysis.py
bq.py
constants.py
dag_helpers.py
file_config.py
natural_keys.py
omop.py
participant_filter.py
post_processing.py
processing.py
processing_jobs.py
storage_backend.py
utils.py
validation.py
vocab.py
config/
site_config.yml