Expert guidance for Opentrons Python API v2 - automated liquid handling, protocol development, labware management, and hardware module integration for OT-2 and Flex robots
The Opentrons Python API v2 enables laboratory automation through Python protocols that control liquid handling robots (OT-2 and Flex). Write protocols that automate pipetting, temperature control, magnetic separation, plate reading, and complex multi-step workflows with reproducible precision.
Core value: Transform manual, error-prone pipetting workflows into automated, reproducible protocols. A 100-line protocol can replace hours of manual work with precise, documented liquid handling.
Use the Opentrons skill when:
Specialized subskills available:
heater-shaker - Temperature control with orbital mixingabsorbance-reader - Microplate spectrophotometrygripper - Automated labware movement (Flex only)thermocycler - PCR thermal cyclingtemperature-module - Precise heating/cooling (4-95°C)magnetic-block - Magnetic bead separation (Flex only)Don't use when:
Modern platform with advanced capabilities:
Established platform:
Both platforms use identical API syntax with platform-specific parameters.
Every Opentrons protocol follows this framework:
from opentrons import protocol_api
# 1. Metadata - Protocol identification
metadata = {
'protocolName': 'My Protocol',
'author': 'Name <email@example.com>',
'description': 'Brief description of what this protocol does',
'apiLevel': '2.19'
}
# 2. Requirements - Robot specification (Flex)
requirements = {"robotType": "Flex", "apiLevel": "2.19"}
# 3. Main function - Setup and commands
def run(protocol: protocol_api.ProtocolContext):
# Setup: Load labware, pipettes, modules
tips = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "A1")
plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "B1")
pipette = protocol.load_instrument("flex_1channel_1000", "left", tip_racks=[tips])
# Commands: Sequential robot instructions
pipette.pick_up_tip()
pipette.aspirate(100, plate["A1"])
pipette.dispense(100, plate["B1"])
pipette.drop_tip()
Key principles:
Labware - Plates, tubes, reservoirs:
# Load standard labware from registry
plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "B1")
tips = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "A1")
# Load custom labware (JSON definition required)
custom = protocol.load_labware("custom_24_tuberack_1500ul", "C1")
Pipettes - Single, 8-channel, or 96-channel:
# Flex pipettes
left_pip = protocol.load_instrument("flex_1channel_1000", "left", tip_racks=[tips])
right_pip = protocol.load_instrument("flex_8channel_1000", "right", tip_racks=[tips])
# OT-2 pipettes
p300 = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips])
Modules - Temperature, magnetic, thermocycler:
# Load module in specific deck slot
temp_mod = protocol.load_module("temperature module gen2", "D1")
# Load labware on module
temp_plate = temp_mod.load_labware("opentrons_96_aluminumblock_generic_pcr_strip_200ul")
Trash bin (Flex, API 2.16+):
trash = protocol.load_trash_bin("A3")
Individual wells:
plate["A1"] # Single well by name
plate.wells()[0] # First well by index
plate.wells_by_name()["A1"] # Dictionary access
Well ranges and groups:
plate.wells()[:8] # First row (A1-H1)
plate.rows()[0] # All wells in row A
plate.columns()[0] # All wells in column 1
plate.wells("A1", "H12") # Range from A1 to H12
8-channel pipetting patterns:
# Pipette to entire column with 8-channel
pipette.transfer(100, reservoir["A1"], plate.columns()[0])
Basic operations:
pipette.pick_up_tip()
pipette.aspirate(volume=100, location=plate["A1"], rate=1.0)
pipette.dispense(volume=100, location=plate["B1"], rate=1.0)
pipette.blow_out(location=plate["B1"])
pipette.mix(repetitions=3, volume=50, location=plate["B1"])
pipette.drop_tip() # or pipette.return_tip()
Complex transfers:
# Single source to multiple destinations
pipette.distribute(
volume=50,
source=reservoir["A1"],
dest=[plate.wells()[i] for i in range(8)],
new_tip="once"
)
# Multiple sources to single destination (pooling)
pipette.consolidate(
volume=20,
source=plate.rows()[0],
dest=reservoir["A2"],
new_tip="always"
)
# One-to-one transfer with mixing
pipette.transfer(
volume=100,
source=plate.columns()[0],
dest=plate.columns()[1],
mix_after=(3, 50),
new_tip="always"
)
Well positions:
# Top of well (default: 1mm below top)
location = plate["A1"].top()
location = plate["A1"].top(z=-2) # 2mm below top
# Bottom of well (default: 1mm above bottom)
location = plate["A1"].bottom()
location = plate["A1"].bottom(z=2) # 2mm above bottom
# Center of well
location = plate["A1"].center()
Movement:
# Move to location without liquid operations
pipette.move_to(plate["A1"].top())
# Delay at current position
protocol.delay(seconds=5)
protocol.delay(minutes=2)
Flexible pickup strategies:
# Pick up from specific tip rack location
pipette.pick_up_tip(tips["A1"])
# Automatic tip tracking
pipette.pick_up_tip() # Uses next available tip
# Reset tip tracking
tips.reset() # Mark all tips as available again
Define liquids for better protocol documentation:
# Define reagents
water = protocol.define_liquid(
name="Water",
description="Molecular biology grade H2O",
display_color="#0000FF"
)
buffer = protocol.define_liquid(
name="Lysis Buffer",
description="10mM Tris-HCl pH 8.0",
display_color="#00FF00"
)
# Assign liquids to wells with volumes
reservoir["A1"].load_liquid(liquid=water, volume=50000) # 50mL
plate["A1"].load_liquid(liquid=buffer, volume=200)
User-configurable variables:
def add_parameters(parameters):
parameters.add_int(
variable_name="sample_count",
display_name="Number of Samples",
description="How many samples to process",
default=24,
minimum=1,
maximum=96,
unit="samples"
)
parameters.add_float(
variable_name="transfer_volume",
display_name="Transfer Volume",
default=50.0,
minimum=10.0,
maximum=200.0,
unit="µL"
)
def run(protocol: protocol_api.ProtocolContext):
# Access parameters
num_samples = protocol.params.sample_count
vol = protocol.params.transfer_volume
Opentrons supports multiple hardware modules for specialized workflows. Each module has dedicated API methods:
Precise heating/cooling (4-95°C):
temp_mod = protocol.load_module("temperature module gen2", "D1")
plate = temp_mod.load_labware("opentrons_96_aluminumblock_generic_pcr_strip_200ul")
temp_mod.set_temperature(celsius=4) # Blocks until reached
temp_mod.deactivate()
Use the temperature-module subskill for detailed guidance.
Temperature control with orbital mixing:
hs_mod = protocol.load_module("heaterShakerModuleV1", "D1")
adapter = hs_mod.load_adapter("opentrons_96_flat_bottom_adapter")
plate = adapter.load_labware("nest_96_wellplate_200ul_flat")
hs_mod.close_labware_latch()
hs_mod.set_and_wait_for_temperature(37)
hs_mod.set_and_wait_for_shake_speed(500) # rpm
protocol.delay(minutes=5)
hs_mod.deactivate_shaker()
hs_mod.deactivate_heater()
hs_mod.open_labware_latch()
Use the heater-shaker subskill for detailed guidance including non-blocking temperature control, speed ranges, and OT-2 deck restrictions.
PCR thermal cycling:
tc_mod = protocol.load_module("thermocyclerModuleV2")
plate = tc_mod.load_labware("opentrons_96_wellplate_200ul_pcr_full_skirt")
tc_mod.close_lid()
tc_mod.set_lid_temperature(105)
tc_mod.set_block_temperature(95, hold_time_seconds=180, block_max_volume=50)
# PCR profile
profile = [
{"temperature": 95, "hold_time_seconds": 30},
{"temperature": 57, "hold_time_seconds": 30},
{"temperature": 72, "hold_time_seconds": 60}
]
tc_mod.execute_profile(steps=profile, repetitions=30, block_max_volume=50)
tc_mod.set_block_temperature(72, hold_time_minutes=5)
tc_mod.deactivate_lid()
tc_mod.deactivate_block()
tc_mod.open_lid()
Use the thermocycler subskill for detailed guidance including auto-sealing lids and advanced profiling.
Microplate spectrophotometry:
reader = protocol.load_module("absorbanceReaderV1", "D3")
reader.close_lid()
reader.initialize("single", [450]) # Single wavelength
reader.open_lid()
# Move plate to reader with gripper
protocol.move_labware(plate, reader, use_gripper=True)
reader.close_lid()
data = reader.read(export_filename="my_plate_data")
reader.open_lid()
# Access results
absorbance_a1 = data[450]["A1"] # OD at 450nm for well A1
Use the absorbance-reader subskill for detailed guidance including multi-wavelength reading and data formats.
Magnetic bead separation:
mag_block = protocol.load_module("magneticBlockV1", "D1")
mag_plate = mag_block.load_labware("biorad_96_wellplate_200ul_pcr")
# Move plate to magnetic block with gripper
protocol.move_labware(mag_plate, mag_block, use_gripper=True)
# Wait for bead separation
protocol.delay(minutes=3)
# Pipette supernatant (beads held by magnets)
pipette.transfer(100, mag_plate["A1"], waste["A1"])
# Move plate away from magnets
protocol.move_labware(mag_plate, "C1", use_gripper=True)
Use the magnetic-block subskill for detailed guidance.
Automated labware movement:
# Move labware between locations
protocol.move_labware(
labware=plate,
new_location="C2",
use_gripper=True
)
# Move to module
protocol.move_labware(plate, temp_mod, use_gripper=True)
# Dispose in waste chute
protocol.move_labware(plate, protocol.load_waste_chute(), use_gripper=True)
# Move with offset adjustments
protocol.move_labware(
plate,
"D2",
use_gripper=True,
pick_up_offset={"x": 0, "y": 0, "z": 2},
drop_offset={"x": 0, "y": 0, "z": 1}
)
Important: Open module lids/latches before gripper movement (e.g., tc_mod.open_lid(), hs_mod.open_labware_latch()).
Use the gripper subskill for detailed guidance.
Full-plate transfers in single operations:
pipette_96 = protocol.load_instrument("flex_96channel_1000", "left")
# Pick up entire tip rack
pipette_96.pick_up_tip()
# Transfer entire plate
pipette_96.transfer(100, source_plate.wells(), dest_plate.wells())
pipette_96.drop_tip()
Column 4 slots (A4, B4, C4, D4) serve as staging areas for:
Note: Cannot load labware in column 4 when absorbance reader is present.
Key changes required:
requirements = {"robotType": "Flex", "apiLevel": "2.19"}
# OT-2
p300 = protocol.load_instrument("p300_single_gen2", "left")
# Flex
p1000 = protocol.load_instrument("flex_1channel_1000", "left")
# OT-2
tips = protocol.load_labware("opentrons_96_tiprack_300ul", "1")
# Flex
tips = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "A1")
trash = protocol.load_trash_bin("A3")
# Numeric (OT-2 style, works on both)
plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "1")
# Coordinate (Flex style, works on both)
plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "D1")
def serial_dilution(pipette, source, destinations, dilution_factor, volume):
"""Perform serial dilution across wells."""
# Transfer from source to first destination
pipette.transfer(volume, source, destinations[0])
# Serial transfers with mixing
for i in range(len(destinations) - 1):
pipette.transfer(
volume / dilution_factor,
destinations[i],
destinations[i + 1],
mix_after=(3, volume / 2)
)
# Use pattern
serial_dilution(
pipette=p1000,
source=reservoir["A1"],
destinations=plate.rows()[0], # Row A
dilution_factor=2,
volume=100
)
# Replicate entire plate
pipette.transfer(
volume=50,
source=source_plate.wells(),
dest=dest_plate.wells(),
new_tip="always"
)
# Transfer specific wells
source_wells = [source_plate["A1"], source_plate["C3"], source_plate["E5"]]
dest_wells = [dest_plate["B2"], dest_plate["D4"], dest_plate["F6"]]
pipette.transfer(100, source_wells, dest_wells, new_tip="always")
# Distribute from reservoir to plate
pipette.distribute(
volume=50,
source=reservoir["A1"],
dest=plate.wells(),
disposal_volume=10, # Extra volume for blowout
new_tip="once"
)
distribute/consolidate to minimize tip consumption# Dynamic protocol behavior based on parameters
if protocol.params.include_control:
pipette.transfer(100, control_buffer, plate["A1"])
# Process only occupied wells
for well in plate.wells():
if well.has_liquid():
pipette.mix(3, 100, well)
Prevent dripping between wells:
pipette.aspirate(100, source_well)
pipette.air_gap(10) # 10µL air gap
pipette.dispense(110, dest_well)
Remove droplets from pipette exterior:
pipette.aspirate(100, source_well)
pipette.touch_tip(location=source_well, radius=0.8, v_offset=-2)
pipette.dispense(100, dest_well)
Ensure complete dispense:
pipette.aspirate(100, source_well)
pipette.dispense(100, dest_well)
pipette.blow_out(dest_well.top())
❌ Forgetting to pick up tips:
pipette.aspirate(100, plate["A1"]) # Error: no tip attached
✅ Correct:
pipette.pick_up_tip()
pipette.aspirate(100, plate["A1"])
❌ Not deactivating modules:
temp_mod.set_temperature(4)
# Protocol ends - module stays at 4°C!
✅ Correct:
temp_mod.set_temperature(4)
# ... operations ...
temp_mod.deactivate()
❌ Gripper movement with closed latch:
protocol.move_labware(plate, hs_mod, use_gripper=True) # Error: latch closed
✅ Correct:
hs_mod.open_labware_latch()
protocol.move_labware(plate, hs_mod, use_gripper=True)
hs_mod.close_labware_latch()
❌ Wrong deck slot format for Flex: Using numeric slots without quotes may cause confusion; both work, but coordinate format is clearer:
plate = protocol.load_labware("corning_96_wellplate_360ul_flat", 1) # Works but unclear
✅ Correct:
plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "D1") # Clear
Protocol won't load:
Pipetting errors:
Module errors:
Gripper errors:
Protocols run on Opentrons robots via the Opentrons App. For protocol development and simulation:
pip install opentrons
Simulation:
opentrons_simulate protocol.py
For detailed module-specific guidance:
heater-shaker - Heater-Shaker Moduleabsorbance-reader - Absorbance Plate Readergripper - Flex Gripperthermocycler - Thermocycler Moduletemperature-module - Temperature Modulemagnetic-block - Magnetic BlockSearch for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer