Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Query GTEx (Genotype-Tissue Expression) portal for tissue-specific gene expression, eQTLs (expression quantitative trait loci), and sQTLs. Essential for linking GWAS variants to gene regulation, understanding tissue-specific expression, and interpreting non-coding variant effects.
.claude/skills/gtex-database/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-13 | ✗→✓ | ▲ Improved | — | — |
| case-16 | ✗→✓ | ▲ Improved | — | — |
| case-20 | ✗→✓ | ▲ Improved | — | — |
| case-02 | ✗→✓ | ▲ Improved | — | — |
| case-03 | ✗→✓ | ▲ Improved | — | — |
The Genotype-Tissue Expression (GTEx) project provides a comprehensive resource for studying tissue-specific gene expression and genetic regulation across 54 non-diseased human tissues from nearly 1,000 individuals. GTEx v10 (the latest release) enables researchers to understand how genetic variants regulate gene expression (eQTLs) and splicing (sQTLs) in a tissue-specific manner, which is critical for interpreting GWAS loci and identifying regulatory mechanisms.
Key resources:
Use GTEx when:
Base URL: https://gtexportal.org/api/v2/
The API returns JSON and does not require authentication. All endpoints support pagination.
pythonimport requests BASE_URL = "https://gtexportal.org/api/v2" def gtex_get(endpoint, params=None): """Make a GET request to the GTEx API.""" url = f"{BASE_URL}/{endpoint}" response = requests.get(url, params=params, headers={"Accept": "application/json"}) response.raise_for_status() return response.json()
pythonimport requests import pandas as pd def get_gene_expression_by_tissue(gene_id_or_symbol, dataset_id="gtex_v10"): """Get median gene expression across all tissues.""" url = "https://gtexportal.org/api/v2/expression/medianGeneExpression" params = { "gencodeId": gene_id_or_symbol, "datasetId": dataset_id, "itemsPerPage": 100 } response = requests.get(url, params=params) data = response.json() records = data.get("data", []) df = pd.DataFrame(records) if not df.empty: df = df[["tissueSiteDetailId", "tissueSiteDetail", "median", "unit"]].sort_values( "median", ascending=False ) return df # Example: get expression of APOE across tissues df = get_gene_expression_by_tissue("ENSG00000130203.10") # APOE GENCODE ID # Or use gene symbol (some endpoints accept both) print(df.head(10)) # Output: tissue name, median TPM, sorted by highest expression
pythonimport requests import pandas as pd def query_eqtl(gene_id, tissue_id=None, dataset_id="gtex_v10"): """Query significant eQTLs for a gene, optionally filtered by tissue.""" url = "https://gtexportal.org/api/v2/association/singleTissueEqtl" params = { "gencodeId": gene_id, "datasetId": dataset_id, "itemsPerPage": 250 } if tissue_id: params["tissueSiteDetailId"] = tissue_id all_results = [] page = 0 while True: params["page"] = page response = requests.get(url, params=params) data = response.json() results = data.get("data", []) if not results: break all_results.extend(results) if len(results) < params["itemsPerPage"]: break page += 1 df = pd.DataFrame(all_results) if not df.empty: df = df.sort_values("pval", ascending=True) return df # Example: Find eQTLs for PCSK9 df = query_eqtl("ENSG00000169174.14") print(df[["snpId", "tissueSiteDetailId", "slope", "pval", "gencodeId"]].head(20))
pythonimport requests def query_variant_eqtl(variant_id, tissue_id=None, dataset_id="gtex_v10"): """Get all eQTL associations for a specific variant.""" url = "https://gtexportal.org/api/v2/association/singleTissueEqtl" params = { "variantId": variant_id, # e.g., "chr1_55516888_G_GA_b38" "datasetId": dataset_id, "itemsPerPage": 250 } if tissue_id: params["tissueSiteDetailId"] = tissue_id response = requests.get(url, params=params) return response.json() # GTEx variant ID format: chr{chrom}_{pos}_{ref}_{alt}_b38 # Example: "chr17_43094692_G_A_b38"
pythonimport requests def get_egenes(tissue_id, dataset_id="gtex_v10"): """Get all eGenes (genes with at least one significant eQTL) in a tissue.""" url = "https://gtexportal.org/api/v2/association/egene" params = { "tissueSiteDetailId": tissue_id, "datasetId": dataset_id, "itemsPerPage": 500 } all_egenes = [] page = 0 while True: params["page"] = page response = requests.get(url, params=params) data = response.json() batch = data.get("data", []) if not batch: break all_egenes.extend(batch) if len(batch) < params["itemsPerPage"]: break page += 1 return all_egenes # Example: all eGenes in whole blood egenes = get_egenes("Whole_Blood") print(f"Found {len(egenes)} eGenes in Whole Blood")
pythonimport requests def get_tissues(dataset_id="gtex_v10"): """Get all available tissues with metadata.""" url = "https://gtexportal.org/api/v2/dataset/tissueSiteDetail" params = {"datasetId": dataset_id, "itemsPerPage": 100} response = requests.get(url, params=params) return response.json()["data"] tissues = get_tissues() # Key fields: tissueSiteDetailId, tissueSiteDetail, colorHex, samplingSite # Common tissue IDs: # Whole_Blood, Brain_Cortex, Liver, Kidney_Cortex, Heart_Left_Ventricle, # Lung, Muscle_Skeletal, Adipose_Subcutaneous, Colon_Transverse, ...
pythonimport requests def query_sqtl(gene_id, tissue_id=None, dataset_id="gtex_v10"): """Query significant sQTLs for a gene.""" url = "https://gtexportal.org/api/v2/association/singleTissueSqtl" params = { "gencodeId": gene_id, "datasetId": dataset_id, "itemsPerPage": 250 } if tissue_id: params["tissueSiteDetailId"] = tissue_id response = requests.get(url, params=params) return response.json()
chr{chrom}_{pos}_{ref}_{alt}_b38)coloc (R package) with full summary statisticspythonimport requests, pandas as pd def interpret_gwas_variant(variant_id, dataset_id="gtex_v10"): """Find all genes regulated by a GWAS variant.""" url = "https://gtexportal.org/api/v2/association/singleTissueEqtl" params = {"variantId": variant_id, "datasetId": dataset_id, "itemsPerPage": 500} response = requests.get(url, params=params) data = response.json() df = pd.DataFrame(data.get("data", [])) if df.empty: return df return df[["geneSymbol", "tissueSiteDetailId", "slope", "pval", "maf"]].sort_values("pval") # Example results = interpret_gwas_variant("chr1_154453788_A_T_b38") print(results.groupby("geneSymbol")["tissueSiteDetailId"].count().sort_values(ascending=False))
| Endpoint | Description | |----------|-------------| | /expression/medianGeneExpression | Median TPM by tissue for a gene | | /expression/geneExpression | Full distribution of expression per tissue | | /association/singleTissueEqtl | Significant eQTL associations | | /association/singleTissueSqtl | Significant sQTL associations | | /association/egene | eGenes in a tissue | | /dataset/tissueSiteDetail | Available tissues with metadata | | /reference/gene | Gene metadata (GENCODE IDs, coordinates) | | /variant/variantPage | Variant lookup by rsID or position |
| ID | Description | |----|-------------| | gtex_v10 | GTEx v10 (current; ~960 donors, 54 tissues) | | gtex_v8 | GTEx v8 (838 donors, 49 tissues) — older but widely cited |
ENSG00000130203.10) for gene queries; the .version suffix matters for some endpointschr{chrom}_{pos}_{ref}_{alt}_b38 (GRCh38) — different from rs IDstissueSiteDetailId (e.g., Whole_Blood) not display names for API callsslope field is the effect of the alternative allele; positive = higher expression with alt alleleFor genome-wide analyses, download full summary statistics rather than using the API:
bash# All significant eQTLs (v10) wget https://storage.googleapis.com/adult-gtex/bulk-qtl/v10/single-tissue-cis-qtl/GTEx_Analysis_v10_eQTL.tar # Normalized expression matrices wget https://storage.googleapis.com/adult-gtex/bulk-gex/v10/rna-seq/GTEx_Analysis_v10_RNASeQCv2.4.2_gene_reads.gct.gz
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +41 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.