Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Query RCSB PDB (200K+ structures) via the public REST + GraphQL APIs with plain `requests` (no SDK). Search by text, attribute, sequence, or 3D structure similarity (Search API); retrieve metadata via GraphQL (Data API); download PDB/mmCIF from files.rcsb.org. For AlphaFold predictions use alphafold-database-access; for protein sequences only use uniprot-protein-database.
.claude/skills/jaechang-hits-pdb-database/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 809% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 1152% | 0% |
| case-19 | ✗→✓ | ▲ Improved | 249% | 0% |
| case-20 | ✗→✓ | ▲ Improved | 476% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 251% | 0% |
> Why no SDK? The rcsb-api Python SDK is convenient sugar over three public, no-auth REST endpoints (search.rcsb.org, data.rcsb.org, files.rcsb.org). When the SDK is unavailable, every operation can be reproduced with plain requests and a small JSON payload. This SKILL.md uses the REST path throughout so the code runs in any environment with requests installed.
RCSB PDB is the worldwide repository for 3D structural data of biological macromolecules with 200,000+ experimentally determined structures. Programmatic access is via three free, no-auth endpoints:
| API | Base URL | Method | Purpose | |---|---|---|---| | Search | https://search.rcsb.org/rcsbsearch/v2/query | POST JSON | Find PDB IDs by text, attribute filters, sequence, or 3D similarity | | Data | https://data.rcsb.org/graphql | POST GraphQL | Retrieve structured metadata (entries, polymer entities, assemblies, ligands) | | Files | https://files.rcsb.org/download/{id}.{format} | GET | Download coordinate files (mmCIF, PDB, FASTA) |
Use this skill for programmatic structural biology queries, drug target analysis, and protein family comparisons.
alphafold-database-access insteaduniprot-protein-database insteadrequests (only requirement). Optional: biopython for parsing downloaded coordinate files.time.sleep(0.2-0.5) between requests are sufficient; implement exponential backoff on HTTP 429.bashpip install requests # Optional, for coordinate parsing: pip install biopython
Typical search-then-fetch pattern: hit the Search API, get a list of PDB IDs, then resolve metadata via the GraphQL Data API.
pythonimport requests SEARCH = "https://search.rcsb.org/rcsbsearch/v2/query" DATA = "https://data.rcsb.org/graphql" # 1. Search: human X-ray structures of "kinase" at resolution < 2.0 Å payload = { "query": { "type": "group", "logical_operator": "and", "nodes": [ {"type": "terminal", "service": "full_text", "parameters": {"value": "kinase"}}, {"type": "terminal", "service": "text", "parameters": {"attribute": "rcsb_entity_source_organism.scientific_name", "operator": "exact_match", "value": "Homo sapiens"}}, {"type": "terminal", "service": "text", "parameters": {"attribute": "rcsb_entry_info.resolution_combined", "operator": "less", "value": 2.0}}, ], }, "return_type": "entry", "request_options": {"paginate": {"rows": 10}}, } r = requests.post(SEARCH, json=payload, timeout=30) r.raise_for_status() result = r.json() pdb_ids = [hit["identifier"] for hit in result["result_set"]] print(f"Total matches: {result['total_count']}, first batch: {pdb_ids}") # 2. Fetch metadata for the first hit via GraphQL gql = """{ entry(entry_id: "%s") { struct { title } exptl { method } rcsb_entry_info { resolution_combined deposited_atom_count polymer_entity_count } } }""" % pdb_ids[0] r2 = requests.post(DATA, json={"query": gql}, timeout=30) entry = r2.json()["data"]["entry"] print(entry["struct"]["title"]) print(f"Method: {entry['exptl'][0]['method']}, Resolution: {entry['rcsb_entry_info']['resolution_combined']} Å")
Free-text search uses service: "full_text" and searches across all indexed fields.
pythonimport requests SEARCH = "https://search.rcsb.org/rcsbsearch/v2/query" def text_search(keyword, rows=25): payload = { "query": {"type": "terminal", "service": "full_text", "parameters": {"value": keyword}}, "return_type": "entry", "request_options": {"paginate": {"rows": rows}}, } r = requests.post(SEARCH, json=payload, timeout=30) r.raise_for_status() data = r.json() return [hit["identifier"] for hit in data["result_set"]], data["total_count"] ids, total = text_search("hemoglobin") print(f"Found {total} structures; first batch: {ids[:5]}")
Attribute search uses service: "text" with structured attribute/operator/value parameters.
pythonimport requests SEARCH = "https://search.rcsb.org/rcsbsearch/v2/query" def attribute_search(attribute, operator, value, return_type="entry", rows=25): payload = { "query": {"type": "terminal", "service": "text", "parameters": {"attribute": attribute, "operator": operator, "value": value}}, "return_type": return_type, "request_options": {"paginate": {"rows": rows}}, } r = requests.post(SEARCH, json=payload, timeout=30) r.raise_for_status() return r.json() # Human proteins human = attribute_search("rcsb_entity_source_organism.scientific_name", "exact_match", "Homo sapiens", rows=5) print(f"Human structures: {human['total_count']}") # X-ray only xray = attribute_search("exptl.method", "exact_match", "X-RAY DIFFRACTION", rows=5) print(f"X-ray structures: {xray['total_count']}") # Resolution range: 1.5–2.5 Å res = attribute_search( "rcsb_entry_info.resolution_combined", "range", {"from": 1.5, "to": 2.5, "include_lower": True, "include_upper": True}, rows=5 ) print(f"1.5–2.5 Å: {res['total_count']}")
Find structures with similar sequences using MMseqs2. Service is "sequence"; target selects protein vs. nucleic acid.
pythonimport requests SEARCH = "https://search.rcsb.org/rcsbsearch/v2/query" kras_seq = ("MTEYKLVVVGAGGVGKSALTIQLIQNHFVDEYDPTIEDSYRKQVVIDGETCLLDILDTAGQ" "EEYSAMRDQYMRTGEGFLCVFAINNTKSFEDIHHYREQIKRVKDSEDVPMVLVGNKCDLPS" "RTVDTKQAQDLARSYGIPFIETSAKTRQGVDDAFYTLVREIRKHKEKMSK") payload = { "query": { "type": "terminal", "service": "sequence", "parameters": { "target": "pdb_protein_sequence", # or "pdb_dna_sequence", "pdb_rna_sequence" "value": kras_seq, "evalue_cutoff": 0.1, "identity_cutoff": 0.9, }, }, "return_type": "polymer_entity", "request_options": {"paginate": {"rows": 10}}, } r = requests.post(SEARCH, json=payload, timeout=30) r.raise_for_status() data = r.json() print(f"KRAS-like hits: {data['total_count']}") for hit in data["result_set"][:5]: print(f" {hit['identifier']} score={hit.get('score', 'n/a')}")
Find structures with similar 3D geometry using BioZernike descriptors. Service is "structure"; pass the reference entry + assembly ID.
pythonimport requests SEARCH = "https://search.rcsb.org/rcsbsearch/v2/query" payload = { "query": { "type": "terminal", "service": "structure", "parameters": { "value": {"entry_id": "4HHB", "assembly_id": "1"}, "operator": "strict_shape_match", # or "relaxed_shape_match" }, }, "return_type": "polymer_entity", "request_options": {"paginate": {"rows": 10}}, } r = requests.post(SEARCH, json=payload, timeout=30) r.raise_for_status() data = r.json() print(f"Structurally similar to 4HHB: {data['total_count']}") for hit in data["result_set"][:5]: print(f" {hit['identifier']} score={hit.get('score', 'n/a')}")
The GraphQL endpoint at data.rcsb.org/graphql is the canonical way to retrieve structured metadata for known PDB IDs. One request can pull fields across the full data hierarchy (entry → polymer_entity → assembly → chem_comp).
pythonimport requests DATA = "https://data.rcsb.org/graphql" # Entry-level metadata gql = """{ entry(entry_id: "4HHB") { struct { title } exptl { method } rcsb_entry_info { resolution_combined deposited_atom_count polymer_entity_count nonpolymer_entity_count } rcsb_accession_info { deposit_date initial_release_date } } }""" r = requests.post(DATA, json={"query": gql}, timeout=30) entry = r.json()["data"]["entry"] print(f"Title : {entry['struct']['title']}") print(f"Method : {entry['exptl'][0]['method']}") print(f"Resolution : {entry['rcsb_entry_info']['resolution_combined']} Å") print(f"Atoms : {entry['rcsb_entry_info']['deposited_atom_count']}")
python# Polymer entity (sequence, organism, MW) gql = """{ polymer_entity(entry_id: "4HHB", entity_id: "1") { entity_poly { pdbx_seq_one_letter_code } rcsb_polymer_entity { formula_weight } rcsb_entity_source_organism { scientific_name ncbi_taxonomy_id } } }""" r = requests.post(DATA, json={"query": gql}, timeout=30) pe = r.json()["data"]["polymer_entity"] print(f"Sequence (first 50): {pe['entity_poly']['pdbx_seq_one_letter_code'][:50]}") print(f"Organism : {pe['rcsb_entity_source_organism'][0]['scientific_name']}") print(f"MW : {pe['rcsb_polymer_entity']['formula_weight']}")
python# Batch: pull metadata for many entries in one request gql = """{ entries(entry_ids: ["4HHB", "1A3N", "1HHB"]) { rcsb_id struct { title } exptl { method } rcsb_entry_info { resolution_combined } } }""" r = requests.post(DATA, json={"query": gql}, timeout=30) for e in r.json()["data"]["entries"]: res = e["rcsb_entry_info"]["resolution_combined"] print(f" {e['rcsb_id']}: {e['exptl'][0]['method']:<25} {res} Å — {e['struct']['title'][:40]}")
Coordinate files (mmCIF, PDB, FASTA, assembly variants) are served directly from files.rcsb.org.
pythonimport requests def download_structure(pdb_id, fmt="cif", output_dir="."): """Download mmCIF / PDB / FASTA. URLs: .pdb, .cif, /fasta/entry/{ID}, .pdb1 (assembly).""" url = f"https://files.rcsb.org/download/{pdb_id}.{fmt}" r = requests.get(url, timeout=60) if r.status_code == 200: path = f"{output_dir}/{pdb_id}.{fmt}" # mmCIF / PDB are text; assemblies and biological units are also text with open(path, "w") as f: f.write(r.text) print(f"Downloaded {path} ({len(r.text)/1024:.1f} KB)") return path print(f"HTTP {r.status_code} for {pdb_id}.{fmt}") return None download_structure("4HHB", fmt="cif") download_structure("4HHB", fmt="pdb")
python# FASTA sequence for an entry r = requests.get("https://www.rcsb.org/fasta/entry/4HHB", timeout=30) r.raise_for_status() print(r.text[:400])
Combine terminal queries with type: "group" and a logical_operator of "and" / "or". Nested groups give arbitrary boolean expressions; negation is via "node_id" references with "operator": "negate" on the group (rare — usually expressed as the inverse attribute filter).
pythonimport requests, datetime SEARCH = "https://search.rcsb.org/rcsbsearch/v2/query" # AND: high-resolution human structures q_and = { "type": "group", "logical_operator": "and", "nodes": [ {"type": "terminal", "service": "text", "parameters": {"attribute": "rcsb_entity_source_organism.scientific_name", "operator": "exact_match", "value": "Homo sapiens"}}, {"type": "terminal", "service": "text", "parameters": {"attribute": "rcsb_entry_info.resolution_combined", "operator": "less", "value": 2.0}}, ], } # OR: human or mouse q_or = { "type": "group", "logical_operator": "or", "nodes": [ {"type": "terminal", "service": "text", "parameters": {"attribute": "rcsb_entity_source_organism.scientific_name", "operator": "exact_match", "value": "Homo sapiens"}}, {"type": "terminal", "service": "text", "parameters": {"attribute": "rcsb_entity_source_organism.scientific_name", "operator": "exact_match", "value": "Mus musculus"}}, ], } # Combined: recent (last 30 days) + high-quality one_month_ago = (datetime.date.today() - datetime.timedelta(days=30)).isoformat() today = datetime.date.today().isoformat() q_recent_hq = { "type": "group", "logical_operator": "and", "nodes": [ {"type": "terminal", "service": "text", "parameters": {"attribute": "rcsb_entry_info.resolution_combined", "operator": "less", "value": 2.0}}, {"type": "terminal", "service": "text", "parameters": {"attribute": "refine.ls_R_factor_R_free", "operator": "less", "value": 0.25}}, {"type": "terminal", "service": "text", "parameters": {"attribute": "rcsb_accession_info.initial_release_date", "operator": "range", "value": {"from": one_month_ago, "to": today, "include_lower": True, "include_upper": True}}}, ], } payload = {"query": q_recent_hq, "return_type": "entry", "request_options": {"paginate": {"rows": 5}}} r = requests.post(SEARCH, json=payload, timeout=30) print(f"Recent high-quality: {r.json()['total_count']} structures")
Search responses include total_count. Paginate with request_options.paginate.start and rows (max ~10000 per page in practice; 100–500 is a good batch size).
pythonimport requests, time SEARCH = "https://search.rcsb.org/rcsbsearch/v2/query" def search_all(query_node, return_type="entry", page=100, max_results=None, delay=0.3): """Paginate through every result; rate-limit between pages.""" out, start = [], 0 while True: payload = {"query": query_node, "return_type": return_type, "request_options": {"paginate": {"start": start, "rows": page}}} r = requests.post(SEARCH, json=payload, timeout=30) r.raise_for_status() d = r.json() batch = [h["identifier"] for h in d.get("result_set", [])] if not batch: break out.extend(batch) if max_results and len(out) >= max_results: return out[:max_results] if len(batch) < page or len(out) >= d.get("total_count", 0): break start += page time.sleep(delay) return out # Example: every insulin entry ids = search_all( {"type": "terminal", "service": "full_text", "parameters": {"value": "insulin"}}, max_results=300, ) print(f"Insulin entries collected: {len(ids)}")
python# Batch metadata fetch via GraphQL `entries(...)` to avoid one round-trip per ID DATA = "https://data.rcsb.org/graphql" def batch_metadata(pdb_ids, chunk=50): """Fetch (title, method, resolution) for many entries with one POST per chunk.""" all_rows = [] for i in range(0, len(pdb_ids), chunk): ids_arr = pdb_ids[i:i+chunk] ids_str = ", ".join(f'"{p}"' for p in ids_arr) gql = f"""{{ entries(entry_ids: [{ids_str}]) {{ rcsb_id struct {{ title }} exptl {{ method }} rcsb_entry_info {{ resolution_combined }} }} }}""" r = requests.post(DATA, json={"query": gql}, timeout=60) r.raise_for_status() for e in r.json()["data"]["entries"]: res = e["rcsb_entry_info"]["resolution_combined"] all_rows.append({ "pdb_id": e["rcsb_id"], "method": e["exptl"][0]["method"] if e["exptl"] else None, "resolution": res[0] if isinstance(res, list) and res else res, "title": e["struct"]["title"], }) return all_rows rows = batch_metadata(ids[:20]) for r in rows[:5]: print(f" {r['pdb_id']}: {r['method']:<25} {r['resolution']} Å — {r['title'][:50]}")
| Service | Use case | Required parameters | |---|---|---| | full_text | Free-text keyword across all indexed fields | value (string) | | text | Structured attribute filter | attribute, operator, value | | sequence | MMseqs2 sequence similarity | target ∈ {pdb_protein_sequence, pdb_dna_sequence, pdb_rna_sequence}, value (sequence), evalue_cutoff, identity_cutoff | | seqmotif | Pattern / regex / PROSITE motif | value (pattern), pattern_type ∈ {simple, prosite, regex} | | structure | 3D shape similarity (BioZernike) | value ({entry_id, assembly_id}), operator ∈ {strict_shape_match, relaxed_shape_match} | | strucmotif | 3D residue-arrangement motif | value (residue list), rmsd_cutoff | | chemical | Ligand similarity by SMILES/InChI | value, match_type ∈ {graph-exact, graph-relaxed, fingerprint-similarity, sub-structure-stereo-relaxed} |
"text")| Operator | Value shape | Example | |---|---|---| | exact_match | string | "Homo sapiens" | | contains_words / contains_phrase | string | "tyrosine kinase" | | equals / greater / less / greater_or_equal / less_or_equal | number | 2.0 | | range | {from, to, include_lower, include_upper} | {"from": 1.5, "to": 2.5, "include_lower": True, "include_upper": True} | | exists | (none) | — | | in | array | ["X-RAY DIFFRACTION", "ELECTRON MICROSCOPY"] |
return_type controls the granularity of identifiers in result_set:
| return_type | Identifier shape | Example | |---|---|---| | entry | 4HHB | One per PDB ID | | polymer_entity | 4HHB_1 | One per polymer chain entity | | non_polymer_entity | 4HHB_2 | Ligands, cofactors | | assembly | 4HHB-1 | Biological unit | | polymer_instance | 4HHB.A | Individual chain coordinates | | mol_definition | HEM | Chemical component (PDB ligand code) |
| Root | Identifier shape | Returns | |---|---|---| | entry(entry_id: ...) | "4HHB" | Entry-level metadata | | entries(entry_ids: [...]) | array | Batch entry lookup | | polymer_entity(entry_id: ..., entity_id: ...) | "4HHB", "1" | Sequence + organism | | polymer_entity_instance(entry_id: ..., asym_id: ...) | "4HHB", "A" | Chain-level coords/metadata | | assembly(entry_id: ..., assembly_id: ...) | "4HHB", "1" | Biological assembly | | chem_comp(comp_id: ...) | "HEM" | Small molecule reference |
| Format | URL pattern | Notes | |---|---|---| | mmCIF | https://files.rcsb.org/download/{id}.cif | Recommended; no atom-count limit | | PDB | https://files.rcsb.org/download/{id}.pdb | Legacy; 99,999 atom limit | | Assembly (mmCIF) | https://files.rcsb.org/download/{id}-assembly{N}.cif | Biological unit | | FASTA | https://www.rcsb.org/fasta/entry/{id} | Sequence only |
Goal: Find high-resolution human EGFR structures with bound ligands.
pythonimport requests, time SEARCH = "https://search.rcsb.org/rcsbsearch/v2/query" DATA = "https://data.rcsb.org/graphql" payload = { "query": { "type": "group", "logical_operator": "and", "nodes": [ {"type": "terminal", "service": "full_text", "parameters": {"value": "EGFR epidermal growth factor receptor"}}, {"type": "terminal", "service": "text", "parameters": {"attribute": "rcsb_entity_source_organism.scientific_name", "operator": "exact_match", "value": "Homo sapiens"}}, {"type": "terminal", "service": "text", "parameters": {"attribute": "rcsb_entry_info.resolution_combined", "operator": "less", "value": 2.5}}, ], }, "return_type": "entry", "request_options": {"paginate": {"rows": 50}}, } r = requests.post(SEARCH, json=payload, timeout=30) r.raise_for_status() pdb_ids = [h["identifier"] for h in r.json()["result_set"]] print(f"EGFR ≤2.5 Å human structures: {len(pdb_ids)}") # Filter to entries with bound ligands via batch GraphQL ids_str = ", ".join(f'"{p}"' for p in pdb_ids[:20]) gql = f"""{{ entries(entry_ids: [{ids_str}]) {{ rcsb_id struct {{ title }} rcsb_entry_info {{ resolution_combined nonpolymer_entity_count }} }} }}""" r2 = requests.post(DATA, json={"query": gql}, timeout=60) for e in r2.json()["data"]["entries"]: n_lig = e["rcsb_entry_info"]["nonpolymer_entity_count"] or 0 if n_lig > 0: res = e["rcsb_entry_info"]["resolution_combined"] res_v = res[0] if isinstance(res, list) else res print(f" {e['rcsb_id']}: {res_v} Å, ligands={n_lig} — {e['struct']['title'][:60]}") time.sleep(0.05)
Goal: Find all PDB structures with sequence similar to a query (KRAS), then summarize their resolution + experimental method.
pythonimport requests, time SEARCH = "https://search.rcsb.org/rcsbsearch/v2/query" DATA = "https://data.rcsb.org/graphql" kras_seq = ("MTEYKLVVVGAGGVGKSALTIQLIQNHFVDEYDPTIEDSYRKQVVIDGETCLLDILDTAGQ" "EEYSAMRDQYMRTGEGFLCVFAINNTKSFEDIHHYREQIKRVKDSEDVPMVLVGNKCDLPS" "RTVDTKQAQDLARSYGIPFIETSAKTRQGVDDAFYTLVREIRKHKEKMSK") payload = { "query": { "type": "terminal", "service": "sequence", "parameters": {"target": "pdb_protein_sequence", "value": kras_seq, "evalue_cutoff": 1e-5, "identity_cutoff": 0.5}, }, "return_type": "polymer_entity", "request_options": {"paginate": {"rows": 20}}, } r = requests.post(SEARCH, json=payload, timeout=30) hits = r.json()["result_set"] print(f"KRAS family hits: {len(hits)}") # Unique PDB IDs from polymer_entity identifiers (e.g., "4OBE_1" -> "4OBE") entry_ids = sorted({h["identifier"].split("_")[0] for h in hits}) # Batch metadata ids_str = ", ".join(f'"{p}"' for p in entry_ids) gql = f"""{{ entries(entry_ids: [{ids_str}]) {{ rcsb_id struct {{ title }} exptl {{ method }} rcsb_entry_info {{ resolution_combined }} }} }}""" r2 = requests.post(DATA, json={"query": gql}, timeout=60) for e in sorted(r2.json()["data"]["entries"], key=lambda x: (x["rcsb_entry_info"]["resolution_combined"] or [99])[0] if isinstance(x["rcsb_entry_info"]["resolution_combined"], list) else (x["rcsb_entry_info"]["resolution_combined"] or 99)): res = e["rcsb_entry_info"]["resolution_combined"] res_v = res[0] if isinstance(res, list) else res print(f" {e['rcsb_id']}: {res_v} Å {e['exptl'][0]['method']:<25} {e['struct']['title'][:50]}")
Goal: Download mmCIF, then enumerate chains with BioPython.
pythonimport requests from Bio.PDB import MMCIFParser pdb_id = "4HHB" r = requests.get(f"https://files.rcsb.org/download/{pdb_id}.cif", timeout=60) r.raise_for_status() with open(f"{pdb_id}.cif", "w") as f: f.write(r.text) parser = MMCIFParser(QUIET=True) structure = parser.get_structure(pdb_id, f"{pdb_id}.cif") for model in structure: for chain in model: std_res = [r for r in chain if r.id[0] == " "] atoms = sum(len(list(r.get_atoms())) for r in std_res) print(f"Chain {chain.id}: {len(std_res)} residues, {atoms} atoms")
| Parameter | Endpoint | Default | Range / Options | Effect | |-----------|----------|---------|-----------------|--------| | value | search sequence | required | protein/DNA/RNA sequence string | Query sequence for MMseqs2 | | evalue_cutoff | search sequence | 0.1 | 1e-10–10 | E-value threshold | | identity_cutoff | search sequence | 0.9 | 0.0–1.0 | Minimum identity fraction | | target | search sequence | "pdb_protein_sequence" | pdb_protein_sequence, pdb_dna_sequence, pdb_rna_sequence | Sequence type | | operator | search text (attribute) | required | see Attribute Operators | Comparison kind | | operator | search structure | strict_shape_match | strict_shape_match, relaxed_shape_match | 3D match stringency | | return_type | all search | entry | entry, polymer_entity, assembly, polymer_instance, mol_definition, … | Identifier granularity | | paginate.start / paginate.rows | request_options | 0 / 25 | up to ~10000 rows/page in practice | Pagination window | | GraphQL field entries(entry_ids: [...]) | data.rcsb.org/graphql | — | array of PDB IDs | Batch entry metadata |
entries(entry_ids: [...]) for batch metadata. Avoid one GraphQL request per ID.full_text vs text deliberately: free-text keyword search needs "service": "full_text". Structured attribute filters need "service": "text". They are not interchangeable..cif for new code.paginate.rows: rows: 100 is a good default for batch work; the API may slow down beyond ~10000. Loop with paginate.start for full traversal.time.sleep(0.2) in batch loops: No published hard cap, but the public infrastructure is shared. On HTTP 429, back off exponentially.print(json.dumps(payload, indent=2)) is the cheapest way to debug HTTP 400 errors.entries(entry_ids: [...]) does not validate every ID: if one ID is wrong, the whole array returns null entries. Validate IDs separately if you can't trust the source.pythonimport requests r = requests.get("https://www.rcsb.org/fasta/entry/4HHB", timeout=30) print(r.text)
pythonimport requests DATA = "https://data.rcsb.org/graphql" gql = """{ entry(entry_id: "4HHB") { polymer_entities { rcsb_id rcsb_polymer_entity_container_identifiers { auth_asym_ids } entity_poly { rcsb_entity_polymer_type pdbx_seq_one_letter_code_can } } } }""" r = requests.post(DATA, json={"query": gql}, timeout=30) for pe in r.json()["data"]["entry"]["polymer_entities"]: chains = pe["rcsb_polymer_entity_container_identifiers"]["auth_asym_ids"] seq = pe["entity_poly"]["pdbx_seq_one_letter_code_can"][:50] print(f" {pe['rcsb_id']} chains={chains} type={pe['entity_poly']['rcsb_entity_polymer_type']} seq={seq}…")
pythonimport requests DATA = "https://data.rcsb.org/graphql" gql = """{ entry(entry_id: "1IEP") { nonpolymer_entities { rcsb_id nonpolymer_comp { chem_comp { id name formula } } } } }""" r = requests.post(DATA, json={"query": gql}, timeout=30) for npe in r.json()["data"]["entry"]["nonpolymer_entities"]: cc = npe["nonpolymer_comp"]["chem_comp"] print(f" {npe['rcsb_id']}: {cc['id']} ({cc['name']}) {cc['formula']}")
The Search API exposes a JSON schema at https://search.rcsb.org/rcsbsearch/v2/metadata/schema. Use it to look up valid attribute paths.
pythonimport requests r = requests.get("https://search.rcsb.org/rcsbsearch/v2/metadata/schema", timeout=30) schema = r.json() # Schema lists hundreds of attribute paths; sample a few sample_paths = [k for k in schema if "resolution" in k.lower()][:5] print(sample_paths)
| Problem | Cause | Solution | |---------|-------|----------| | HTTP 400 — Invalid request to the [ text ] service on a free-text query | Wrong service name | Use "service": "full_text" for keyword search; "service": "text" is for structured attribute filters | | HTTP 400 with cryptic schema message | Bad operator/value shape | Check the AttributeQuery Operators table; range needs the {from,to,include_lower,include_upper} dict | | Empty result_set | Filters too strict | Relax filters one at a time; verify attribute names via the schema endpoint | | HTTP 404 on entries(entry_ids: ["XYZW"]) | The entry doesn't exist | RCSB returns null rather than 404 inside the GraphQL response — check each data.entries[i] for null | | HTTP 429 Too Many Requests | Burst pace | Add time.sleep(0.3) between requests; exponential backoff on 429 | | HTTP 500 from search | Server-side glitch | Retry after 5–10 s; check status.rcsb.org | | Downloaded .pdb file truncated | >99,999 atoms (legacy format limit) | Download .cif instead | | GraphQL response has errors array | Field name typo or wrong root | Read the error message; the API is strict about field names — check the schema browser at <https://data.rcsb.org/index.html#graphql-api> |
rcsb-api PyPI package; this SKILL.md uses the underlying REST/GraphQL directly so no SDK install is needed.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 15,523 | 9,668 | -38% | 1 | 1 | 0% | 3,316 | 11,640 | +251% | 0 | 0 | — |
case-02 | pass→pass | 7,110 | 3,980 | -44% | 1 | 1 | 0% | 1,428 | 10,266 | +619% | 0 | 0 | — |
case-03 | fail→pass | 5,005 | 3,298 | -34% | 1 | 1 | 0% | 1,126 | 10,237 | +809% | 0 | 0 | — |
case-04 | pass→pass | 6,311 | 3,770 | -40% | 1 | 1 | 0% | 1,449 | 10,309 | +611% | 0 | 0 | — |
case-05 | pass→pass | 3,964 | 4,184 | +6% | 1 | 1 | 0% | 779 | 10,315 | +1224% | 0 | 0 | — |
case-06 | pass→pass | 13,565 | 8,538 | -37% | 1 | 1 | 0% | 2,139 | 11,035 | +416% | 0 | 0 | — |
case-07 | pass→pass | 7,869 | 4,760 | -40% | 1 | 1 | 0% | 1,574 | 10,501 | +567% | 0 | 0 | — |
case-08 | pass→pass | 7,368 | 5,081 | -31% | 1 | 1 | 0% | 1,684 | 10,572 | +528% | 0 | 0 | — |
case-09 | pass→pass | 4,395 | 3,118 | -29% | 1 | 1 | 0% | 850 | 10,053 | +1083% | 0 | 0 | — |
case-10 | pass→pass | 8,679 | 3,178 | -63% | 1 | 1 | 0% | 1,508 | 10,157 | +574% | 0 | 0 | — |
case-11 | pass→pass | 4,668 | 2,193 | -53% | 1 | 1 | 0% | 876 | 9,879 | +1028% | 0 | 0 | — |
case-12 | pass→pass | 5,062 | 3,744 | -26% | 1 | 1 | 0% | 946 | 10,240 | +982% | 0 | 0 | — |
case-13 | pass→pass | 6,044 | 3,745 | -38% | 1 | 1 | 0% | 1,121 | 10,175 | +808% | 0 | 0 | — |
case-14 | pass→pass | 14,292 | 6,758 | -53% | 1 | 1 | 0% | 2,750 | 10,752 | +291% | 0 | 0 | — |
case-15 | pass→pass | 4,932 | 2,579 | -48% | 1 | 1 | 0% | 999 | 9,949 | +896% | 0 | 0 | — |
case-16 | pass→pass | 5,086 | 2,427 | -52% | 1 | 1 | 0% | 1,094 | 9,924 | +807% | 0 | 0 | — |
case-17 | pass→pass | 13,208 | 4,154 | -69% | 1 | 1 | 0% | 2,768 | 10,306 | +272% | 0 | 0 | — |
case-18 | fail→pass | 3,832 | 2,591 | -32% | 1 | 1 | 0% | 796 | 9,962 | +1152% | 0 | 0 | — |
case-19 | fail→pass | 16,128 | 8,583 | -47% | 1 | 1 | 0% | 3,167 | 11,067 | +249% | 0 | 0 | — |
case-20 | fail→pass | 10,143 | 7,627 | -25% | 1 | 1 | 0% | 1,927 | 11,093 | +476% | 0 | 0 | — |
case-21 | pass→pass | 9,669 | 6,120 | -37% | 1 | 1 | 0% | 1,835 | 10,607 | +478% | 0 | 0 | — |
case-22 | pass→pass | 9,789 | 5,154 | -47% | 1 | 1 | 0% | 1,829 | 10,463 | +472% | 0 | 0 | — |
case-23 | pass→pass | 6,539 | 2,975 | -55% | 1 | 1 | 0% | 1,377 | 10,134 | +636% | 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. 23 cases were attempted. The headline lift of +17 percentage points is the difference between those two pass rates over the 23 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.