Opentrons 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_gen2,p300_multi_gen2 - Flex:
p50_single_flex,p1000_single_flex,p50_multi_flex,p1000_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", desc