Install any skill in seconds. Free to start, no credit card required.
Get Started Free →ENA REST API for sequences, reads, assemblies, and annotations. Portal API search, Browser API retrieval (XML/FASTA/EMBL), file reports for FASTQ/BAM URLs, taxonomy, cross-refs. For multi-DB Python use bioservices; for NCBI-only use pubmed-database or Biopython Entrez.
.claude/skills/jaechang-hits-ena-database/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 178% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 155% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 111% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 265% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 313% | 0% |
The European Nucleotide Archive (ENA) is EMBL-EBI's comprehensive nucleotide sequence database, encompassing raw sequencing reads, genome assemblies, annotated sequences, and associated metadata. It mirrors and extends INSDC data (GenBank, DDBJ). All access is via REST APIs with no authentication required.
bioservices insteadpubmed-database or Biopython Entrezbashpip install requests
API constraints:
limit + offset) or streaming (limit=0 for TSV download)https://www.ebi.ac.uk/ena/portal/apihttps://www.ebi.ac.uk/ena/browser/apihttps://www.ebi.ac.uk/ena/taxonomy/resthttps://www.ebi.ac.uk/ena/xref/restpythonimport requests import time BASE_PORTAL = "https://www.ebi.ac.uk/ena/portal/api" BASE_BROWSER = "https://www.ebi.ac.uk/ena/browser/api" BASE_TAXONOMY = "https://www.ebi.ac.uk/ena/taxonomy/rest" BASE_XREF = "https://www.ebi.ac.uk/ena/xref/rest" def ena_query(endpoint, params=None, base=BASE_PORTAL): """Reusable ENA API caller with rate-limit compliance.""" resp = requests.get(f"{base}/{endpoint}", params=params) resp.raise_for_status() time.sleep(0.02) # 50 req/sec limit return resp # Search for human RNA-seq studies resp = ena_query("search", params={ "result": "study", "query": 'tax_tree(9606)', # `library_strategy` is a `read_run`/`read_experiment` field, not a `study` field "fields": "study_accession,study_title", "format": "json", "limit": 3, }) studies = resp.json() for s in studies: print(f"{s['study_accession']}: {s['study_title'][:60]}") # PRJEB12345: Transcriptome analysis of human liver tissue...
The Portal API provides advanced metadata search across all ENA data types with boolean query syntax, field selection, and pagination.
python# Search read runs for a specific study resp = ena_query("search", params={ "result": "read_run", "query": 'study_accession="PRJEB1787"', "fields": "run_accession,sample_accession,instrument_model,read_count,base_count", "format": "json", "limit": 5, }) runs = resp.json() for r in runs: print(f"{r['run_accession']} — {r.get('instrument_model', 'N/A')}, " f"{int(r.get('read_count', 0)):,} reads") # ERR123456 — Illumina HiSeq 2000, 45,231,890 reads # Count total results without fetching data count_resp = ena_query("count", params={ "result": "read_run", "query": 'study_accession="PRJEB1787"', }) print(f"Total runs: {count_resp.text.strip()}") # Total runs: 142
Fetch individual records by accession in multiple formats: XML, FASTA, EMBL flat-file, or plain text.
python# Retrieve XML metadata for a study resp = ena_query("xml/PRJEB1787", base=BASE_BROWSER) print(resp.text[:300]) # <?xml version="1.0" encoding="UTF-8"?><PROJECT_SET>... # Retrieve FASTA sequence for a coding sequence resp = ena_query("fasta/M10051.1", base=BASE_BROWSER) print(resp.text[:200]) # >ENA|M10051|M10051.1 Human insulin mRNA, complete cds. # AGCCCTCCAGGACAGGCTGCAT... # Retrieve EMBL flat-file format resp = ena_query("embl/M10051.1", base=BASE_BROWSER) print(resp.text[:300]) # ID M10051; SV 1; linear; mRNA; STD; HUM; 786 BP. # ...
Get download URLs for FASTQ, submitted, and analysis files. File reports return FTP and Aspera paths.
python# Get FASTQ file URLs for specific runs resp = ena_query("filereport", params={ "accession": "ERR000589", "result": "read_run", "fields": "run_accession,fastq_ftp,fastq_bytes,fastq_md5", "format": "json", }) files = resp.json() for f in files: ftp_urls = f.get("fastq_ftp", "").split(";") sizes = f.get("fastq_bytes", "").split(";") for url, size in zip(ftp_urls, sizes): if url: print(f"ftp://{url} ({int(size)/1e6:.1f} MB)") # ftp://ftp.sra.ebi.ac.uk/vol1/fastq/ERR000/ERR000589/ERR000589_1.fastq.gz (234.5 MB) # ftp://ftp.sra.ebi.ac.uk/vol1/fastq/ERR000/ERR000589/ERR000589_2.fastq.gz (241.2 MB)
Look up organisms by taxonomy ID, scientific name, or partial name match.
python# Lookup by taxonomy ID resp = ena_query("tax-id/9606", base=BASE_TAXONOMY) tax = resp.json() print(f"{tax['scientificName']} (taxId: {tax['taxId']}, rank: {tax['rank']})") # Homo sapiens (taxId: 9606, rank: species) print(f"Lineage: {tax['lineage'][:80]}...") # Search by scientific name — endpoint returns a list (one entry per matching taxon) resp = ena_query("scientific-name/Arabidopsis thaliana", base=BASE_TAXONOMY) matches = resp.json() result = matches[0] if isinstance(matches, list) else matches print(f"Tax ID: {result['taxId']}, Common: {result.get('commonName', 'N/A')}") # Tax ID: 3702, Common: thale cress # Suggest organisms by partial name resp = ena_query("suggest-for-search/salmo", base=BASE_TAXONOMY) suggestions = resp.json() for s in suggestions[:3]: print(f" {s['scientificName']} (taxId: {s['taxId']})") # Salmo salar (taxId: 8030) # Salmo trutta (taxId: 8032) # Salmonella enterica (taxId: 28901)
Find links between ENA records and external databases (ArrayExpress, UniProt, PDB, etc.).
python# Find cross-references for an ENA accession resp = ena_query("json/search", base=BASE_XREF, params={ "accession": "M10051", }) xrefs = resp.json() for x in xrefs[:5]: print(f" {x['Source']} → {x['Source Primary Accession']} " f"({x.get('Source Description', '')[:50]})") # UniProt → P01308 (Insulin precursor) # PDB → 1A7F (Crystal structure of human insulin) # Search cross-references by external database resp = ena_query("json/search", base=BASE_XREF, params={ "source": "UniProt", "accession": "P01308", }) xrefs = resp.json() for x in xrefs[:3]: print(f" ENA: {x['Target Primary Accession']} — {x.get('Target Description', '')[:60]}")
Retrieve reference sequences used in CRAM files by MD5 or SHA1 checksum. Essential for CRAM decompression.
python# Look up reference by MD5 checksum md5 = "aef131c3b4b05d8e2b3f907faba5af9b" # example try: resp = ena_query( f"cram/md5/{md5}", base="https://www.ebi.ac.uk/ena/cram" ) print(f"Reference found: {len(resp.content)} bytes") except requests.HTTPError as e: if e.response.status_code == 404: print("Reference not found — check MD5 checksum") else: raise
| Level | Accession Prefix | Description | Contains | |-------|-----------------|-------------|----------| | Study | PRJEB/ERP | Research project | Samples, Experiments | | Sample | ERS/SAMEA | Biological sample | Metadata, taxonomy | | Experiment | ERX | Library/sequencing setup | Runs | | Run | ERR | Sequencing run | Raw read files (FASTQ) | | Analysis | ERZ | Derived analysis | Assemblies, alignments | | Assembly | GCA | Genome assembly | Contigs, scaffolds | | Sequence | Accession.version | Annotated sequence | Features, coding seqs |
| Operator | Example | Description | |----------|---------|-------------| | Equality | instrument_model="Illumina NovaSeq 6000" | Exact match | | Wildcard | study_title="*melanoma*" | Partial match | | Range | base_count>=1000000 | Numeric comparison | | Taxonomy tree | tax_tree(9606) | Taxon and all descendants | | Exact taxon | tax_eq(9606) | Exact taxon only | | Date range | first_public>=2023-01-01 | Date filtering | | Boolean | AND, OR, NOT | Combine conditions | | Grouping | (A OR B) AND C | Parenthetical grouping |
| Result Type | Description | Key Fields | |------------|-------------|------------| | study | Research projects | study_accession, study_title, center_name | | sample | Biological samples | sample_accession, tax_id, scientific_name | | read_run | Sequencing runs | run_accession, read_count, base_count, fastq_ftp | | read_experiment | Experiments | experiment_accession, library_strategy, instrument_model | | analysis | Derived analyses | analysis_accession, analysis_type | | assembly | Genome assemblies | assembly_accession, assembly_level, genome_representation | | sequence | Annotated sequences | accession, sequence_length, mol_type | | wgs_set | WGS scaffold sets | set_accession, set_size | | tsa_set | Transcriptome assemblies | set_accession, set_size | | coding | Coding sequences | accession, gene, product | | noncoding | Non-coding features | accession, description | | taxon | Taxonomy entries | tax_id, scientific_name, lineage |
Use the returnFields endpoint to discover available fields for any result type:
pythonresp = ena_query("returnFields", params={"result": "read_run"}) fields = resp.text.strip().split("\n") print(f"Available fields for read_run: {len(fields)}") print(fields[:10]) # ['accession', 'altitude', 'assembly_quality', 'assembly_software', ...]
Search for a study, list its samples, then retrieve run metadata.
pythonimport json # Step 1: Find studies by organism — `study_title="*…*"` wildcards no longer match; # use `tax_tree()` against the species tax ID (SARS-CoV-2 = 2697049). resp = ena_query("search", params={ "result": "study", "query": 'tax_tree(2697049) AND first_public>=2023-01-01', "fields": "study_accession,study_title,center_name", "format": "json", "limit": 3, }) studies = resp.json() study_acc = studies[0]["study_accession"] print(f"Selected: {study_acc} — {studies[0]['study_title'][:60]}") # Step 2: List samples in the study resp = ena_query("search", params={ "result": "sample", "query": f'study_accession="{study_acc}"', "fields": "sample_accession,scientific_name,collection_date", "format": "json", "limit": 5, }) samples = resp.json() print(f"Found {len(samples)} samples (showing first 5)") for s in samples: print(f" {s['sample_accession']} — {s.get('scientific_name', 'N/A')}") # Step 3: Get run metadata for each sample for s in samples[:2]: resp = ena_query("search", params={ "result": "read_run", "query": f'sample_accession="{s["sample_accession"]}"', "fields": "run_accession,instrument_model,read_count,library_strategy", "format": "json", }) runs = resp.json() for r in runs: print(f" {r['run_accession']}: {r.get('library_strategy','N/A')}, " f"{int(r.get('read_count',0)):,} reads") time.sleep(0.02)
Search for runs matching criteria and collect download URLs.
python# Step 1: Search for Illumina RNA-Seq runs from a specific organism resp = ena_query("search", params={ "result": "read_run", "query": ('tax_tree(10090) AND library_strategy="RNA-Seq" ' 'AND instrument_platform="ILLUMINA" AND read_count>=10000000'), "fields": "run_accession,study_accession,read_count", "format": "json", "limit": 10, }) runs = resp.json() print(f"Found {len(runs)} runs meeting criteria") # Step 2: Get file reports with download URLs download_list = [] for run in runs[:5]: acc = run["run_accession"] resp = ena_query("filereport", params={ "accession": acc, "result": "read_run", "fields": "run_accession,fastq_ftp,fastq_bytes,fastq_md5", "format": "json", }) for f in resp.json(): urls = f.get("fastq_ftp", "").split(";") md5s = f.get("fastq_md5", "").split(";") for url, md5 in zip(urls, md5s): if url: download_list.append({"url": f"ftp://{url}", "md5": md5, "run": acc}) time.sleep(0.02) print(f"\nDownload list: {len(download_list)} files") for d in download_list[:4]: print(f" {d['run']}: {d['url'].split('/')[-1]}")
Find organisms, search their assemblies, and check quality statistics.
python# Step 1: Resolve organism by exact scientific name (returns a list, take the first match). # `suggest-for-search` only returns prefix matches and may not include the species you want. resp = ena_query("scientific-name/Drosophila melanogaster", base=BASE_TAXONOMY) matches = resp.json() target_tax = matches[0]["taxId"] print(f"Selected: {matches[0]['scientificName']} (taxId={target_tax})") # Step 2: Search assemblies for this organism. `n50` is no longer a valid `assembly` # field — use `base_count` and `program` for what's available. resp = ena_query("search", params={ "result": "assembly", "query": f'tax_eq({target_tax}) AND assembly_level="chromosome"', "fields": ("assembly_accession,assembly_name,assembly_level," "genome_representation,base_count"), "format": "json", "limit": 5, }) assemblies = resp.json() for a in assemblies: size = int(a.get("base_count", 0)) print(f" {a['assembly_accession']}: {a.get('assembly_name','N/A')}, " f"Size={size/1e6:.1f} Mb") # GCA_000001215.4: Release 6 plus ISO1 MT, Size=143.7 Mb
| Endpoint | Parameter | Default | Description | |----------|-----------|---------|-------------| | search | result | (required) | Result type: study, sample, read_run, assembly, etc. | | search | query | (required) | Boolean query string with field operators | | search | fields | all | Comma-separated field names to return | | search | format | tsv | Output format: json, tsv, xml | | search | limit | 100000 | Max results (0 = unlimited streaming) | | search | offset | 0 | Skip first N results (pagination) | | search | sortFields | — | Field(s) to sort by | | filereport | accession | (required) | Study, sample, or run accession | | filereport | result | (required) | read_run, analysis, etc. | | xml/ | accession path | (required) | Any ENA accession (Browser API) | | fasta/ | accession path | (required) | Sequence accession (Browser API) |
tax_tree() over tax_eq() for organism queries — it includes subspecies and strains automaticallylimit=0 for streaming large result sets directly to file, avoiding memory issuesfastq_ftp and submitted_ftp — some runs have submitted files but no processed FASTQfastq_md5 for integrity checkingpython# Find chromosome-level, full-representation assemblies. # `n50` is no longer a valid `assembly` field — filter by base_count instead. resp = ena_query("search", params={ "result": "assembly", "query": ('tax_tree(7742) AND assembly_level="chromosome" ' 'AND genome_representation="full"'), "fields": "assembly_accession,scientific_name,base_count,assembly_level", "format": "json", "limit": 10, }) for a in resp.json(): size = int(a.get("base_count", 0)) if size > 100_000_000: # >100 Mb (proxy for "well-assembled") print(f"{a['assembly_accession']}: {a.get('scientific_name','?')}, Size={size/1e6:.0f} Mb")
python# Find UniProt/PDB cross-references for an ENA sequence resp = ena_query("json/search", base=BASE_XREF, params={ "accession": "M10051", }) xrefs = resp.json() by_source = {} for x in xrefs: src = x.get("Source", "unknown") by_source.setdefault(src, []).append(x["Source Primary Accession"]) for src, accs in by_source.items(): print(f" {src}: {', '.join(accs[:5])}") # UniProt: P01308 # PDB: 1A7F, 1AI0, 1BEN
pythonfrom requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def ena_session(): """Create a requests session with retry logic for ENA APIs.""" session = requests.Session() retry = Retry( total=3, backoff_factor=1.0, status_forcelist=[429, 500, 502, 503, 504], ) session.mount("https://", HTTPAdapter(max_retries=retry)) return session session = ena_session() resp = session.get(f"{BASE_PORTAL}/search", params={ "result": "study", "query": 'tax_tree(9606)', "fields": "study_accession", "format": "json", "limit": 5, }) print(f"Status: {resp.status_code}, results: {len(resp.json())}")
| Problem | Cause | Solution | |---------|-------|----------| | 400 Bad Request on search | Invalid query syntax or unknown field name | Use returnFields endpoint to verify field names; check query operator syntax | | 400 with result param | Invalid result type | Check result types table above; common: read_run not run | | Empty results for known data | Wrong taxonomy operator | Use tax_tree() (includes descendants) not tax_eq() (exact only) | | fastq_ftp field is empty | Submitted files not processed to FASTQ | Check submitted_ftp field instead; some datasets only have BAM/CRAM | | 429 Too Many Requests | Exceeded 50 req/sec rate limit | Add time.sleep(0.02) between requests; use retry session with backoff | | Timeout on large queries | Result set too large for single request | Use limit + offset pagination, or limit=0 with streaming to file | | XML parsing errors | Malformed XML for some records | Use JSON format instead (format=json) when available | | Wrong sequence version | Accession without version suffix | Always use versioned accessions (e.g., M10051.1 not M10051) for Browser API | | CRAM reference not found | MD5 checksum mismatch or non-INSDC reference | Verify MD5; check if reference is from a custom genome (not in registry) |
This skill is self-contained. The original entry had a separate references/api_reference.md (490 lines) covering all 6 API endpoints in detail. That content has been fully consolidated inline:
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 18,966 | 21,563 | +14% | 1 | 1 | 0% | 3,754 | 10,445 | +178% | 0 | 0 | — |
case-02 | fail→pass | 16,291 | 7,896 | -52% | 1 | 1 | 0% | 3,177 | 8,094 | +155% | 0 | 0 | — |
case-03 | fail→pass | 23,239 | 17,246 | -26% | 1 | 1 | 0% | 4,670 | 9,872 | +111% | 0 | 0 | — |
case-04 | pass→pass | 15,074 | 7,829 | -48% | 1 | 1 | 0% | 2,618 | 8,026 | +207% | 0 | 0 | — |
case-05 | pass→pass | 13,242 | 12,540 | -5% | 1 | 1 | 0% | 2,533 | 8,912 | +252% | 0 | 0 | — |
case-06 | pass→pass | 10,866 | 5,858 | -46% | 1 | 1 | 0% | 2,037 | 7,616 | +274% | 0 | 0 | — |
case-07 | pass→pass | 7,006 | 3,968 | -43% | 1 | 1 | 0% | 1,293 | 7,301 | +465% | 0 | 0 | — |
case-08 | pass→pass | 7,385 | 4,415 | -40% | 1 | 1 | 0% | 1,485 | 7,400 | +398% | 0 | 0 | — |
case-09 | pass→pass | 7,873 | 4,779 | -39% | 1 | 1 | 0% | 1,525 | 7,490 | +391% | 0 | 0 | — |
case-10 | pass→pass | 11,573 | 6,419 | -45% | 1 | 1 | 0% | 2,139 | 7,701 | +260% | 0 | 0 | — |
case-19 | pass→pass | 8,813 | 5,649 | -36% | 1 | 1 | 0% | 1,797 | 7,632 | +325% | 0 | 0 | — |
case-11 | fail→pass | 27,114 | 3,829 | -86% | 1 | 1 | 0% | 1,988 | 7,259 | +265% | 0 | 0 | — |
case-12 | fail→pass | 10,453 | 8,470 | -19% | 1 | 1 | 0% | 1,987 | 8,199 | +313% | 0 | 0 | — |
case-13 | pass→pass | 7,588 | 4,027 | -47% | 1 | 1 | 0% | 1,392 | 7,270 | +422% | 0 | 0 | — |
case-14 | pass→pass | 13,079 | 11,913 | -9% | 1 | 1 | 0% | 2,542 | 9,072 | +257% | 0 | 0 | — |
case-15 | fail→pass | 10,144 | 6,448 | -36% | 1 | 1 | 0% | 1,952 | 7,832 | +301% | 0 | 0 | — |
case-16 | pass→pass | 6,909 | 7,406 | +7% | 1 | 1 | 0% | 1,365 | 7,942 | +482% | 0 | 0 | — |
case-17 | pass→pass | 14,422 | 9,624 | -33% | 1 | 1 | 0% | 2,632 | 8,351 | +217% | 0 | 0 | — |
case-18 | pass→pass | 7,839 | 4,257 | -46% | 1 | 1 | 0% | 1,221 | 7,284 | +497% | 0 | 0 | — |
case-20 | fail→pass | 13,485 | 7,484 | -45% | 1 | 1 | 0% | 2,535 | 8,035 | +217% | 0 | 0 | — |
case-21 | pass→pass | 8,864 | 5,181 | -42% | 1 | 1 | 0% | 1,778 | 7,589 | +327% | 0 | 0 | — |
case-22 | pass→pass | 7,522 | 5,718 | -24% | 1 | 1 | 0% | 1,491 | 7,677 | +415% | 0 | 0 | — |
case-23 | pass→pass | 10,820 | 8,133 | -25% | 1 | 1 | 0% | 2,084 | 8,086 | +288% | 0 | 0 | — |
case-24 | pass→pass | 10,285 | 7,107 | -31% | 1 | 1 | 0% | 1,800 | 7,909 | +339% | 0 | 0 | — |
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. 24 cases were attempted. The headline lift of +29 percentage points is the difference between those two pass rates over the 24 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.