HER2 expression across GTEx tissues, live from the GTEx API
Prompt
Plot median expression of HER2 across GTEx tissues, highlight the top 5.
No inputs attached.
At a glance
The identical prompt went to ScientistWorkbench and to a frontier chat model with no tools, no sandbox and no history. Same rows, side by side.
| ScientistWorkbench | Plain frontier model | |
|---|---|---|
| time · tokens | 2 min · 58k | 34 s · 2,892 (output, mostly thinking) |
| tool calls · files · figures | 16 · 7 · 1 | 0 · 0 · 0 |
| What ran | GTEx API queried live (gene lookup + median expression, logged), table checked, chart drawn | Nothing. A script was written for you to run |
| Top tissue | Nerve – Tibial, 130.1 TPM | Guessed: “breast, esophageal mucosa, skin, bladder/vagina, stomach or kidney cortex” |
| Top 5 | Tibial nerve · sun-exposed skin · esophagus mucosa · non-sun-exposed skin · thyroid | Not given (“run the code rather than trusting that ordering”) |
| Where breast ranks | 23rd, 37.1 TPM | Expected near the top |
| Data provenance | Two request URLs with status and byte counts in _queries.jsonl; raw JSON saved | None |
| Figure | Bar chart, 54 tissues, top 5 highlighted (PNG + PDF) | None |
| independent review | 7 findings from a second pass over the transcript | none |
What the plain model did. It answered in 34 seconds with a well-written Python script and a paragraph of advice, then said, in its own words, to "run the code rather than trusting that ordering". It guessed the top tissues would be "breast, esophageal mucosa, skin, bladder/vagina, stomach or kidney cortex". No data was fetched, no numbers, no figure.
What the workbench did. It resolved HER2 to ERBB2 (ENSG00000141736.13), called the GTEx v8 API, checked the response (54 tissues, one page, all in TPM, no duplicates), ranked them, drew the chart and wrote the report — 16 tool calls in about 2 minutes, with every request URL and status logged in _queries.jsonl.
The difference that matters. The real top tissue is tibial nerve (130 TPM), followed by sun-exposed skin, esophagus mucosa, non-sun-exposed skin and thyroid; breast is 23rd at 37 TPM. The plain model's expectation would have put breast near the top. A script is not a result: until it runs, the ranking is a guess, and here the guess was wrong about the headline.
The two outputs, section by section
Switch tabs; the sections line up (figures → report → review → execution log). Everything is verbatim.

