Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Benchling R&D Python SDK: CRUD on registry entities (DNA, RNA, proteins, custom), inventory, ELN, workflow automation. Needs Benchling account and API key. Use biopython for local sequence analysis; pubchem for chemical DBs.
.claude/skills/jaechang-hits-benchling-integration/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 90% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 117% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 121% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 222% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 291% | 0% |
Benchling is a cloud platform for life sciences R&D. The Python SDK provides programmatic access to registry entities (DNA, proteins), inventory, electronic lab notebooks, and workflows. All operations require a Benchling tenant URL and API key or OAuth credentials.
bashpip install benchling-sdk
Authentication setup: Obtain an API key from Benchling Profile Settings. Store securely in environment variables — never commit to version control.
pythonimport os from benchling_sdk.benchling import Benchling from benchling_sdk.auth.api_key_auth import ApiKeyAuth benchling = Benchling( url="https://your-tenant.benchling.com", auth_method=ApiKeyAuth(os.environ["BENCHLING_API_KEY"]) )
OAuth (for multi-user apps):
pythonfrom benchling_sdk.auth.client_credentials_oauth2 import ClientCredentialsOAuth2 benchling = Benchling( url="https://your-tenant.benchling.com", auth_method=ClientCredentialsOAuth2( client_id=os.environ["BENCHLING_CLIENT_ID"], client_secret=os.environ["BENCHLING_CLIENT_SECRET"] ) )
API rate limits: Benchling enforces per-tenant rate limits. The SDK automatically retries on 429 responses with exponential backoff (up to 5 retries by default). For bulk operations, add time.sleep(0.5) between batches.
pythonfrom benchling_sdk.benchling import Benchling from benchling_sdk.auth.api_key_auth import ApiKeyAuth from benchling_sdk.models import DnaSequenceCreate import os benchling = Benchling( url="https://your-tenant.benchling.com", auth_method=ApiKeyAuth(os.environ["BENCHLING_API_KEY"]) ) # Create a DNA sequence seq = benchling.dna_sequences.create( DnaSequenceCreate(name="GFP-insert", bases="ATGGTGAGCAAGGGC", is_circular=False, folder_id="fld_abc123") ) print(f"Created: {seq.name} ({seq.id})")
Registry entities include DNA sequences, RNA sequences, AA sequences, custom entities, and mixtures. All entity types follow the same create/read/update/archive pattern.
pythonfrom benchling_sdk.models import DnaSequenceCreate, DnaSequenceUpdate # Create sequence = benchling.dna_sequences.create( DnaSequenceCreate( name="My Plasmid", bases="ATCGATCG", is_circular=True, folder_id="fld_abc123", schema_id="ts_abc123", fields=benchling.models.fields({"gene_name": "GFP"}) ) ) print(f"Created: {sequence.id}") # Read seq = benchling.dna_sequences.get_by_id(sequence.id) print(f"Name: {seq.name}, Length: {len(seq.bases)} bp") # Update (partial — unspecified fields unchanged) updated = benchling.dna_sequences.update( sequence_id=sequence.id, dna_sequence=DnaSequenceUpdate( name="Updated Plasmid", fields=benchling.models.fields({"gene_name": "mCherry"}) ) ) # Archive benchling.dna_sequences.archive(ids=[sequence.id], reason="RETIRED")
python# Register entity in registry (with auto-generated ID) registered = benchling.dna_sequences.create( DnaSequenceCreate( name="Production Plasmid", bases="ATCGATCG", is_circular=True, folder_id="fld_abc123", entity_registry_id="src_abc123", naming_strategy="NEW_IDS" # or "IDS_FROM_NAMES" ) ) print(f"Registry ID: {registered.entity_registry_id}") # Entity types available via SDK: # benchling.dna_sequences, benchling.rna_sequences, # benchling.aa_sequences, benchling.custom_entities, benchling.mixtures
All list operations return paginated generators for memory efficiency.
python# List with pagination sequences = benchling.dna_sequences.list() total = sequences.estimated_count() print(f"Total sequences: {total}") for page in sequences: for seq in page: print(f" {seq.name} ({seq.id}): {len(seq.bases)} bp") # Filter by schema filtered = benchling.dna_sequences.list(schema_id="ts_abc123") for page in filtered: for seq in page: print(f" {seq.name}")
Manage physical samples, containers, boxes, and locations.
pythonfrom benchling_sdk.models import ContainerCreate, BoxCreate # Create container (sample tube) container = benchling.containers.create( ContainerCreate( name="Sample Tube 001", schema_id="cont_schema_abc123", parent_storage_id="box_abc123", fields=benchling.models.fields({"concentration": "100 ng/uL"}) ) ) print(f"Container: {container.id}, Barcode: {container.barcode}") # Create box box = benchling.boxes.create( BoxCreate( name="Freezer Box A1", schema_id="box_schema_abc123", parent_storage_id="loc_abc123" ) ) # Transfer container to new location benchling.containers.transfer( container_id=container.id, destination_id="box_xyz789" ) print(f"Transferred {container.name} to new box")
Create and manage electronic lab notebook entries.
pythonfrom benchling_sdk.models import EntryCreate # Create notebook entry entry = benchling.entries.create( EntryCreate( name="Experiment 2026-02-17", folder_id="fld_abc123", schema_id="entry_schema_abc123", fields=benchling.models.fields({ "objective": "Test gene expression levels", "protocol": "Standard qPCR" }) ) ) print(f"Entry: {entry.id}") # Link entity to entry benchling.entry_links.create( entry_id=entry.id, entity_id="seq_xyz789" )
Create and manage workflow tasks for lab process automation.
pythonfrom benchling_sdk.models import WorkflowTaskCreate, WorkflowTaskUpdate # Create workflow task task = benchling.workflow_tasks.create( WorkflowTaskCreate( name="PCR Amplification", workflow_id="wf_abc123", assignee_id="user_abc123", fields=benchling.models.fields({"template": "seq_abc123"}) ) ) print(f"Task: {task.id}, Status: {task.status}") # Update task status benchling.workflow_tasks.update( task_id=task.id, workflow_task=WorkflowTaskUpdate(status_id="status_complete_abc123") ) # Wait for async operations from benchling_sdk.helpers.tasks import wait_for_task result = wait_for_task( benchling, task_id="task_abc123", interval_wait_seconds=2, max_wait_seconds=300 ) print(f"Async task completed: {result}")
pythonfrom benchling_sdk.retry import RetryStrategy from benchling_sdk.errors import BenchlingError # Custom retry strategy benchling = Benchling( url="https://your-tenant.benchling.com", auth_method=ApiKeyAuth(os.environ["BENCHLING_API_KEY"]), retry_strategy=RetryStrategy(max_retries=3) ) # SDK auto-retries on 429 (rate limit), 502, 503, 504 # Error handling try: seq = benchling.dna_sequences.get_by_id("seq_nonexistent") except BenchlingError as e: print(f"API error: {e.status_code} — {e.message}")
| Benchling Type | SDK Accessor | Use Case | |---------------|-------------|----------| | DNA Sequence | benchling.dna_sequences | Plasmids, primers, gene inserts | | RNA Sequence | benchling.rna_sequences | mRNA, gRNA, siRNA | | AA Sequence | benchling.aa_sequences | Proteins, antibodies, enzymes | | Custom Entity | benchling.custom_entities | Cell lines, reagents, samples | | Mixture | benchling.mixtures | Buffers, media, compound formulations | | Container | benchling.containers | Tubes, wells, vials | | Box | benchling.boxes | Storage boxes, racks | | Entry | benchling.entries | Lab notebook entries | | Workflow Task | benchling.workflow_tasks | Process steps, assignments |
Benchling entities use schema-defined custom fields. Always use the fields() helper:
python# Correct: use fields() helper fields = benchling.models.fields({ "concentration": "100 ng/uL", "date_prepared": "2026-02-17", "passage_number": 5 }) # Fields are typed by schema — string, number, date, entity link, dropdown
All list() calls return paginated generators. Never call list() without iterating:
python# Correct: iterate through pages for page in benchling.dna_sequences.list(): for item in page: process(item) # Get count without loading all data count = benchling.dna_sequences.list().estimated_count()
pythonimport os, time from Bio import SeqIO from benchling_sdk.benchling import Benchling from benchling_sdk.auth.api_key_auth import ApiKeyAuth from benchling_sdk.models import DnaSequenceCreate benchling = Benchling( url="https://your-tenant.benchling.com", auth_method=ApiKeyAuth(os.environ["BENCHLING_API_KEY"]) ) created = [] for record in SeqIO.parse("sequences.fasta", "fasta"): seq = benchling.dna_sequences.create( DnaSequenceCreate( name=record.id, bases=str(record.seq), is_circular=False, folder_id="fld_abc123", fields=benchling.models.fields({ "description": record.description, "source": "FASTA import" }) ) ) created.append(seq.id) time.sleep(0.5) # Rate limit compliance print(f"Created: {record.id} -> {seq.id}") print(f"Imported {len(created)} sequences")
pythonimport os, csv from benchling_sdk.benchling import Benchling from benchling_sdk.auth.api_key_auth import ApiKeyAuth benchling = Benchling( url="https://your-tenant.benchling.com", auth_method=ApiKeyAuth(os.environ["BENCHLING_API_KEY"]) ) audit = [] containers = benchling.containers.list(parent_storage_id="loc_freezer01") for page in containers: for c in page: audit.append({ "id": c.id, "name": c.name, "barcode": c.barcode, "location": c.parent_storage_id, "created": str(c.created_at) }) with open("inventory_audit.csv", "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=audit[0].keys()) writer.writeheader() writer.writerows(audit) print(f"Audit complete: {len(audit)} containers")
benchling.workflow_tasks.list(workflow_id=..., status="pending")benchling.dna_sequences.get_by_id()benchling.workflow_tasks.update()benchling.entries.create()| Parameter | Function/Endpoint | Default | Options | Effect | |-----------|------------------|---------|---------|--------| | folder_id | All create operations | Required | fld_... | Target folder for new entity | | schema_id | All create operations | Optional | ts_..., cont_... | Schema defining custom fields | | entity_registry_id | Entity registration | Optional | src_... | Registry to register entity in | | naming_strategy | Entity registration | — | NEW_IDS, IDS_FROM_NAMES | How registry IDs are generated | | parent_storage_id | Containers, boxes | Optional | box_..., loc_... | Storage location for inventory | | max_retries | RetryStrategy | 5 | 0–10 | Number of retry attempts on failure | | interval_wait_seconds | wait_for_task | 2 | 1–60 | Polling interval for async tasks | | max_wait_seconds | wait_for_task | 300 | 10–3600 | Maximum wait for async completion |
os.environ["BENCHLING_API_KEY"].fields() helper for custom schema fields: Raw dicts will not work — the SDK requires typed Fields objects.list() to a Python list for large datasets.time.sleep(0.5) between create/update calls when processing >50 entities.entity_registry_id and naming_strategy: These are mutually exclusive on create. Use one or the other.BenchlingError explicitly: Catch SDK exceptions and log the status code and message for debugging.pythonimport csv export = [] for page in benchling.dna_sequences.list(schema_id="ts_target_schema"): for seq in page: export.append({ "registry_id": seq.entity_registry_id, "name": seq.name, "length": len(seq.bases), "bases": seq.bases[:50] + "..." if len(seq.bases) > 50 else seq.bases }) with open("sequences_export.csv", "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=export[0].keys()) writer.writeheader() writer.writerows(export) print(f"Exported {len(export)} sequences")
python# Search entities with specific field values # Note: SDK list() supports limited filtering; for complex queries use Data Warehouse results = [] for page in benchling.custom_entities.list(schema_id="ts_cell_lines"): for entity in page: fields = entity.fields or {} if fields.get("organism", {}).get("value") == "Human": results.append(entity) print(f"Found: {entity.name} ({entity.id})") print(f"Total human cell lines: {len(results)}")
pythonimport time from datetime import datetime, timedelta cutoff = datetime.now() - timedelta(days=365) to_archive = [] for page in benchling.custom_entities.list(): for entity in page: if entity.modified_at and entity.modified_at < cutoff: to_archive.append(entity.id) # Archive in batches batch_size = 50 for i in range(0, len(to_archive), batch_size): batch = to_archive[i:i+batch_size] benchling.custom_entities.archive(ids=batch, reason="RETIRED") print(f"Archived batch {i//batch_size + 1}: {len(batch)} entities") time.sleep(1) print(f"Total archived: {len(to_archive)}")
| Problem | Cause | Solution | |---------|-------|----------| | 401 Unauthorized | Invalid or expired API key | Regenerate key in Benchling Profile Settings; check env var is set | | 403 Forbidden | Insufficient permissions | API key inherits user permissions; check user role in Benchling admin | | 404 Not Found | Wrong entity ID or tenant URL | Verify ID format (seq_, fld_, etc.); check tenant URL matches | | 429 Too Many Requests | Rate limit exceeded | SDK auto-retries; add time.sleep() between bulk operations | | fields ignored on create | Using raw dict instead of fields() helper | Use benchling.models.fields({...}) for custom schema fields | | naming_strategy error | Used with entity_registry_id | These are mutually exclusive — use one or the other | | Pagination memory issues | Collecting all items into a list | Iterate page-by-page with for page in .list() pattern | | OAuth token expired | Client credentials not refreshing | SDK handles refresh automatically; check client_id/secret are valid |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 19,856 | 19,057 | -4% | 1 | 1 | 0% | 3,651 | 6,937 | +90% | 0 | 0 | — |
case-02 | fail→pass | 18,063 | 11,847 | -34% | 1 | 1 | 0% | 3,348 | 7,251 | +117% | 0 | 0 | — |
case-03 | fail→pass | 18,832 | 18,877 | +0% | 1 | 1 | 0% | 3,881 | 8,593 | +121% | 0 | 0 | — |
case-04 | pass→pass | 14,879 | 11,474 | -23% | 1 | 1 | 0% | 2,718 | 6,875 | +153% | 0 | 0 | — |
case-05 | pass→pass | 14,629 | 10,935 | -25% | 1 | 1 | 0% | 2,787 | 7,081 | +154% | 0 | 0 | — |
case-06 | pass→pass | 10,151 | 23,592 | +132% | 1 | 1 | 0% | 2,058 | 6,967 | +239% | 0 | 0 | — |
case-07 | fail→pass | 9,229 | 4,759 | -48% | 1 | 1 | 0% | 1,811 | 5,834 | +222% | 0 | 0 | — |
case-08 | pass→pass | 10,230 | 5,413 | -47% | 1 | 1 | 0% | 1,925 | 5,887 | +206% | 0 | 0 | — |
case-09 | pass→pass | 9,094 | 5,052 | -44% | 1 | 1 | 0% | 1,802 | 5,809 | +222% | 0 | 0 | — |
case-10 | fail→pass | 7,332 | 5,329 | -27% | 1 | 1 | 0% | 1,529 | 5,975 | +291% | 0 | 0 | — |
case-15 | pass→pass | 16,231 | 7,603 | -53% | 1 | 1 | 0% | 3,107 | 6,364 | +105% | 0 | 0 | — |
case-11 | fail→pass | 14,670 | 5,869 | -60% | 1 | 1 | 0% | 2,741 | 6,102 | +123% | 0 | 0 | — |
case-12 | fail→pass | 11,757 | 5,672 | -52% | 1 | 1 | 0% | 2,183 | 6,016 | +176% | 0 | 0 | — |
case-13 | pass→pass | 11,541 | 7,513 | -35% | 1 | 1 | 0% | 2,424 | 6,405 | +164% | 0 | 0 | — |
case-14 | fail→pass | 6,564 | 4,040 | -38% | 1 | 1 | 0% | 1,332 | 5,655 | +325% | 0 | 0 | — |
case-16 | fail→pass | 23,641 | 3,036 | -87% | 1 | 1 | 0% | 3,949 | 5,345 | +35% | 0 | 0 | — |
case-17 | fail→pass | 5,511 | 4,904 | -11% | 1 | 1 | 0% | 1,038 | 5,814 | +460% | 0 | 0 | — |
case-18 | pass→pass | 6,004 | 4,720 | -21% | 1 | 1 | 0% | 1,235 | 5,795 | +369% | 0 | 0 | — |
case-19 | pass→pass | 5,246 | 4,030 | -23% | 1 | 1 | 0% | 959 | 5,559 | +480% | 0 | 0 | — |
case-20 | pass→pass | 4,993 | 2,814 | -44% | 1 | 1 | 0% | 960 | 5,368 | +459% | 0 | 0 | — |
case-21 | pass→pass | 6,034 | 3,488 | -42% | 1 | 1 | 0% | 1,243 | 5,519 | +344% | 0 | 0 | — |
case-22 | pass→pass | 15,548 | 9,183 | -41% | 1 | 1 | 0% | 2,866 | 6,643 | +132% | 0 | 0 | — |
case-23 | pass→pass | 9,384 | 4,385 | -53% | 1 | 1 | 0% | 1,799 | 5,710 | +217% | 0 | 0 | — |
case-24 | pass→pass | 6,638 | 4,716 | -29% | 1 | 1 | 0% | 1,331 | 5,732 | +331% | 0 | 0 | — |
case-25 | fail→pass | 13,355 | 7,029 | -47% | 1 | 1 | 0% | 2,701 | 6,346 | +135% | 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. 25 cases were attempted. The headline lift of +44 percentage points is the difference between those two pass rates over the 25 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.