Skill Tracer
Skills › Scientific Agent Skills › benchling-integration › sdk_reference.md

sdk_reference.md

329 lines · skills/benchling-integration/references/sdk_reference.md at 5f2bc03

Open on GitHub
1# Benchling Python SDK Reference
2
3Targets **1.25.0**, the stable release verified on 2026-09-30. Python requirement:
4`>=3.9,<4`. Install with `uv pip install "benchling-sdk==1.25.0"`.
5Examples assume an initialized `benchling` client from [authentication](authentication.md).
6All IDs and schema fields are placeholders. Model serialization and request construction
7were tested offline; server behavior remains illustrative until checked in your tenant.
8
9Source of truth: [versioned SDK reference](https://benchling.com/sdk-docs/1.25.0/index.html)
10and the released `benchling-sdk` / `benchling-api-client` Python packages. The
11[interaction guide](https://docs.benchling.com/docs/common-sdk-interactions-and-examples)
12explains the patterns; use installed signatures when guide snippets differ.

Benchling Python SDK Reference

Targets 1.25.0, the stable release verified on 2026-09-30. Python requirement: >=3.9,<4. Install with uv pip install "benchling-sdk==1.25.0". Examples assume an initialized benchling client from authentication. All IDs and schema fields are placeholders. Model serialization and request construction were tested offline; server behavior remains illustrative until checked in your tenant.

Source of truth: versioned SDK reference and the released benchling-sdk / benchling-api-client Python packages. The interaction guide explains the patterns; use installed signatures when guide snippets differ.

Raw lines 1 to 13
14## Schemas and fields
16```python
17from benchling_sdk.helpers.serialization_helpers import fields
19custom_fields = fields({
20 "concentration": {"value": 100.0},
21 "notes": {"value": "QC passed"},
22})
23schema = benchling.schemas.get_entity_schema_by_id("ts_example")
24```
26Use actual field names, numeric types, units, multiplicity, and dropdown **option IDs**
27from the schema. `fields()` is a serializer, not schema validation. A field named
28`concentration` may be a custom number; it is not the inventory contents concentration.
29Omitting a field preserves it on a partial update; `{"value": None}` explicitly clears it
30when allowed. Do not confuse `fields` with the free-text `custom_fields` property.

Schemas and fields

from benchling_sdk.helpers.serialization_helpers import fields

custom_fields = fields({
    "concentration": {"value": 100.0},
    "notes": {"value": "QC passed"},
})
schema = benchling.schemas.get_entity_schema_by_id("ts_example")

Use actual field names, numeric types, units, multiplicity, and dropdown option IDs from the schema. fields() is a serializer, not schema validation. A field named concentration may be a custom number; it is not the inventory contents concentration. Omitting a field preserves it on a partial update; {"value": None} explicitly clears it when allowed. Do not confuse fields with the free-text custom_fields property.

Raw lines 14 to 31
32## Registry entities

Registry entities

Raw lines 32 to 33
34### DNA: create, register, read, update, archive
36```python
37from benchling_sdk.models import (
38 DnaSequenceCreate, DnaSequenceUpdate, EntityArchiveReason, NamingStrategy,
40from benchling_sdk.helpers.serialization_helpers import fields
42payload = DnaSequenceCreate(
43 name="Construct-001",
44 bases="ATCGATCG",
45 is_circular=True,
46 folder_id="lib_example",
47 schema_id="ts_example",
48 registry_id="src_example",
49 naming_strategy=NamingStrategy.NEW_IDS,
50 fields=fields({"gene_name": {"value": "GFP"}}),
52sequence = benchling.dna_sequences.create(payload)
53print(sequence.id, sequence.entity_registry_id)
54sequence = benchling.dna_sequences.get_by_id(sequence.id)
55updated = benchling.dna_sequences.update(
56 dna_sequence_id=sequence.id,
57 dna_sequence=DnaSequenceUpdate(
58 fields=fields({"gene_name": {"value": "mCherry"}}),
59 ),
61```
63For an unregistered entity omit `registry_id` and the naming arguments. For an explicit
64human registry ID, replace `naming_strategy` with `entity_registry_id="CONSTRUCT001"`.
65These are mutually exclusive. Required schema fields are enforced during registration.
66`sequence.id` is the API ID; `sequence.entity_registry_id` is the human registry ID.
68Archival is a separate requested operation, with an iterable of IDs and an enum:
70```python
71benchling.dna_sequences.archive(
72 dna_sequence_ids=["seq_example"],
73 reason=EntityArchiveReason.RETIRED,
75```

DNA: create, register, read, update, archive

from benchling_sdk.models import (
    DnaSequenceCreate, DnaSequenceUpdate, EntityArchiveReason, NamingStrategy,
)
from benchling_sdk.helpers.serialization_helpers import fields

payload = DnaSequenceCreate(
    name="Construct-001",
    bases="ATCGATCG",
    is_circular=True,
    folder_id="lib_example",
    schema_id="ts_example",
    registry_id="src_example",
    naming_strategy=NamingStrategy.NEW_IDS,
    fields=fields({"gene_name": {"value": "GFP"}}),
)
sequence = benchling.dna_sequences.create(payload)
print(sequence.id, sequence.entity_registry_id)
sequence = benchling.dna_sequences.get_by_id(sequence.id)
updated = benchling.dna_sequences.update(
    dna_sequence_id=sequence.id,
    dna_sequence=DnaSequenceUpdate(
        fields=fields({"gene_name": {"value": "mCherry"}}),
    ),
)

For an unregistered entity omit registry_id and the naming arguments. For an explicit human registry ID, replace naming_strategy with entity_registry_id="CONSTRUCT001". These are mutually exclusive. Required schema fields are enforced during registration. sequence.id is the API ID; sequence.entity_registry_id is the human registry ID.

Archival is a separate requested operation, with an iterable of IDs and an enum:

benchling.dna_sequences.archive(
    dna_sequence_ids=["seq_example"],
    reason=EntityArchiveReason.RETIRED,
)
Raw lines 34 to 76
77### RNA, proteins, and custom entities
79```python
80from benchling_sdk.models import (
81 RnaSequenceCreate, RnaSequenceUpdate, AaSequenceCreate,
82 CustomEntityCreate, CustomEntityUpdate,
85rna = benchling.rna_sequences.create(RnaSequenceCreate(
86 name="gRNA-001", bases="AUCGAUCG", is_circular=False,
87 folder_id="lib_example",
88))
89benchling.rna_sequences.update(
90 rna_sequence_id=rna.id,
91 rna_sequence=RnaSequenceUpdate(name="gRNA-001-validated"),
93protein = benchling.aa_sequences.create(AaSequenceCreate(
94 name="Peptide-001", amino_acids="MSKGEELFTGVVPIL",
95 folder_id="lib_example",
96))
97cell_line = benchling.custom_entities.create(CustomEntityCreate(
98 name="HEK293T-Clone5", schema_id="ts_cellline", folder_id="lib_example",
99 fields=fields({"passage_number": {"value": 15}}),
100))
101benchling.custom_entities.update(
102 entity_id=cell_line.id,
103 entity=CustomEntityUpdate(fields=fields({"passage_number": {"value": 16}})),
105```

RNA, proteins, and custom entities

from benchling_sdk.models import (
    RnaSequenceCreate, RnaSequenceUpdate, AaSequenceCreate,
    CustomEntityCreate, CustomEntityUpdate,
)

rna = benchling.rna_sequences.create(RnaSequenceCreate(
    name="gRNA-001", bases="AUCGAUCG", is_circular=False,
    folder_id="lib_example",
))
benchling.rna_sequences.update(
    rna_sequence_id=rna.id,
    rna_sequence=RnaSequenceUpdate(name="gRNA-001-validated"),
)
protein = benchling.aa_sequences.create(AaSequenceCreate(
    name="Peptide-001", amino_acids="MSKGEELFTGVVPIL",
    folder_id="lib_example",
))
cell_line = benchling.custom_entities.create(CustomEntityCreate(
    name="HEK293T-Clone5", schema_id="ts_cellline", folder_id="lib_example",
    fields=fields({"passage_number": {"value": 15}}),
))
benchling.custom_entities.update(
    entity_id=cell_line.id,
    entity=CustomEntityUpdate(fields=fields({"passage_number": {"value": 16}})),
)
Raw lines 77 to 106
107### Mixtures
109`IngredientWriteParams` uses separate amount and units, with explicit nullable provenance
110fields. There is no `IngredientCreate` in this release.
112```python
113from benchling_sdk.models import MixtureCreate, IngredientWriteParams, IngredientMeasurementUnits
115ingredient = IngredientWriteParams(
116 component_entity_id="bfi_component",
117 amount="100", units=IngredientMeasurementUnits.MG,
118 catalog_identifier=None, component_lot_container_id=None,
119 component_lot_entity_id=None, component_lot_text=None, notes=None,
121mixture = benchling.mixtures.create(MixtureCreate(
122 name="Example formulation", schema_id="ts_mixture", folder_id="lib_example",
123 ingredients=[ingredient],
124))
125```
127Resolve ingredient identity/lot and measurement dimensions before transfer or formulation;
128serialization does not establish material balance or concentration correctness.

Mixtures

IngredientWriteParams uses separate amount and units, with explicit nullable provenance fields. There is no IngredientCreate in this release.

from benchling_sdk.models import MixtureCreate, IngredientWriteParams, IngredientMeasurementUnits

ingredient = IngredientWriteParams(
    component_entity_id="bfi_component",
    amount="100", units=IngredientMeasurementUnits.MG,
    catalog_identifier=None, component_lot_container_id=None,
    component_lot_entity_id=None, component_lot_text=None, notes=None,
)
mixture = benchling.mixtures.create(MixtureCreate(
    name="Example formulation", schema_id="ts_mixture", folder_id="lib_example",
    ingredients=[ingredient],
))

Resolve ingredient identity/lot and measurement dimensions before transfer or formulation; serialization does not establish material balance or concentration correctness.

Raw lines 107 to 129
130## Inventory
132### Storage and physical movement
134```python
135from benchling_sdk.models import ContainerCreate, ContainerUpdate, BoxCreate, LocationCreate
137location = benchling.locations.create(LocationCreate(
138 name="Freezer A - Shelf 2", schema_id="locsch_example",
139 parent_storage_id="loc_freezer",
140))
141box = benchling.boxes.create(BoxCreate(
142 name="Box 01", schema_id="boxsch_example", parent_storage_id=location.id,
143))
144container = benchling.containers.create(ContainerCreate(
145 name="Sample 001", schema_id="consch_example", parent_storage_id=box.id,
146))
147moved = benchling.containers.update(
148 container_id=container.id,
149 container=ContainerUpdate(parent_storage_id="loc_destination"),
151for page in benchling.containers.list(ancestor_storage_id=box.id):
152 for item in page:
153 print(item.id, item.name)
154```
156When placing a container into a box, resolve the supported box position identifier from
157the box contents/reference; don't guess coordinates or assume every box has free space.

Storage and physical movement

from benchling_sdk.models import ContainerCreate, ContainerUpdate, BoxCreate, LocationCreate

location = benchling.locations.create(LocationCreate(
    name="Freezer A - Shelf 2", schema_id="locsch_example",
    parent_storage_id="loc_freezer",
))
box = benchling.boxes.create(BoxCreate(
    name="Box 01", schema_id="boxsch_example", parent_storage_id=location.id,
))
container = benchling.containers.create(ContainerCreate(
    name="Sample 001", schema_id="consch_example", parent_storage_id=box.id,
))
moved = benchling.containers.update(
    container_id=container.id,
    container=ContainerUpdate(parent_storage_id="loc_destination"),
)
for page in benchling.containers.list(ancestor_storage_id=box.id):
    for item in page:
        print(item.id, item.name)

When placing a container into a box, resolve the supported box position identifier from the box contents/reference; don't guess coordinates or assume every box has free space.

Raw lines 132 to 158
159### Check out and in
161```python
162from benchling_sdk.models import ContainersCheckout, ContainersCheckin
164benchling.containers.checkout(ContainersCheckout(
165 assignee_id="usr_example", container_ids=["con_example"], comment="At bench",
166))
167benchling.containers.checkin(ContainersCheckin(
168 container_ids=["con_example"], comments="Returned",
169))
170```
172Check-in does not take a `location_id`. Change physical storage separately if needed.

Check out and in

from benchling_sdk.models import ContainersCheckout, ContainersCheckin

benchling.containers.checkout(ContainersCheckout(
    assignee_id="usr_example", container_ids=["con_example"], comment="At bench",
))
benchling.containers.checkin(ContainersCheckin(
    container_ids=["con_example"], comments="Returned",
))

Check-in does not take a location_id. Change physical storage separately if needed.

Raw lines 159 to 173
174### Plates and material transfer
176```python
177from benchling_sdk.models import PlateCreate
179plate = benchling.plates.create(PlateCreate(
180 name="PCR plate", schema_id="pltsch_example", barcode="PLATE001",
181))
182```
184`PlateCreate.wells`, when used, is a `PlateCreateWells` mapping of positions to well
185creation properties (for example barcodes), not a list of `WellCreate` entity assignments.
186Read back well/container IDs, then transfer contents with the container service.
188```python
189from benchling_sdk.models import MultipleContainersTransfer, ContainerQuantity, ContainerQuantityUnits
191transfer_task = benchling.containers.transfer_into_containers([
192 MultipleContainersTransfer(
193 source_container_id="con_source",
194 destination_container_id="con_destination",
195 transfer_quantity=ContainerQuantity(units=ContainerQuantityUnits.UL, value=5.0),
196 ),
197])
198completion = transfer_task.wait_for_completion(max_wait_seconds=60)
199if not completion.success:
200 raise RuntimeError("Benchling material transfer failed; inspect completion.errors")
201```
203This calls `POST /api/v2/transfers` and returns a `TaskHelper`. Check `completion.success` and
204`completion.errors` before consuming `completion.response`. Single-destination transfer uses
205`transfer_into_container(destination_container_id, ContainerTransfer(...))`, with
206`destination_contents`; it is a different payload. Neither method moves a tube to a box.

Plates and material transfer

from benchling_sdk.models import PlateCreate

plate = benchling.plates.create(PlateCreate(
    name="PCR plate", schema_id="pltsch_example", barcode="PLATE001",
))

PlateCreate.wells, when used, is a PlateCreateWells mapping of positions to well creation properties (for example barcodes), not a list of WellCreate entity assignments. Read back well/container IDs, then transfer contents with the container service.

from benchling_sdk.models import MultipleContainersTransfer, ContainerQuantity, ContainerQuantityUnits

transfer_task = benchling.containers.transfer_into_containers([
    MultipleContainersTransfer(
        source_container_id="con_source",
        destination_container_id="con_destination",
        transfer_quantity=ContainerQuantity(units=ContainerQuantityUnits.UL, value=5.0),
    ),
])
completion = transfer_task.wait_for_completion(max_wait_seconds=60)
if not completion.success:
    raise RuntimeError("Benchling material transfer failed; inspect completion.errors")

This calls POST /api/v2/transfers and returns a TaskHelper. Check completion.success and completion.errors before consuming completion.response. Single-destination transfer uses transfer_into_container(destination_container_id, ContainerTransfer(...)), with destination_contents; it is a different payload. Neither method moves a tube to a box.

Raw lines 174 to 207
208## Notebook
210```python
211from benchling_sdk.models import EntryCreate, EntryUpdate
213entry = benchling.entries.create_entry(EntryCreate(
214 name="Cloning experiment", folder_id="lib_example",
215))
216entry = benchling.entries.get_entry_by_id(entry.id)
217updated_entry = benchling.entries.update_entry(
218 entry_id=entry.id, entry=EntryUpdate(name="Cloning experiment - reviewed"),
220for page in benchling.entries.list_entries(project_id="src_project", page_size=10):
221 for item in page:
222 print(item.id, item.name)
223 break
224```
226Use `entry_template_id` and `initial_tables` for supported template-based creation.
227`EntryUpdate` updates metadata/schema fields; it is not arbitrary rich-text editing.
228There is no `benchling.entry_links` service in 1.25.0. Use supported schema link fields
229or template/table mechanisms for the intended relationship. Discover actual template
230requirements with `entries.get_entry_template_by_id` and `entries.list_entry_templates`.

Notebook

from benchling_sdk.models import EntryCreate, EntryUpdate

entry = benchling.entries.create_entry(EntryCreate(
    name="Cloning experiment", folder_id="lib_example",
))
entry = benchling.entries.get_entry_by_id(entry.id)
updated_entry = benchling.entries.update_entry(
    entry_id=entry.id, entry=EntryUpdate(name="Cloning experiment - reviewed"),
)
for page in benchling.entries.list_entries(project_id="src_project", page_size=10):
    for item in page:
        print(item.id, item.name)
    break

Use entry_template_id and initial_tables for supported template-based creation. EntryUpdate updates metadata/schema fields; it is not arbitrary rich-text editing. There is no benchling.entry_links service in 1.25.0. Use supported schema link fields or template/table mechanisms for the intended relationship. Discover actual template requirements with entries.get_entry_template_by_id and entries.list_entry_templates.

Raw lines 208 to 231
232## Workflow tasks versus async tasks
234```python
235from benchling_sdk.models import WorkflowTaskCreate, WorkflowTaskUpdate
237task = benchling.workflow_tasks.create(WorkflowTaskCreate(
238 workflow_task_group_id="wtg_example", assignee_id="usr_example",
239))
240updated_task = benchling.workflow_tasks.update(
241 workflow_task_id=task.id,
242 workflow_task=WorkflowTaskUpdate(status_id="wts_complete"),
244for page in benchling.workflow_tasks.list(
245 workflow_task_group_ids=["wtg_example"], status_ids=["wts_pending"],
246):
247 for item in page:
248 print(item.id)
249```
251Resolve status IDs from the workflow task schema. There is no `workflow_id` filter or
252`name`/`schema_id` constructor field on `WorkflowTaskCreate` in this SDK. The task group
253provides workflow context. Validate scientific completion before changing a status.
255Async server jobs are a separate service and endpoint:
257```python
258from benchling_sdk.models import AsyncTaskStatus
259from benchling_sdk.errors import WaitForTaskExpiredError
261try:
262 finished = benchling.tasks.wait_for_task(
263 task_id="task_example", interval_wait_seconds=2, max_wait_seconds=60,
264 )
265 if finished.status != AsyncTaskStatus.SUCCEEDED:
266 raise RuntimeError(f"Benchling job ended with {finished.status}")
267except WaitForTaskExpiredError:
268 # The server job can continue after the local timeout. Save its ID for later polling.
269 print("[FAIL] Polling deadline reached; inspect task_example before resubmission")
270```
272The default polling deadline is 600 seconds. `wait_for_task` returns on terminal failure
273as well as success; do not treat return alone as a successful operation.

Workflow tasks versus async tasks

from benchling_sdk.models import WorkflowTaskCreate, WorkflowTaskUpdate

task = benchling.workflow_tasks.create(WorkflowTaskCreate(
    workflow_task_group_id="wtg_example", assignee_id="usr_example",
))
updated_task = benchling.workflow_tasks.update(
    workflow_task_id=task.id,
    workflow_task=WorkflowTaskUpdate(status_id="wts_complete"),
)
for page in benchling.workflow_tasks.list(
    workflow_task_group_ids=["wtg_example"], status_ids=["wts_pending"],
):
    for item in page:
        print(item.id)

Resolve status IDs from the workflow task schema. There is no workflow_id filter or name/schema_id constructor field on WorkflowTaskCreate in this SDK. The task group provides workflow context. Validate scientific completion before changing a status.

Async server jobs are a separate service and endpoint:

from benchling_sdk.models import AsyncTaskStatus
from benchling_sdk.errors import WaitForTaskExpiredError

try:
    finished = benchling.tasks.wait_for_task(
        task_id="task_example", interval_wait_seconds=2, max_wait_seconds=60,
    )
    if finished.status != AsyncTaskStatus.SUCCEEDED:
        raise RuntimeError(f"Benchling job ended with {finished.status}")
except WaitForTaskExpiredError:
    # The server job can continue after the local timeout. Save its ID for later polling.
    print("[FAIL] Polling deadline reached; inspect task_example before resubmission")

The default polling deadline is 600 seconds. wait_for_task returns on terminal failure as well as success; do not treat return alone as a successful operation.

Raw lines 232 to 274
275## Pagination, errors, and retries
277```python
278sequences = benchling.dna_sequences.list(schema_id="ts_example", page_size=50)
279try:
280 total = sequences.estimated_count
281except NotImplementedError:
282 total = None
283for page in sequences:
284 for seq in page:
285 print(seq.id, seq.name)
286```
288The iterator is consumable once; create another for another pass. Count access may fetch
289a page and counts are approximate. `returning` can omit model attributes; do not access
290an unrequested property as if it were a complete resource.
292```python
293from benchling_sdk.errors import BenchlingError
295try:
296 sequence = benchling.dna_sequences.get_by_id("seq_example")
297except BenchlingError as error:
298 print(f"[FAIL] Benchling HTTP status {error.status_code}")
299 raise
300```
302`BenchlingError` covers HTTP failures; transport and parsing errors can still surface
303separately. Avoid logging request headers or sensitive response bodies.
305```python
306from benchling_sdk.helpers.retry_helpers import RetryStrategy
308retry_strategy = RetryStrategy(max_tries=3, backoff_factor=1.0)
309```
311Pass this to `Benchling(..., retry_strategy=retry_strategy)`. Default `max_tries=5`
312counts total attempts; default retry codes are 429, 502, 503, and 504. Set
313`retry_strategy=None` to disable. Retrying a write after an ambiguous failure requires
314reconciliation; do not assume every create is idempotent.

Pagination, errors, and retries

sequences = benchling.dna_sequences.list(schema_id="ts_example", page_size=50)
try:
    total = sequences.estimated_count
except NotImplementedError:
    total = None
for page in sequences:
    for seq in page:
        print(seq.id, seq.name)

The iterator is consumable once; create another for another pass. Count access may fetch a page and counts are approximate. returning can omit model attributes; do not access an unrequested property as if it were a complete resource.

from benchling_sdk.errors import BenchlingError

try:
    sequence = benchling.dna_sequences.get_by_id("seq_example")
except BenchlingError as error:
    print(f"[FAIL] Benchling HTTP status {error.status_code}")
    raise

BenchlingError covers HTTP failures; transport and parsing errors can still surface separately. Avoid logging request headers or sensitive response bodies.

from benchling_sdk.helpers.retry_helpers import RetryStrategy

retry_strategy = RetryStrategy(max_tries=3, backoff_factor=1.0)

Pass this to Benchling(..., retry_strategy=retry_strategy). Default max_tries=5 counts total attempts; default retry codes are 429, 502, 503, and 504. Set retry_strategy=None to disable. Retrying a write after an ambiguous failure requires reconciliation; do not assume every create is idempotent.

Raw lines 275 to 315
316## Direct requests and model compatibility
318For a documented endpoint not wrapped by a service, use its exact path and response
319shape. `api.get_response` returns a `Response` whose `parsed` value is a dictionary:
321```python
322response = benchling.api.get_response(url="/api/v2/projects?pageSize=1")
323projects = response.parsed["projects"]
324```
326`get_modeled(url, target_type)` and `post_modeled(url, target_type, body=...)` use
327SDK deserializable models. Use the endpoint's actual response model; a list envelope is not a single resource. Unknown enums/polymorphic responses are preserved where
328supported, but new values still need application handling; successful deserialization
329does not mean the integration understands their scientific meaning.

Direct requests and model compatibility

For a documented endpoint not wrapped by a service, use its exact path and response shape. api.get_response returns a Response whose parsed value is a dictionary:

response = benchling.api.get_response(url="/api/v2/projects?pageSize=1")
projects = response.parsed["projects"]

get_modeled(url, target_type) and post_modeled(url, target_type, body=...) use SDK deserializable models. Use the endpoint's actual response model; a list envelope is not a single resource. Unknown enums/polymorphic responses are preserved where supported, but new values still need application handling; successful deserialization does not mean the integration understands their scientific meaning.

Raw lines 316 to 329