HER2 (ERBB2) median expression across GTEx tissues
Objective
Plot median expression of HER2 (gene symbol ERBB2) across GTEx tissues and highlight the five tissues with the highest median expression.
Method
- Resolved the stable gene identifier for HER2/ERBB2 using the GTEx Portal API v2 reference endpoint:
GET https://gtexportal.org/api/v2/reference/gene?geneId=ERBB2→ versioned GENCODE idENSG00000141736.13(GENCODE v26, GRCh38/hg38, Entrez 2064, protein coding, chr17). - Retrieved the per-tissue median gene expression (GTEx release v8) with:
GET https://gtexportal.org/api/v2/expression/medianGeneExpression?gencodeId=ENSG00000141736.13&datasetId=gtex_v8. The response contains one record per tissue withmedianexpression in TPM (transcripts per million); response was a single page covering all 54 GTEx v8 tissues (paging_info.totalNumberOfItems = 54). - Saved the raw JSON response verbatim (
raw_gtex_ERBB2_medianGeneExpression.json) and parsed it into a tidy table sorted by descending median TPM (her2_gtex_median_expression.csv). - Ranked tissues by median TPM and extracted the top 5 (
her2_gtex_top5_tissues.csv). - Plotted a horizontal bar chart (all 54 tissues, ranked) with the top 5 tissues highlighted in an Okabe-Ito colour-blind-safe orange, all others in grey, per the lab's
science-figuresconventions (labelled axis with units, grid, legend, descriptive title, annotated top-5 values). - Every API call was logged with timestamp, URL, HTTP status and response size in
_queries.jsonl.
No local input files were used (none were attached to this job); all data were fetched from the public GTEx Portal API v2 as described above.
Results
- Gene resolved: HER2 / ERBB2, GENCODE id
ENSG00000141736.13(GTEx reference/gene lookup). - Dataset: GTEx v8, median expression across n = 54 tissues, unit TPM.
- Expression range across all tissues: 1.25 TPM (Whole Blood) to 130.13 TPM (Nerve — Tibial) (from
her2_gtex_median_expression.csv). - Top 5 tissues by median ERBB2 (HER2) expression (from
her2_gtex_top5_tissues.csv):
| Rank | Tissue | Median expression (TPM) |
|---|---|---|
| 1 | Nerve – Tibial | 130.13 |
| 2 | Skin – Sun-exposed (Lower leg) | 118.26 |
| 3 | Esophagus – Mucosa | 113.10 |
| 4 | Skin – Not sun-exposed (Suprapubic) | 110.94 |
| 5 | Thyroid | 99.31 |
- No fitted statistical model was applied (this is a descriptive plot of pre-computed GTEx median values); the only "statistic shown" is the per-tissue median TPM as reported by the GTEx v8 release, ranked in descending order.
- QC: the API returned exactly one page (
numberOfPages = 1) with 54 records, matchingtotalNumberOfItems = 54; no missing or duplicate tissue entries were found in the parsed table; all records carried unitTPM(verified programmatically).
Assumptions & caveats
- No input files were provided for this job; data were fetched live from the public GTEx Portal API v2 (
gtexportal.org/api/v2), which is standard practice for this kind of query and is recorded here for reproducibility. - Used GTEx release v8 (
datasetId=gtex_v8), the latest stable release exposed by this endpoint at query time; no newer release was requested explicitly by the task, so the default/most complete release was chosen. - "Median expression" is the tissue-level median TPM computed and provided directly by GTEx (not recomputed from raw per-sample read counts, since GTEx does not expose raw sample-level counts through this lightweight API endpoint); values are used as returned.
- "Top 5" is defined as the five tissues with the highest median TPM value, a standard and unambiguous ranking criterion.
- GTEx tissue granularity separates closely related sub-sites (e.g., multiple brain sub-regions, two skin sites) as distinct tissues; this is the standard GTEx tissue nomenclature (
tissueSiteDetailId) and was kept as-is rather than aggregated, to match how GTEx itself reports and plots this data.
Files
summary.md— this report.fig1_HER2_median_expression_GTEx.png— horizontal bar chart of median ERBB2 (HER2) expression (TPM) across all 54 GTEx v8 tissues, ranked descending, top 5 tissues highlighted in orange with annotated values.fig1_HER2_median_expression_GTEx.pdf— vector twin of the above figure.her2_gtex_median_expression.csv— tidy table of all 54 tissues with median ERBB2 expression (TPM), tissue ontology id, and unit, sorted descending.her2_gtex_top5_tissues.csv— the 5 tissues with highest median ERBB2 expression, with rank and TPM value.raw_gtex_ERBB2_medianGeneExpression.json— raw JSON response snapshot from the GTEx Portal API v2medianGeneExpressionendpoint forENSG00000141736.13/gtex_v8.scripts/plot_her2_gtex.py— script that reads the CSV and generates the figure and top-5 table._queries.jsonl— log of every API request made (timestamp, URL, HTTP status, response size).
A second pass re-reads the transcript and artifacts and checks each claim of the report against what actually ran.
| verdict | claim | evidence |
|---|---|---|
| pass | Gene resolved: HER2 / ERBB2, GENCODE id ENSG00000141736.13 | cell 7 output: GET /reference/gene?geneId=ERBB2 returns gencodeId 'ENSG00000141736.13', geneSymbol 'ERBB2', matching report exactly. |
| pass | Top 5 tissues by median ERBB2 expression: Nerve-Tibial 130.13, Skin Sun-exposed Lower leg 118.26, Esophagus Mucosa 113.10, Skin Not Sun-exposed Suprapubic 110.94, Thyroid 99.31 | cell 12 stdout table exactly matches these 5 rank/tissue/median values (130.1310, 118.2600, 113.1010, 110.9440, 99.3077). |
| pass | n = 54 tissues, dataset GTEx v8, unit TPM | cell 9 stdout 'print(len(r['data']))' -> 54; cell 12 stdout 'n_tissues: 54'; JSON records show unit TPM in cell 8/9 output. |
| warn | Expression range across all tissues: 1.25 TPM (Whole Blood) to 130.13 TPM (Nerve — Tibial) | cell 12 stdout only shows 'median range: 1.24878 - 130.131' without naming which tissue corresponds to the minimum value; no execution record explicitly confirms 'Whole Blood' as the minimum-expression tissue. |
| warn | no missing or duplicate tissue entries were found in the parsed table; all records carried unit TPM (verified programmatically) | cell 10 code computes df['unit'].unique() and prints it, but the captured stdout for cell 10 is truncated before this line appears; no visible output in the record confirms this QC check actually ran/printed a result. |
| warn | Every API call was logged with timestamp, URL, HTTP status and response size in _queries.jsonl | cell 14 shows 3 log lines: 1 reference/gene call plus 2 identical medianGeneExpression calls (cells 8 and 9 both queried the same endpoint), meaning an extra duplicate query was made beyond what the Method section describes as a single retrieval step. |
| pass | Files produced: her2_gtex_median_expression.csv, her2_gtex_top5_tissues.csv, raw_gtex_ERBB2_medianGeneExpression.json, fig1_HER2_median_expression_GTEx.png/.pdf, _queries.jsonl, scripts/plot_her2_gtex.py | cell 14 'ls -la /mnt/session/outputs/' lists all these files present with nonzero size, matching the Files section. |
The full log — every code cell with source, stdout, stderr and exit status — ships with every job as _execution_log.json.
#1 · read · ok
{"file_path": "/workspace/skills/life-science-apis/SKILL.md"}1 ---
2 name: life-science-apis
3 description: How to query public life-science databases from the sandbox with plain HTTP (UniProt, Ensembl, InterPro, PDB, AlphaFold, gnomAD, ClinVar, GTEx, cBioPortal, PubChem, BindingDB, openFDA, OpenAlex, Open Targets, DepMap). Use when a task needs an identifier lookup, sequence, structure, variant, expression, compound, trial or literature record. Every query must be logged.
4 ---
5
6 # Life-science APIs from the sandbox
7
8 Use `helpers/lsapi.py` (`from lsapi import get_json, get_text`) or plain `requests`. Rules:
9
10 1. **Log every query.** `lsapi` appends `{ts, url, status, bytes}` to `/mnt/session/outputs/_queries.jsonl`; if you use
11 `reque
… [more]#2 · read · error
{"file_path": "/workspace/skills/life-science-apis/apis/gtex.md"}awk: cannot open "/workspace/skills/life-science-apis/apis/gtex.md" (No such file or directory)
#3 · bash · error
ls /workspace/skills/life-science-apis/apis/ ; ls /workspace/skills/life-science-apis/helpers/
ls: cannot access '/workspace/skills/life-science-apis/apis/': No such file or directory ls: cannot access '/workspace/skills/life-science-apis/helpers/': No such file or directory
#4 · bash · ok
find /workspace/skills -type f | head -100
/workspace/skills/science-figures/SKILL.md /workspace/skills/life-science-apis/lsapi.py /workspace/skills/life-science-apis/gtex.md /workspace/skills/life-science-apis/SKILL.md /workspace/skills/life-science-apis/interpro.md /workspace/skills/life-science-apis/bindingdb.md /workspace/skills/life-science-apis/openalex.md /workspace/skills/life-science-apis/openfda.md /workspace/skills/life-science-apis/gnomad.md /workspace/skills/life-science-apis/depmap.md /workspace/skills/life-science-apis/opentargets.md /workspace/skills/life-science-apis/pubchem.md /workspace/skills/life-science-apis/cbioportal.md /workspace/skills/life-science-apis/uniprot.md /workspace/skills/life-science-apis/alphafol … [more]
#5 · read · ok
{"file_path": "/workspace/skills/life-science-apis/gtex.md"}1 # GTEx Portal API v2 — https://gtexportal.org/api/v2 2 - Gene expression by tissue: `GET /expression/medianGeneExpression?gencodeId=ENSG00000141510.18&datasetId=gtex_v8`. 3 - Gene lookup: `GET /reference/gene?geneId=TP53`. eQTLs: `GET /association/singleTissueEqtl?gencodeId=…&tissueSiteDetailId=Liver`. 4 - Gotchas: needs versioned GENCODE ids; tissue ids are like `Brain_Cortex`.
#6 · read · ok
{"file_path": "/workspace/skills/life-science-apis/lsapi.py"}1 """Tiny polite HTTP helper with provenance logging. from lsapi import get_json, get_text"""
2 import json, os, time
3 from pathlib import Path
4 import requests
5
6 LOG = Path(os.environ.get("WB_QUERY_LOG", "/mnt/session/outputs/_queries.jsonl"))
7 _last: dict[str, float] = {}
8 UA = {"User-Agent": "ScientistWorkbench/0.1 (research sandbox; contact via job report)"}
9
10
11 def _pace(host: str, min_gap: float = 1.0) -> None:
12 t = _last.get(host, 0.0)
13 wait = min_gap - (time.time() - t)
14 if wait > 0:
15 time.sleep(wait)
16 _last[host] = time.time()
17
18
19 def _log(url: str, status: int, nbytes: int) -> None:
20 try:
21 LOG.parent.mkdir(parent
… [more]#7 · read · ok
{"file_path": "/workspace/skills/science-figures/SKILL.md"}1 --- 2 name: science-figures 3 description: Conventions for publication-quality scientific figures produced in the sandbox (matplotlib/seaborn). Use whenever a task produces a plot, tree, heatmap, structure view or multi-panel figure; covers layout, annotation, colour, labelling, file output and the figure record in summary.md. 4 --- 5 6 # Science figures 7 8 **One message per figure.** Decide what the reader must see in three seconds and design for that; move everything else 9 to a table. Multi-panel figures get panel letters (A, B, …) and a one-sentence title per panel. 10 11 **Layout** 12 - `fig, ax = plt.subplots(figsize=(w, h), constrained_layout=True)`; 7.2 in wide for single/doubl … [more]
#8 · bash · ok
mkdir -p /mnt/session/outputs/scripts
cd /mnt/session/outputs
python3 -c "
import sys
sys.path.insert(0, '/workspace/skills/life-science-apis')
from lsapi import get_json
import json
r = get_json('https://gtexportal.org/api/v2/reference/gene', params={'geneId':'ERBB2'})
print(json.dumps(r, indent=2)[:3000])
"
{
"data": [
{
"chromosome": "chr17",
"dataSource": "HAVANA",
"description": "erb-b2 receptor tyrosine kinase 2 [Source:HGNC Symbol;Acc:HGNC:3430]",
"end": 39730426,
"entrezGeneId": 2064,
"gencodeId": "ENSG00000141736.13",
"gencodeVersion": "v26",
"geneStatus": "",
"geneSymbol": "ERBB2",
"geneSymbolUpper": "ERBB2",
"geneType": "protein coding",
"genomeBuild": "GRCh38/hg38",
"start": 39687914,
"strand": "+",
"tss": 39687914
}
],
"paging_info": {
"numberOfPages": 1,
"page": 0,
"maxItemsPerPage": 250,
"totalNumberOfItems": 1
}
}
Approach
GTEx publishes median TPM per tissue, which you can pull straight from the GTEx Portal API (no bulk download needed). HER2's official symbol is ERBB2 (Ensembl ENSG00000141736), so the script resolves the symbol → GENCODE ID first, then fetches median expression.
"""
Median expression of HER2 (ERBB2) across GTEx tissues, top 5 highlighted.
Requires: requests, pandas, matplotlib
"""
import requests
import pandas as pd
import matplotlib.pyplot as plt
GENE = "ERBB2" # HER2
DATASET = "gtex_v8" # use "gtex_v10" for the latest release
API = "https://gtexportal.org/api/v2"
TOP_N = 5
def get_gencode_id(symbol, dataset=DATASET):
r = requests.get(f"{API}/reference/gene",
params={"geneId": symbol, "gencodeVersion": "v26",
"genomeBuild": "GRCh38/hg38"},
timeout=30)
r.raise_for_status()
hits = r.json()["data"]
# keep the exact symbol match (avoids pseudogenes / read-throughs)
exact = [h for h in hits if h["geneSymbol"].upper() == symbol.upper()]
return (exact or hits)[0]["gencodeId"]
def get_median_expression(gencode_id, dataset=DATASET):
r = requests.get(f"{API}/expression/medianGeneExpression",
params={"gencodeId": gencode_id, "datasetId": dataset,
"itemsPerPage": 1000},
timeout=60)
r.raise_for_status()
df = pd.DataFrame(r.json()["data"])
df = df.rename(columns={"median": "median_tpm",
"tissueSiteDetailId": "tissue_id"})
df["tissue"] = df["tissue_id"].str.replace("_", " ", regex=False)
return df[["tissue", "median_tpm"]].sort_values("median_tpm",
ascending=False)
# ---- fetch -----------------------------------------------------------------
gid = get_gencode_id(GENE)
df = get_median_expression(gid).reset_index(drop=True)
print(df.head(10).to_string(index=False))
# ---- plot ------------------------------------------------------------------
top = df.head(TOP_N)["tissue"].tolist()
colors = ["#d62728" if t in top else "#c9ccd1" for t in df["tissue"]]
fig, ax = plt.subplots(figsize=(8, 12))
ax.barh(df["tissue"], df["median_tpm"], color=colors, edgecolor="none")
ax.invert_yaxis() # highest at top
ax.set_xlabel("Median expression (TPM)")
ax.set_title(f"HER2 / {GENE} ({gid}) median expression across GTEx tissues\n"
f"{DATASET.upper().replace('_', ' ')} — top {TOP_N} highlighted",
fontsize=12)
ax.spines[["top", "right"]].set_visible(False)
ax.grid(axis="x", alpha=0.3)
# label the highlighted bars
for t, v in zip(top, df.head(TOP_N)["median_tpm"]):
ax.text(v + df["median_tpm"].max() * 0.01, list(df["tissue"]).index(t),
f"{v:,.1f}", va="center", fontsize=9, color="#d62728",
fontweight="bold")
for lbl in ax.get_yticklabels():
if lbl.get_text() in top:
lbl.set_color("#d62728")
lbl.set_fontweight("bold")
plt.tight_layout()
plt.savefig("HER2_GTEx_median_expression.png", dpi=300)
plt.show()
Notes / options
- Log scale: ERBB2 spans a wide dynamic range (whole blood ≈ 0 TPM vs. epithelial tissues in the hundreds). Add
ax.set_xscale("log")and plotmedian_tpm + 0.1if you want the low tissues visible. - Offline alternative: if the API is blocked, download
GTEx_Analysis_2017-06-05_v8_RNASeQCv1.1.9_gene_median_tpm.gct.gzfrom the GTEx downloads page and replace the fetch step with:python df = pd.read_csv("...gene_median_tpm.gct.gz", sep="\t", skiprows=2) row = df[df["Description"] == "ERBB2"].iloc[0, 2:] df = row.rename("median_tpm").rename_axis("tissue").reset_index() \ .sort_values("median_tpm", ascending=False) - Caveat on isoforms: these are gene-level TPMs; they don't distinguish HER2 splice variants (e.g., p95HER2/Δ16) or reflect protein/IHC status, which is what matters clinically.
- v8 vs v10: switch
DATASET(andgencodeVersiontov39/GRCh38for v10) if you want the newer release — tissue list and medians differ slightly.
Biologically, expect the top tissues to be epithelium-rich ones (breast, esophageal mucosa, skin, bladder/vagina, stomach or kidney cortex), with whole blood at the bottom — but run the code rather than trusting that ordering, since exact ranks shift between GTEx releases.
AI-generated analysis. Outputs may contain errors; verify numbers against the execution log and source files before relying on them. Not a substitute for review by a qualified scientist, and not medical advice: any use affecting patient care or healthcare decisions requires review by a qualified professional.