SciAgent-Skills opentrons-integration
Opentrons Protocol API v2 for OT-2 and Flex liquid handling robots. Write Python protocols for automated pipetting, serial dilutions, PCR setup, plate replication. Control hardware modules (thermocycler, heater-shaker, magnetic, temperature). For multi-vendor lab automation use pylabrobot.
git clone https://github.com/jaechang-hits/SciAgent-Skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/jaechang-hits/SciAgent-Skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/lab-automation/opentrons-integration" ~/.claude/skills/jaechang-hits-sciagent-skills-opentrons-integration && rm -rf "$T"
skills/lab-automation/opentrons-integration/SKILL.mdOpentrons Integration — Lab Automation
Overview
Opentrons provides a Python-based Protocol API (v2) for programming OT-2 and Flex liquid handling robots. Protocols are structured Python files with metadata and a
run() function that controls pipettes, labware, and hardware modules. All protocols can be simulated locally before running on physical hardware.
When to Use
- Automating liquid handling workflows (pipetting, mixing, distributing)
- Writing PCR setup protocols with thermocycler control
- Performing serial dilutions across plates
- Replicating plates or reformatting between plate types
- Controlling hardware modules (temperature, magnetic, heater-shaker, thermocycler)
- Setting up multi-channel pipetting for 96-well plate operations
- Simulating protocols before running on the robot
- For multi-vendor automation (Hamilton, Beckman, etc.), use pylabrobot instead
- For flow cytometry analysis of automated experiment results, use flowio/flowkit
Prerequisites
pip install opentrons # Simulate protocols locally (no robot needed) opentrons_simulate my_protocol.py
Protocol API Version: Always use the latest stable API level (currently
2.19). Set apiLevel in protocol metadata. Protocols are forward-compatible within major versions.
Robot Types: Flex (newer, larger deck, 96-channel pipette) vs OT-2 (smaller, 8-channel max). Key differences: deck slot naming (Flex: A1-D3, OT-2: 1-11), available pipettes, and module support.
Quick Start
from opentrons import protocol_api metadata = {"protocolName": "Quick Transfer", "apiLevel": "2.19"} def run(protocol: protocol_api.ProtocolContext): tips = protocol.load_labware("opentrons_96_tiprack_300ul", "1") source = protocol.load_labware("nest_12_reservoir_15ml", "2") plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "3") pipette = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips]) pipette.distribute(50, source["A1"], plate.wells()[:12], new_tip="once")
Core API
1. Protocol Structure
Every Opentrons protocol follows a required structure: metadata dict +
run() function.
from opentrons import protocol_api metadata = { "protocolName": "My Protocol", "author": "Name <email>", "description": "Protocol description", "apiLevel": "2.19", } # Optional: specify robot type requirements = {"robotType": "Flex", "apiLevel": "2.19"} def run(protocol: protocol_api.ProtocolContext): # All protocol logic goes here protocol.comment("Protocol started")
2. Labware and Deck Layout
Load labware (plates, reservoirs, tip racks) onto deck slots and optionally onto adapters.
def run(protocol: protocol_api.ProtocolContext): # Tip racks tips_300 = protocol.load_labware("opentrons_96_tiprack_300ul", "1") tips_20 = protocol.load_labware("opentrons_96_tiprack_20ul", "4") # Plates and reservoirs plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "2", label="Sample Plate") reservoir = protocol.load_labware("nest_12_reservoir_15ml", "3") # Labware on adapter (Flex) adapter = protocol.load_adapter("opentrons_flex_96_tiprack_adapter", "B1") tips_on_adapter = adapter.load_labware("opentrons_flex_96_tiprack_200ul") # Pipettes p300 = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips_300]) p20 = protocol.load_instrument("p20_single_gen2", "right", tip_racks=[tips_20])
Common pipette names:
- OT-2:
,p20_single_gen2
,p300_single_gen2
,p1000_single_gen2
,p20_multi_gen2p300_multi_gen2 - Flex:
,p50_single_flex
,p1000_single_flex
,p50_multi_flexp1000_multi_flex
3. Pipette Operations
Basic, compound, and advanced liquid handling operations.
def run(protocol: protocol_api.ProtocolContext): # ... (labware loaded above) # === Basic operations === p300.pick_up_tip() p300.aspirate(100, source["A1"]) # Draw 100 µL p300.dispense(100, dest["B1"]) # Expel 100 µL p300.drop_tip() # === Compound operations (auto tip management) === # Transfer: single source → single dest p300.transfer(100, source["A1"], dest["B1"], new_tip="always") # Distribute: one source → many dests p300.distribute(50, reservoir["A1"], [plate["A1"], plate["A2"], plate["A3"]], new_tip="once") # Consolidate: many sources → one dest p300.consolidate(50, [plate["A1"], plate["A2"]], reservoir["A1"]) # === Advanced techniques === p300.pick_up_tip() p300.mix(repetitions=3, volume=50, location=plate["A1"]) # Mix in place p300.aspirate(100, source["A1"]) p300.air_gap(20) # Prevent dripping p300.dispense(120, dest["A1"]) p300.blow_out(dest["A1"].top()) # Expel residual p300.touch_tip(plate["A1"]) # Remove exterior drops p300.drop_tip()
4. Well Access and Locations
Navigate wells by name, index, row, or column. Control vertical position within wells.
def run(protocol: protocol_api.ProtocolContext): plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "1") # Access by name or index well = plate["A1"] first = plate.wells()[0] # Same as plate["A1"] # Iterate rows/columns row_a = plate.rows()[0] # [A1, A2, ..., A12] col_1 = plate.columns()[0] # [A1, B1, ..., H1] # Vertical positions pipette.aspirate(100, well.top()) # 1mm below top pipette.aspirate(100, well.bottom(z=2)) # 2mm above bottom pipette.aspirate(100, well.center()) # Center of well pipette.dispense(100, well.top(z=5)) # 5mm above top
5. Hardware Modules
Control temperature, magnetic, heater-shaker, and thermocycler modules.
def run(protocol: protocol_api.ProtocolContext): # Temperature module temp_mod = protocol.load_module("temperature module gen2", "3") temp_plate = temp_mod.load_labware("corning_96_wellplate_360ul_flat") temp_mod.set_temperature(celsius=4) # temp_mod.temperature → current temp; temp_mod.deactivate() # Magnetic module mag_mod = protocol.load_module("magnetic module gen2", "6") mag_plate = mag_mod.load_labware("nest_96_wellplate_100ul_pcr_full_skirt") mag_mod.engage(height_from_base=10) # Raise magnets (mm) mag_mod.disengage() # Heater-Shaker module hs_mod = protocol.load_module("heaterShakerModuleV1", "1") hs_plate = hs_mod.load_labware("corning_96_wellplate_360ul_flat") hs_mod.close_labware_latch() hs_mod.set_target_temperature(celsius=37) hs_mod.wait_for_temperature() hs_mod.set_and_wait_for_shake_speed(rpm=500) hs_mod.deactivate_shaker() hs_mod.deactivate_heater() hs_mod.open_labware_latch() # Thermocycler (auto-assigned to slots) tc_mod = protocol.load_module("thermocyclerModuleV2") tc_plate = tc_mod.load_labware("nest_96_wellplate_100ul_pcr_full_skirt") tc_mod.open_lid() tc_mod.close_lid() tc_mod.set_lid_temperature(celsius=105) tc_mod.set_block_temperature(95, hold_time_seconds=180) profile = [ {"temperature": 95, "hold_time_seconds": 15}, {"temperature": 60, "hold_time_seconds": 30}, {"temperature": 72, "hold_time_seconds": 60}, ] tc_mod.execute_profile(steps=profile, repetitions=30, block_max_volume=50) tc_mod.deactivate_lid() tc_mod.deactivate_block()
6. Protocol Control and Utilities
Pause, delay, comment, liquid tracking, and simulation detection.
def run(protocol: protocol_api.ProtocolContext): # Execution control protocol.pause(msg="Replace tip box and resume") protocol.delay(seconds=60) protocol.delay(minutes=5) protocol.comment("Starting serial dilution") protocol.home() # Liquid tracking (visual in Opentrons App) water = protocol.define_liquid(name="Water", description="Ultrapure water", display_color="#0000FF") reservoir["A1"].load_liquid(liquid=water, volume=50000) plate["B1"].load_empty() # Check simulation vs real run if protocol.is_simulating(): protocol.comment("Simulation mode") # Flow rate control (µL/s) pipette.flow_rate.aspirate = 150 pipette.flow_rate.dispense = 300 pipette.flow_rate.blow_out = 400
Key Concepts
Protocol File Structure
All Opentrons protocols are Python files with this required structure:
┌─ metadata dict ──────────────── protocolName, apiLevel, author ├─ requirements dict (optional) ── robotType └─ def run(protocol): ─────────── All robot commands
The
run() function receives a ProtocolContext object — all labware loading, pipette operations, and module control happen through this single entry point. Protocols cannot import arbitrary packages for execution on the robot.
OT-2 vs Flex Differences
| Feature | OT-2 | Flex |
|---|---|---|
| Deck slots | 1-11 (numeric) | A1-D3 (grid) |
| Pipettes | Gen2 (, , ) | Flex (, , 96-channel) |
| Max channels | 8-channel multi | 96-channel |
| Modules | Gen1/Gen2 | V2 modules |
| Adapters | Not supported | Supported (tiprack, flat) |
Multi-Channel Pipette Behavior
When using multi-channel pipettes, referencing a single well accesses the entire column:
multi = protocol.load_instrument("p300_multi_gen2", "left", tip_racks=[tips]) # This transfers from ALL wells in column 1 of source to column 1 of dest multi.transfer(100, source["A1"], dest["A1"])
Common Workflows
Workflow: Serial Dilution
from opentrons import protocol_api metadata = {"protocolName": "Serial Dilution", "apiLevel": "2.19"} def run(protocol: protocol_api.ProtocolContext): tips = protocol.load_labware("opentrons_96_tiprack_300ul", "1") reservoir = protocol.load_labware("nest_12_reservoir_15ml", "2") plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "3") p300 = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips]) # Add diluent to columns 2-12 p300.transfer(100, reservoir["A1"], plate.rows()[0][1:]) # Serial dilution across row A p300.transfer( 100, plate.rows()[0][:11], plate.rows()[0][1:], mix_after=(3, 50), new_tip="always", )
Workflow: PCR Setup with Thermocycler
from opentrons import protocol_api metadata = {"protocolName": "PCR Setup", "apiLevel": "2.19"} def run(protocol: protocol_api.ProtocolContext): tc_mod = protocol.load_module("thermocyclerModuleV2") tc_plate = tc_mod.load_labware("nest_96_wellplate_100ul_pcr_full_skirt") tips = protocol.load_labware("opentrons_96_tiprack_300ul", "1") reagents = protocol.load_labware("opentrons_24_tuberack_nest_1.5ml_snapcap", "2") p300 = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips]) tc_mod.open_lid() # Distribute master mix p300.distribute(20, reagents["A1"], tc_plate.wells()[:8], new_tip="once") # Add samples for i in range(8): p300.transfer(5, reagents.wells()[i + 1], tc_plate.wells()[i], new_tip="always") # Run PCR tc_mod.close_lid() tc_mod.set_lid_temperature(105) tc_mod.set_block_temperature(95, hold_time_seconds=180) # Initial denaturation profile = [ {"temperature": 95, "hold_time_seconds": 15}, {"temperature": 60, "hold_time_seconds": 30}, {"temperature": 72, "hold_time_seconds": 30}, ] tc_mod.execute_profile(steps=profile, repetitions=35, block_max_volume=25) tc_mod.set_block_temperature(72, hold_time_minutes=5) # Final extension tc_mod.set_block_temperature(4) # Hold tc_mod.deactivate_lid() tc_mod.open_lid()
Workflow: Magnetic Bead Cleanup
- Load magnetic module with deep-well plate, reservoir with wash buffers
- Engage magnets → aspirate supernatant → dispense to waste
- Disengage magnets → add wash buffer → mix → engage → remove wash (repeat 2x)
- Disengage → add elution buffer → mix → engage → transfer eluate to clean plate
Key Parameters
| Parameter | Function | Default | Range | Effect |
|---|---|---|---|---|
| , , | — | 1–1000 µL | Liquid volume |
| , , | | , , | Tip change strategy |
| | | tuple | Post-dispense mixing |
| | | tuple | Pre-aspirate mixing |
| | | / | Blow out after dispense |
| | | / | Touch tip after dispense |
| | | 0–pipette max µL | Air gap volume |
| pipette property | varies | 1–1000 µL/s | Aspirate speed |
| pipette property | varies | 1–1000 µL/s | Dispense speed |
| | — | 0–20 mm | Magnet engagement height |
Best Practices
-
Always simulate first: Run
before uploading to the robot. Catches labware conflicts, volume errors, and tip shortages without wasting consumables.opentrons_simulate my_protocol.py -
Use compound operations over basic: Prefer
,transfer()
,distribute()
over manualconsolidate()
sequences — they handle tip management automatically.pick_up_tip/aspirate/dispense/drop_tip -
Anti-pattern — ignoring tip count: A protocol that runs out of tips will error mid-run. Count total tip uses vs rack capacity before running.
-
Track liquids for setup validation: Use
anddefine_liquid()
to enable volume tracking in the Opentrons App.load_liquid() -
Anti-pattern — hardcoding deck slots across robot types: Flex uses grid coordinates (A1-D3), OT-2 uses numbers (1-11). Write separate metadata or use
to ensure compatibility.requirements["robotType"] -
Control flow rates for difficult liquids: Reduce aspirate speed for viscous solutions (glycerol, PEG), increase for water-like liquids.
-
Use pauses for manual intervention:
is safer thanprotocol.pause(msg=...)
when you need user action (e.g., adding reagent, sealing plate).protocol.delay()
Common Recipes
Recipe: Plate Replication
from opentrons import protocol_api metadata = {"protocolName": "Plate Replication", "apiLevel": "2.19"} def run(protocol: protocol_api.ProtocolContext): tips = protocol.load_labware("opentrons_96_tiprack_300ul", "1") source = protocol.load_labware("corning_96_wellplate_360ul_flat", "2") dest = protocol.load_labware("corning_96_wellplate_360ul_flat", "3") p300 = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips]) p300.transfer(100, source.wells(), dest.wells(), new_tip="always")
Recipe: Reagent Distribution with Multi-Channel
from opentrons import protocol_api metadata = {"protocolName": "Multi-Channel Distribution", "apiLevel": "2.19"} def run(protocol: protocol_api.ProtocolContext): tips = protocol.load_labware("opentrons_96_tiprack_300ul", "1") reservoir = protocol.load_labware("nest_12_reservoir_15ml", "2") plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "3") multi = protocol.load_instrument("p300_multi_gen2", "left", tip_racks=[tips]) # Fill all 96 wells: 12 columns × 8 rows via multi-channel multi.transfer(100, reservoir["A1"], plate.rows()[0], new_tip="once")
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| Protocol needs more tips than available | Add multiple tip racks to list, or reload tips with |
| Labware collision on deck | Two items assigned to overlapping slots | Check deck map — thermocycler auto-occupies multiple slots; use to inspect |
| Volume exceeds pipette capacity | Attempting to aspirate/dispense > max volume | Use which auto-splits volumes, or switch to a larger pipette |
| Wrong labware API name | Check names at labware.opentrons.com; use exact API name strings |
| Protocol works in simulation but fails on robot | Hardware-specific timing issue | Add between temperature changes; increase magnet engage time |
| Inaccurate volumes | Pipette calibration or air bubbles | Recalibrate pipette; pre-wet tips with ; adjust flow rates for viscous liquids |
| Module not connected or wrong model string | Verify module serial connection; use exact model strings () |
Related Skills
- pylabrobot — multi-vendor lab automation (Hamilton, Beckman, Tecan) for cross-platform protocols
- biopython-molecular-biology — sequence design and primer tools for PCR protocol inputs
References
- Opentrons Protocol API v2 docs — official API reference
- Opentrons Labware Library — searchable labware definitions
- Opentrons Python Protocol Tutorial — step-by-step getting started guide