Query Metabolomics Workbench REST API (4,200+ NIH studies) for metabolite ID, study discovery, RefMet standardization, m/z precursor searches, and gene/protein annotations. Quirks: compound input_item rejects `name` (use pubchem_cid/kegg_id/inchi_key/etc.); free-text → compound is a two-step refmet/match→refmet/name flow; moverz endpoint returns TSV text, not JSON. Use hmdb-database for local XML; pubchem-compound-search for general compound lookup.
Scanned 5/29/2026
Install to Claude Code
npx -y skills add jaechang-hits/SciAgent-Skills --skill metabolomics-workbench-database --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Metabolomics Workbench Database?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/jaechang-hits-metabolomics-workbench-database)More formats (shields.io, HTML) on the badges page.
---
name: "metabolomics-workbench-database"
description: "Query Metabolomics Workbench REST API (4,200+ NIH studies) for metabolite ID, study discovery, RefMet standardization, m/z precursor searches, and gene/protein annotations. Quirks: compound input_item rejects `name` (use pubchem_cid/kegg_id/inchi_key/etc.); free-text → compound is a two-step refmet/match→refmet/name flow; moverz endpoint returns TSV text, not JSON. Use hmdb-database for local XML; pubchem-compound-search for general compound lookup."
license: "CC-BY-4.0"
---
# Metabolomics Workbench Database — REST API Access
## Overview
The Metabolomics Workbench (MW) REST API at `https://www.metabolomicsworkbench.org/rest/` exposes 4,200+ metabolomics studies hosted at UCSD under NIH Common Fund sponsorship. URL pattern is `/{context}/{input_item}/{input_value}/{output_item}/{format}`. Contexts include `compound`, `refmet`, `moverz`, `study`, `analysis`, `metabolite`, `gene`, `protein`. Notable quirks discovered live:
- `compound/name/{x}` is **rejected** — `name` is not an allowed input_item. Use `pubchem_cid`, `kegg_id`, `inchi_key`, `hmdb_id`, `regno`, `lm_id`, `formula`, `smiles`, or `abbrev`. For free-text input, go through `refmet/match/{x}` first.
- `refmet/name/{x}/all` requires the **exact** RefMet name (e.g. `Glucose`, not `D-glucose`); use `refmet/match/{x}` for fuzzy normalisation first.
- `moverz/{REFMET|LIPIDS|MB}/{mz}/{ion}/{tol}/txt` returns **TSV text** (no JSON variant).
- The `metstat/filter/...` endpoint shown in older examples returns `[]` — replace with `study/{context}/{value}/summary` (or `/metabolites`) + client-side filtering.
No authentication required.
## When to Use
- Searching metabolite records by PubChem CID, KEGG ID, InChIKey, HMDB ID, formula, or SMILES
- Discovering studies by species, disease, last_name, institute, analysis_type, or polarity
- Standardising metabolite names to RefMet nomenclature for cross-study integration
- Identifying unknown compounds from MS m/z values with adduct-aware matching (`moverz`)
- Retrieving experimental metabolite tables (analyses, abundances) from published studies
- Querying gene/protein annotations linked to metabolomics pathways
- Downloading raw mwTab files for local analysis
- For local 220K-metabolite XML parsing with NMR/MS spectra use `hmdb-database` instead
- For live 110M-compound property lookups use `pubchem-compound-search` instead
## Prerequisites
- **Python packages**: `requests`, `pandas`
- **No API key required**: publicly accessible
- **Rate limits**: MW does not enforce strict limits; add `time.sleep(0.3)` between bulk requests
- **Base URL**: `https://www.metabolomicsworkbench.org/rest`
```bash
pip install requests pandas
```
## Quick Start
```python
import requests
BASE = "https://www.metabolomicsworkbench.org/rest"
# Two-step free-text → compound (the API rejects compound/name/...)
def lookup_by_name(name):
# 1) Normalise to RefMet name
r = requests.get(f"{BASE}/refmet/match/{name}", timeout=30)
r.raise_for_status()
refmet = r.json()
if not refmet.get("refmet_name"):
return None
# 2) Pull full compound record by RefMet name (or by pubchem_cid)
r2 = requests.get(f"{BASE}/refmet/name/{refmet['refmet_name']}/all", timeout=30)
rec = r2.json() if r2.json() else {}
return rec if isinstance(rec, dict) else None
c = lookup_by_name("glucose")
print(f"{c['name']}: formula={c['formula']}, PubChem CID={c['pubchem_cid']}, "
f"InChIKey={c['inchi_key']}")
# Glucose: formula=C6H12O6, PubChem CID=5793, InChIKey=WQZGKKKJIJFFOK-GASJEMHNSA-N
```
## Core API
### Module 1: Compound Queries
`compound/{input_item}/{input_value}/all/json` — `input_item` must be one of `regno`, `formula`, `inchi_key`, `lm_id`, `pubchem_cid`, `hmdb_id`, `kegg_id`, `smiles`, `abbrev`. The legacy `name` input is rejected by the server.
```python
import requests
BASE = "https://www.metabolomicsworkbench.org/rest"
# By PubChem CID
r = requests.get(f"{BASE}/compound/pubchem_cid/5793/all/json", timeout=30)
glucose = r.json()
print(f"PubChem 5793 -> {glucose['name']}, formula={glucose['formula']}, "
f"HMDB={glucose.get('hmdb_id')}, KEGG={glucose.get('kegg_id')}")
# By KEGG ID
r = requests.get(f"{BASE}/compound/kegg_id/C00031/all/json", timeout=30)
print("KEGG C00031 ->", r.json()["name"])
# By InChIKey
r = requests.get(f"{BASE}/compound/inchi_key/WQZGKKKJIJFFOK-GASJEMHNSA-N/all/json", timeout=30)
print("InChIKey -> regno:", r.json()["regno"])
```
```python
# Compound by formula returns a paged dict (multiple matches)
import requests
BASE = "https://www.metabolomicsworkbench.org/rest"
r = requests.get(f"{BASE}/compound/formula/C6H12O6/all/json", timeout=30)
matches = r.json()
print(f"Compounds with formula C6H12O6: {len(matches)}")
for k in list(matches)[:3]:
print(f" regno={matches[k]['regno']} name={matches[k]['name']}")
```
### Module 2: Study Discovery
`study/{input_item}/{input_value}/{output}` — `input_item` includes `study_id`, `study_title`, `last_name`, `institute`, `analysis_id`, `metabolite_id`, `kegg_id`, `refmet_name`. `output` includes `summary`, `metabolites`, `factors`, `data`, `available_studies`, `species`, `disease`. `summary` for `study_id` returns a dict (keyed by accession when multiple); for `last_name`/`institute` it returns a list.
```python
import requests, pandas as pd
BASE = "https://www.metabolomicsworkbench.org/rest"
# Single-study summary — `study/study_id/{id}/summary` returns a flat dict
# (keys: study_id, study_title, species, institute, analysis_type, ...)
r = requests.get(f"{BASE}/study/study_id/ST000001/summary", timeout=30)
s = r.json()
print(f"{s['study_id']}: {s['study_title'][:60]}")
print(f" Species : {s.get('species')} Institute: {s.get('institute')}")
print(f" Submit : {s.get('submission_date')}")
```
```python
# Studies that detected a metabolite — `study/refmet_name/{x}/summary` returns
# a thin index of (refmet_name, kegg_id, study_id) rows. Chain study_id → full summary
# to get title and species.
import requests, pandas as pd
BASE = "https://www.metabolomicsworkbench.org/rest"
r = requests.get(f"{BASE}/study/refmet_name/Glucose/summary", timeout=60)
d = r.json()
rows = list(d.values()) if isinstance(d, dict) else d
print(f"Studies referencing 'Glucose': {len(rows)}")
print(pd.DataFrame(rows).head(5).to_string(index=False))
# refmet_name kegg_id study_id
# Glucose C00031 ST000001
# Glucose C00031 ST000002
# ...
```
### Module 3: RefMet Standardisation
`refmet/match/{user_text}` is a fuzzy normaliser — returns the standard RefMet record (no `pubchem_cid`/`kegg_id` though). `refmet/name/{exact_refmet_name}/all` returns the full record including IDs. Use them as a two-step pipeline.
```python
import requests
BASE = "https://www.metabolomicsworkbench.org/rest"
def normalise_to_refmet(user_text):
r = requests.get(f"{BASE}/refmet/match/{user_text}", timeout=30)
r.raise_for_status()
m = r.json()
if not m or not m.get("refmet_name"):
return None
return m["refmet_name"]
def refmet_full(refmet_name):
r = requests.get(f"{BASE}/refmet/name/{refmet_name}/all", timeout=30)
r.raise_for_status()
rec = r.json()
return rec if isinstance(rec, dict) and rec else None
name = normalise_to_refmet("alpha-D-glucose") # -> 'Glucose'
print(f"Normalised: {name}")
rec = refmet_full(name)
print(f" PubChem CID : {rec['pubchem_cid']}")
print(f" InChIKey : {rec['inchi_key']}")
print(f" Super class : {rec['super_class']} / {rec['main_class']} / {rec['sub_class']}")
```
### Module 4: Study Filtering (replaces broken `metstat`)
The older `metstat/filter/...` endpoint returns `[]`. Use the study context endpoints with client-side filtering instead.
```python
import requests, pandas as pd
BASE = "https://www.metabolomicsworkbench.org/rest"
def study_ids_for_metabolite(refmet_name):
"""Return the study_id list that report a given RefMet name."""
r = requests.get(f"{BASE}/study/refmet_name/{refmet_name}/summary", timeout=60)
r.raise_for_status()
d = r.json()
rows = list(d.values()) if isinstance(d, dict) else d
return sorted({row["study_id"] for row in rows if row.get("study_id")})
def study_summary(study_id):
"""Pull full summary (title, species, institute, dates) for one study_id.
Response is a flat dict with keys study_id/study_title/species/institute/..."""
return requests.get(f"{BASE}/study/study_id/{study_id}/summary", timeout=30).json()
# Find glucose studies, then enrich the first few
ids = study_ids_for_metabolite("Glucose")
print(f"Studies referencing 'Glucose': {len(ids)}")
rows = [study_summary(sid) for sid in ids[:5]]
df = pd.DataFrame(rows)
print(df[["study_id", "study_title", "species"]].head().to_string(index=False))
```
### Module 5: m/z Precursor Search (`moverz`)
`moverz/{REFMET|LIPIDS|MB}/{mz}/{ion}/{tolerance}/txt` returns **tab-separated text** (not JSON). The first DB selector (`REFMET`, `LIPIDS`, or `MB`) is required — `mz` as the first segment is rejected.
```python
import requests, io, pandas as pd
BASE = "https://www.metabolomicsworkbench.org/rest"
def moverz_search(db, mz, ion, tolerance=0.005):
"""Search precursor m/z in REFMET / LIPIDS / MB and return a DataFrame.
Response is TSV text — no JSON variant."""
assert db in {"REFMET", "LIPIDS", "MB"}
r = requests.get(f"{BASE}/moverz/{db}/{mz}/{ion}/{tolerance}/txt", timeout=30)
r.raise_for_status()
return pd.read_csv(io.StringIO(r.text), sep="\t")
df = moverz_search("REFMET", 180.063, "M+H", 0.005)
print(f"Candidates at m/z 180.063 [M+H]+ (REFMET): {len(df)}")
print(df.head(5).to_string(index=False))
```
```python
# Same query against the LIPIDS database
import requests, io, pandas as pd
BASE = "https://www.metabolomicsworkbench.org/rest"
r = requests.get(f"{BASE}/moverz/LIPIDS/760.585/M+H/0.01/txt", timeout=30)
df_lipids = pd.read_csv(io.StringIO(r.text), sep="\t")
print(f"Lipid candidates at m/z 760.585: {len(df_lipids)}")
print(df_lipids.head(3).to_string(index=False))
```
### Module 6: Genes and Proteins
```python
import requests
BASE = "https://www.metabolomicsworkbench.org/rest"
r = requests.get(f"{BASE}/gene/gene_symbol/HMGCR/all", timeout=30)
g = r.json()
print(f"{g['gene_symbol']} -> MGP: {g.get('mgp_id')} ({g.get('gene_name', '')[:60]})")
# Protein by UniProt accession
r2 = requests.get(f"{BASE}/protein/uniprot_id/P04035/all", timeout=30)
p = r2.json()
print(f"UniProt P04035: {p.get('protein_name', '')[:60]} organism={p.get('organism')}")
```
## Key Concepts
### Allowed `input_item` Values per Context
| Context | Valid input_item | Notes |
|---------|------------------|-------|
| `compound` | `regno`, `formula`, `inchi_key`, `lm_id`, `pubchem_cid`, `hmdb_id`, `kegg_id`, `smiles`, `abbrev` | `name` is **rejected** — go via `refmet/match` first |
| `refmet` | `match`, `name`, `formula`, `exactmass`, `inchi_key`, `pubchem_cid`, `regno` | `match` is fuzzy; `name` requires the canonical RefMet name |
| `study` | `study_id`, `study_title`, `last_name`, `institute`, `analysis_id`, `metabolite_id`, `kegg_id`, `refmet_name` | `summary` for `study_id` is a dict keyed by accession; for `last_name`/`institute` it's a list |
| `moverz` | (path) `REFMET` / `LIPIDS` / `MB` | First segment is the DB, not `mz` |
| `gene` | `gene_id`, `gene_symbol`, `gene_name`, `mgp_id` | Returns a dict |
| `protein` | `mgp_id`, `gene_id`, `uniprot_id`, `gene_symbol` | Returns a dict |
### Output Type Conventions
- `output=summary` returns a **dict** when the input identifier is unique (e.g. `study_id`), a **list** when it isn't (e.g. `last_name`).
- Appending `/json` to `study/.../summary` flips the response to TSV. Omit the format suffix — JSON is the default.
- `moverz` only emits `/txt` (TSV); there is no JSON variant.
### `refmet/match` vs `refmet/name`
- `refmet/match/{user_text}` — fuzzy. Always returns a single dict with `refmet_name`, `formula`, `exactmass`, classification. Does **not** include `pubchem_cid`/`inchi_key`.
- `refmet/name/{exact_refmet_name}/all` — requires the canonical RefMet name. Returns the full record including IDs. Returns an empty list if the name isn't canonical.
## Common Workflows
### Workflow 1: Free-Text → Full Compound Record
```python
import requests, pandas as pd
BASE = "https://www.metabolomicsworkbench.org/rest"
def resolve_to_compound(user_text):
# 1) Normalise via refmet/match
rm = requests.get(f"{BASE}/refmet/match/{user_text}", timeout=30).json()
if not rm.get("refmet_name"):
return None
name = rm["refmet_name"]
# 2) Fetch full RefMet record (includes pubchem_cid / inchi_key)
full = requests.get(f"{BASE}/refmet/name/{name}/all", timeout=30).json()
if not isinstance(full, dict) or not full:
return None
# 3) Optionally pull the matching compound record via PubChem CID
cid = full.get("pubchem_cid")
compound = None
if cid:
compound = requests.get(f"{BASE}/compound/pubchem_cid/{cid}/all/json",
timeout=30).json()
return {
"input": user_text,
"refmet_name": name,
"formula": full.get("formula"),
"pubchem_cid": cid,
"hmdb_id": compound.get("hmdb_id") if compound else None,
"kegg_id": compound.get("kegg_id") if compound else None,
"inchi_key": full.get("inchi_key"),
}
queries = ["glucose", "alpha-D-glucose", "L-tyrosine", "cholesterol"]
df = pd.DataFrame([resolve_to_compound(q) for q in queries])
print(df.to_string(index=False))
df.to_csv("name_resolution.csv", index=False)
```
### Workflow 2: Annotate MS Hit List
**Goal**: Take a list of measured m/z values and assign RefMet candidate compounds.
```python
import requests, io, pandas as pd, time
BASE = "https://www.metabolomicsworkbench.org/rest"
def annotate_peaks(mz_values, ion="M+H", tolerance=0.005):
out = []
for mz in mz_values:
r = requests.get(f"{BASE}/moverz/REFMET/{mz}/{ion}/{tolerance}/txt", timeout=30)
if r.status_code != 200 or not r.text.strip():
time.sleep(0.3); continue
df = pd.read_csv(io.StringIO(r.text), sep="\t")
for _, row in df.iterrows():
out.append({
"query_mz": mz,
"matched_mz": row["Matched m/z"],
"delta": row["Delta"],
"name": row["Name"],
"formula": row["Formula"],
"ion": row["Ion"],
"main_class": row.get("Main class"),
})
time.sleep(0.3)
return pd.DataFrame(out)
peaks = [180.063, 166.086, 90.055] # glucose, phenylalanine, alanine
df_ann = annotate_peaks(peaks)
print(df_ann.head(10).to_string(index=False))
df_ann.to_csv("ms_annotations.csv", index=False)
```
### Workflow 3: Find Studies Detecting a Metabolite
```python
import requests, pandas as pd
BASE = "https://www.metabolomicsworkbench.org/rest"
def studies_with(refmet_name, enrich_n=20):
"""Return a DataFrame: study_id rows that report the metabolite, enriched with
title + species for the first `enrich_n` IDs (via study/study_id/.../summary)."""
r = requests.get(f"{BASE}/study/refmet_name/{refmet_name}/summary", timeout=60)
r.raise_for_status()
d = r.json()
rows = list(d.values()) if isinstance(d, dict) else d
ids = sorted({row["study_id"] for row in rows if row.get("study_id")})
enriched = []
for sid in ids[:enrich_n]:
enriched.append(requests.get(
f"{BASE}/study/study_id/{sid}/summary", timeout=30).json())
return pd.DataFrame(enriched)
df = studies_with("Glucose", enrich_n=20)
print(f"Glucose-detecting studies (first 20 enriched): {len(df)}")
print(df.groupby("species").size().sort_values(ascending=False).head(8))
```
## Key Parameters
| Parameter | Endpoint | Default | Range / Options | Effect |
|-----------|----------|---------|-----------------|--------|
| `context` | path | required | `compound`, `refmet`, `moverz`, `study`, `analysis`, `metabolite`, `gene`, `protein` | API context selector |
| `input_item` | path | required | depends on context (see "Allowed `input_item` Values per Context") | Identifier type |
| `input_value` | path | required | string | The actual identifier or value |
| `output_item` | path | `all` | `all`, `summary`, `metabolites`, `factors`, `data`, etc. | What aspect to return |
| `format` | path | (varies) | `json`, `txt` | `moverz` only emits `txt`; do NOT append `/json` to `study/.../summary` |
| `mz` / `ion` / `tolerance` | `moverz` path | required | float / `M+H`, `M-H`, `M+Na`, `M+K`, etc. / float | Mass tolerance in Da |
## Best Practices
1. **Use `refmet/match` first for free-text input.** `compound/name/...` is rejected by the server (`name` is not an allowed `input_item`).
2. **`moverz` is TSV-only.** Parse with `pd.read_csv(io.StringIO(r.text), sep="\t")` — never call `.json()` on the response.
3. **Don't append `/json` to `study/.../summary`.** The default is JSON; the suffix flips the response to TSV.
4. **`refmet/name/{x}/all` needs the canonical RefMet name.** If you have user text, run it through `refmet/match` first.
5. **Compound results paged by formula come keyed `'1','2',...`** — iterate `dict.values()` or pass to `pd.DataFrame.from_dict(orient="index")`.
6. **Always `time.sleep(0.3)` in batch loops** — no rate limit is published but the server is shared.
## Common Recipes
### Recipe: Cross-Database ID Mapping (PubChem ↔ KEGG ↔ HMDB)
```python
import requests, pandas as pd
BASE = "https://www.metabolomicsworkbench.org/rest"
def cross_refs(refmet_name):
rm = requests.get(f"{BASE}/refmet/name/{refmet_name}/all", timeout=30).json()
if not isinstance(rm, dict) or not rm:
return None
cid = rm.get("pubchem_cid")
if not cid:
return {"refmet": refmet_name, "pubchem_cid": None}
c = requests.get(f"{BASE}/compound/pubchem_cid/{cid}/all/json", timeout=30).json()
return {"refmet": refmet_name,
"pubchem_cid": cid,
"kegg_id": c.get("kegg_id"),
"hmdb_id": c.get("hmdb_id"),
"inchi_key": rm.get("inchi_key")}
df = pd.DataFrame([cross_refs(n) for n in ["Glucose", "L-Tyrosine", "Cholesterol"]])
print(df.to_string(index=False))
```
### Recipe: Pull a Study's Metabolite Table
```python
import requests, pandas as pd
BASE = "https://www.metabolomicsworkbench.org/rest"
r = requests.get(f"{BASE}/study/study_id/ST000001/metabolites", timeout=60)
r.raise_for_status()
rows = list(r.json().values())
df = pd.DataFrame(rows)
print(f"ST000001 metabolites: {len(df)}")
print(df[["analysis_id", "analysis_summary", "metabolite_name", "refmet_name"]].head(5).to_string(index=False))
```
### Recipe: Gene → Metabolomics Pathways
```python
import requests
BASE = "https://www.metabolomicsworkbench.org/rest"
g = requests.get(f"{BASE}/gene/gene_symbol/HMGCR/all", timeout=30).json()
print({k: g.get(k) for k in ["gene_symbol", "gene_id", "mgp_id",
"gene_name", "gene_synonyms"]})
```
## Troubleshooting
| Problem | Cause | Solution |
|---------|-------|----------|
| `"This input item (name) is not allowed..."` | `compound/name/...` is rejected | Use one of the allowed `input_item` values (`pubchem_cid`, `kegg_id`, `inchi_key`, `hmdb_id`, etc.); for free text, go through `refmet/match/{x}` first |
| `JSONDecodeError` on `moverz` response | `moverz` returns TSV text, not JSON | Parse with `pd.read_csv(io.StringIO(r.text), sep="\t")` |
| Empty list from `refmet/name/{x}/all` | Need the canonical RefMet name (case-sensitive) | Normalise via `refmet/match/{user_text}` first, then plug `refmet_name` into `refmet/name/.../all` |
| `metstat/filter/...` returns `[]` | Endpoint syntax is non-functional | Use `study/{refmet_name|last_name|institute|...}/{value}/summary` and filter client-side |
| `study/.../summary` returns TSV instead of JSON | The `/json` suffix flips to TSV | Drop the trailing `/json` — JSON is the default |
| Compound query by formula returns a dict, not a list | Server pages multiple matches as `{'1': {...}, '2': {...}}` | Iterate `dict.values()` (or `pd.DataFrame.from_dict(d, orient='index')`) |
| `refmet/match` response lacks `pubchem_cid` | `match` returns the lightweight record | Use `refmet/name/{refmet_name}/all` for the full record |
## Related Skills
- `hmdb-database` — Local HMDB XML (220K metabolites, NMR/MS spectra, disease links) for offline queries
- `pubchem-compound-search` — General compound property lookups (110M+ compounds) via PubChemPy
- `kegg-database` — Pathway and orthology data complementary to MW's study/metabolite hits
- `chembl-database-bioactivity` — Bioactivity data for the same compounds
## References
- [Metabolomics Workbench home](https://www.metabolomicsworkbench.org/)
- [REST API documentation (PDF)](https://www.metabolomicsworkbench.org/tools/MWRestAPIv1.0.pdf)
- [RefMet nomenclature](https://www.metabolomicsworkbench.org/databases/refmet/index.php)
- Sud M et al. "Metabolomics Workbench: An international repository for metabolomics data and metadata." *Nucleic Acids Research* 44(D1): D463–D470 (2016). https://doi.org/10.1093/nar/gkv1042
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!