Opentrons Python Protocol API
Overview
The Opentrons Protocol API v2 lets you write liquid handling protocols as plain Python files that run on OT-2 or Flex robots. Every protocol defines a metadata dictionary, an optional requirements dictionary, and a run(protocol) function. The ProtocolContext object passed to run() exposes all deck setup, pipette operations, module control, and utility methods. Protocols can be simulated on any computer with opentrons_simulate before uploading to the robot through the Opentrons App or HTTP API.
When to Use
- Setting up PCR reactions: Distribute master mix from a tube rack into a thermocycler plate, add template DNA from individual tubes, then execute a PCR profile automatically.
- Running serial dilutions: Programmatically step a multi-channel pipette across a 96-well plate to create 2-fold or custom dilution curves with defined diluent volumes.
- Performing ELISA plate layouts: Add blocking buffer, primary antibody, secondary antibody, and substrate to defined wells with tip changes between each reagent.
- Automating magnetic bead cleanups: Engage/disengage the magnetic module, aspirate supernatant, wash with ethanol, and elute — in a fully automated loop.
- Plate reformatting and stamping: Transfer an entire 96-well plate to a destination plate with one command; reformat from tubes to plates.
- Integrating hardware modules: Coordinate temperature control, shaking, and liquid handling steps in a single protocol with precise timing.
- Use
PyLabRobotinstead when writing protocols that must run on Hamilton STAR, Tecan Freedom EVO, or other vendors without Opentrons-specific hardware; for Opentrons-only workflows the native Protocol API provides tighter integration and module support. - For retrieving and parsing published protocols before automation, use
protocolsio-integrationto search protocols.io alongside this skill.
Prerequisites
- Python packages:
opentrons - Robot types: OT-2 (slots 1-11, Gen2 pipettes) or Flex (slots A1-D3, Flex pipettes)
- Environment: Python 3.10+; Opentrons App for uploading to physical robot
- CLI tool:
opentrons_simulateships with the package for local testing
pip install opentrons
# Verify installation and simulate a protocol locally
opentrons_simulate my_protocol.py
Quick Start
A minimal protocol showing all required elements — metadata, labware, instrument, and a transfer:
from opentrons import protocol_api
metadata = {
"protocolName": "Simple Reagent Distribution",
"author": "Lab Automation Team",
"apiLevel": "2.19",
}
def run(protocol: protocol_api.ProtocolContext):
# Load labware onto deck slots
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")
# Load pipette and attach tip rack
pipette = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips])
# Distribute 50 µL from reservoir A1 to first 12 wells using one tip
pipette.distribute(50, source["A1"], plate.wells()[:12], new_tip="once")
protocol.comment("Distribution complete")
# Simulate locally — no robot needed
opentrons_simulate simple_reagent_distribution.py
Core API
Module 1: Protocol Metadata and Deck Setup
Every protocol requires a metadata dict specifying at minimum apiLevel. The optional requirements dict sets the target robot type. All labware and instruments are loaded through the ProtocolContext.
from opentrons import protocol_api
# Minimum required metadata
metadata = {
"protocolName": "My Assay Protocol",
"author": "Jane Smith <jane@lab.org>",
"description": "96-well assay setup with temperature control",
"apiLevel": "2.19",
}
# Optional: target a specific robot type (Flex or OT-2)
requirements = {"robotType": "OT-2", "apiLevel": "2.19"}
def run(protocol: protocol_api.ProtocolContext):
# OT-2: slots numbered 1-11 in a 3×4 grid
tips_300 = protocol.load_labware("opentrons_96_tiprack_300ul", "1")
tips_20 = protocol.load_labware("opentrons_96_tiprack_20ul", "4")
source = protocol.load_labware("nest_12_reservoir_15ml", "2", label="Buffer Reservoir")
plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "3")
tube_rack = protocol.load_labware("opentrons_24_tuberack_nest_1.5ml_snapcap", "5")
# Load both pipettes (optional: one or two mounts)
p300 = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips_300])
p20 = protocol.load_instrument("p20_single_gen2", "right", tip_racks=[tips_20])
print(f"Deck has {len(protocol.deck)} slots; pipettes: {[p300.name, p20.name]}")
OT-2 deck layout (3 columns × 4 rows, numbered left-to-right, bottom-to-top):
Slot map (OT-2): Slot map (Flex, A-D rows, 1-3 cols):
10 | 11 | Trash D1 | D2 | D3
7 | 8 | 9 C1 | C2 | C3
4 | 5 | 6 B1 | B2 | B3
1 | 2 | 3 A1 | A2 | A3
Common OT-2 pipette names: p20_single_gen2, p300_single_gen2, p1000_single_gen2, p20_multi_gen2, p300_multi_gen2. Common Flex pipette names: p50_single_flex, p1000_single_flex, p50_multi_flex, p1000_multi_flex, flex_96channel_1000.
Module 2: Pipette Operations
Low-level aspirate/dispense/blow-out operations for precise step-by-step control.
def run(protocol: protocol_api.ProtocolContext):
tips = protocol.load_labware("opentrons_96_tiprack_300ul", "1")
source = protocol.load_labware("nest_12_reservoir_15ml", "2")
dest = protocol.load_labware("corning_96_wellplate_360ul_flat", "3")
p300 = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips])
p300.pick_up_tip()
# Aspirate and dispense — basic liquid movement
p300.aspirate(100, source["A1"]) # draw 100 µL from reservoir
p300.dispense(100, dest["A1"]) # expel into plate well
# Air gap to prevent dripping during transport
p300.aspirate(80, source["A2"])
p300.air_gap(20) # draw 20 µL air to cap the tip
p300.dispense(100, dest["A2"]) # dispenses liquid + air
# Mix in place (repetitions, volume)
p300.mix(3, 60, dest["A1"]) # mix 60 µL × 3 times
# Remove exterior droplets / expel residual
p300.touch_tip(dest["A1"]) # wipe tip on well rim
p300.blow_out(dest["A1"].top()) # expel last drop at top
p300.drop_tip()
protocol.comment("Low-level operations complete")
def run(protocol: protocol_api.ProtocolContext):
tips = protocol.load_labware("opentrons_96_tiprack_300ul", "1")
p300 = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips])
# Adjust flow rates (µL/s) for viscous or sensitive samples
p300.flow_rate.aspirate = 50 # slow down for viscous liquids (default ~150)
p300.flow_rate.dispense = 150 # default dispense speed
p300.flow_rate.blow_out = 300 # fast blow-out for complete expulsion
print(f"Aspirate rate: {p300.flow_rate.aspirate} µL/s")
Module 3: transfer() Shortcut
transfer(), distribute(), and consolidate() handle tip management automatically and accept mix, blow-out, and air-gap options.
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])
# transfer(): one source → one destination, with optional per-well tip changes
p300.transfer(
100,
source["A1"],
dest["A1"],
new_tip="always", # options: "