SciAgent-Skills opentrons-protocol-api
Python API v2 for programming Opentrons OT-2 and Flex liquid handling robots. Write protocols as Python files with metadata and a run() function; control pipettes, labware, and hardware modules (thermocycler, heater-shaker, magnetic, temperature). Simulate locally with opentrons_simulate, then upload to the robot app. Use PyLabRobot instead for hardware-agnostic scripts that run on Hamilton, Tecan, or other vendors.
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-protocol-api" ~/.claude/skills/jaechang-hits-sciagent-skills-opentrons-protocol-api && rm -rf "$T"
skills/lab-automation/opentrons-protocol-api/SKILL.mdOpentrons 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
instead 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.PyLabRobot - For retrieving and parsing published protocols before automation, use
to search protocols.io alongside this skill.protocolsio-integration
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:
ships with the package for local testingopentrons_simulate
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: "always", "once", "never" mix_after=(3, 50), # mix 50 µL × 3 reps after each dispense blow_out=True, touch_tip=True, ) # transfer() with lists: pairwise source-destination mapping sources = source.wells()[:8] dests = dest.wells()[:8] p300.transfer(75, sources, dests, new_tip="always") # distribute(): one source → many destinations (single tip, multi-dispense) p300.distribute( 50, source["A1"], dest.wells()[:12], new_tip="once", # use one tip for all destinations disposal_volume=10, # extra volume drawn to ensure accuracy ) # consolidate(): many sources → one destination (collect, then dispense) p300.consolidate( 50, source.wells()[:8], dest["A1"], mix_after=(3, 100), ) print("Compound transfer operations complete")
Module 4: Labware, Liquids, and Well Access
Load labware from the library, navigate wells by name/row/column, and define liquids for visual tracking in the Opentrons App.
def run(protocol: protocol_api.ProtocolContext): plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "1") p300 = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[protocol.load_labware("opentrons_96_tiprack_300ul", "2")]) # Access wells by alphanumeric name well_a1 = plate["A1"] # Access all wells (column-major order: A1, B1, C1, ..., H1, A2, ...) all_wells = plate.wells() print(f"Total wells: {len(all_wells)}") # 96 # Access by row (8 rows, A-H; each row has 12 wells) row_a = plate.rows()[0] # [A1, A2, ..., A12] row_b = plate.rows()[1] # [B1, B2, ..., B12] # Access by column (12 columns, 1-12; each column has 8 wells) col_1 = plate.columns()[0] # [A1, B1, C1, D1, E1, F1, G1, H1] # Vertical position control within a well p300.pick_up_tip() p300.aspirate(80, well_a1.bottom(z=1)) # 1 mm above well bottom p300.dispense(80, well_a1.top(z=-2)) # 2 mm below well top p300.aspirate(80, well_a1.center()) # geometric center p300.drop_tip()
def run(protocol: protocol_api.ProtocolContext): reservoir = protocol.load_labware("nest_12_reservoir_15ml", "1") plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "2") # Define liquids for visual tracking in Opentrons App pbs = protocol.define_liquid(name="1× PBS", description="Phosphate buffered saline", display_color="#0077BB") sample = protocol.define_liquid(name="Sample", description="Cell lysate, 1 mg/mL protein", display_color="#EE7733") # Assign liquids to wells with known starting volumes (µL) reservoir["A1"].load_liquid(liquid=pbs, volume=10000) reservoir["A2"].load_liquid(liquid=sample, volume=5000) # Mark destination wells as empty for well in plate.wells(): well.load_empty() print("Liquids defined and assigned")
Module 5: Hardware Modules
Control temperature, magnetic, thermocycler, and heater-shaker modules. Each module is loaded by its model name string and occupies specific deck slots.
def run(protocol: protocol_api.ProtocolContext): # --- Temperature Module (Gen2) --- 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) # blocks until target reached print(f"Temp module: {temp_mod.temperature}°C") # temp_mod.deactivate() # turn off at end # --- Magnetic Module (Gen2) --- 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 10 mm from plate base protocol.delay(seconds=300) # hold beads for 5 min 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) protocol.delay(minutes=30) hs_mod.deactivate_shaker() hs_mod.deactivate_heater() hs_mod.open_labware_latch() print("Heater-shaker cycle complete")
def run(protocol: protocol_api.ProtocolContext): # --- Thermocycler Module (Gen2) --- # Auto-occupies slots 7-11 on OT-2; no slot argument needed 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.set_lid_temperature(celsius=105) # pre-heat lid to prevent condensation # Initial denaturation tc_mod.set_block_temperature(95, hold_time_seconds=180) # PCR cycling profile profile = [ {"temperature": 95, "hold_time_seconds": 15}, # denaturation {"temperature": 60, "hold_time_seconds": 30}, # annealing {"temperature": 72, "hold_time_seconds": 30}, # extension ] tc_mod.execute_profile(steps=profile, repetitions=35, block_max_volume=25) # Final extension and hold tc_mod.set_block_temperature(72, hold_time_minutes=5) tc_mod.set_block_temperature(4) # hold at 4°C indefinitely tc_mod.deactivate_lid() tc_mod.open_lid() print("PCR complete; plate held at 4°C")
Module 6: Advanced Protocol Features
Pause for user interaction, log comments visible in the app, control rail lights, and detect simulation mode.
def run(protocol: protocol_api.ProtocolContext): # Pause and prompt the user (robot stops, app shows message) protocol.pause(msg="Add 10 µL of enzyme to tube A1, then resume") # Timed delay (robot waits without user action) protocol.delay(seconds=30, msg="Waiting 30s for reaction incubation") protocol.delay(minutes=5) # Log a comment visible in Opentrons App run log protocol.comment("Starting serial dilution — columns 1 to 11") # Rail lights for visual status indication protocol.set_rail_lights(True) # lights on protocol.set_rail_lights(False) # lights off # Home all axes (useful after an error or before finishing) protocol.home() # Detect simulation vs. physical run — skip slow waits in simulation if protocol.is_simulating(): protocol.comment("Running in simulation mode — skipping 10-min incubation") else: protocol.delay(minutes=10) # Load waste bin (Flex only — OT-2 uses fixed trash) # trash = protocol.load_trash_bin("A3") print("Protocol control features demonstrated")
Common Workflows
Workflow 1: PCR Setup with Thermocycler
Goal: Transfer master mix from a tube rack into a PCR plate on the thermocycler, add template DNA from individual samples, then run a complete PCR cycling program.
from opentrons import protocol_api metadata = { "protocolName": "PCR Setup and Run", "author": "Lab Automation", "apiLevel": "2.19", } def run(protocol: protocol_api.ProtocolContext): # Hardware setup tc_mod = protocol.load_module("thermocyclerModuleV2") tc_plate = tc_mod.load_labware("nest_96_wellplate_100ul_pcr_full_skirt") tips_300 = protocol.load_labware("opentrons_96_tiprack_300ul", "1") tips_20 = protocol.load_labware("opentrons_96_tiprack_20ul", "4") reagents = protocol.load_labware("opentrons_24_tuberack_nest_1.5ml_snapcap", "2") p300 = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips_300]) p20 = protocol.load_instrument("p20_single_gen2", "right", tip_racks=[tips_20]) # Define liquids master_mix = protocol.define_liquid("Master Mix", "2× PCR master mix", "#33BBEE") template = protocol.define_liquid("Template", "gDNA 10 ng/µL", "#EE3377") reagents["A1"].load_liquid(master_mix, volume=500) for i in range(8): reagents.wells()[i + 1].load_liquid(template, volume=50) # Step 1: Open lid and distribute master mix (20 µL per well, 8 wells) tc_mod.open_lid() protocol.comment("Distributing master mix") p300.distribute( 20, reagents["A1"], tc_plate.wells()[:8], new_tip="once", blow_out=True, blowout_location="source well", ) # Step 2: Add template DNA (5 µL per well, fresh tip each time) protocol.comment("Adding template DNA") for i in range(8): p20.transfer( 5, reagents.wells()[i + 1], tc_plate.wells()[i], new_tip="always", mix_after=(2, 10), ) # Step 3: 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() protocol.comment("PCR complete — 8 reactions in wells A1:H1")
Workflow 2: ELISA Serial Dilution with Multi-Channel Pipette
Goal: Use a multi-channel pipette to add diluent to columns 2-12, perform 2-fold serial dilutions across the plate, and add detection reagent to all wells in a single pass.
from opentrons import protocol_api metadata = { "protocolName": "ELISA Serial Dilution", "author": "Lab Automation", "apiLevel": "2.19", } def run(protocol: protocol_api.ProtocolContext): # Deck layout tips_300 = protocol.load_labware("opentrons_96_tiprack_300ul", "1") tips_300b = protocol.load_labware("opentrons_96_tiprack_300ul", "4") # extra rack 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_300, tips_300b]) # Define liquids diluent = protocol.define_liquid("Diluent", "PBS + 1% BSA", "#0077BB") sample = protocol.define_liquid("Sample", "Serum 1:10", "#EE7733") reservoir["A1"].load_liquid(diluent, volume=50000) reservoir["A2"].load_liquid(sample, volume=5000) # Step 1: Load column 1 with undiluted sample (all 8 rows at once) protocol.comment("Loading undiluted sample into column 1") multi.transfer(100, reservoir["A2"], plate.columns()[0], new_tip="once") # Step 2: Add diluent to columns 2-12 protocol.comment("Adding diluent to columns 2-12") multi.distribute( 100, reservoir["A1"], [col[0] for col in plate.columns()[1:]], # A2 through A12 (multi-channel reads full column) new_tip="once", disposal_volume=10, ) # Step 3: Serial dilution — transfer 100 µL from each column to the next, mix protocol.comment("Performing 2-fold serial dilution across columns 1→11") multi.transfer( 100, [col[0] for col in plate.columns()[:11]], # cols 1-11 as source [col[0] for col in plate.columns()[1:]], # cols 2-12 as destination mix_after=(5, 80), # mix 80 µL × 5 reps after each dispense new_tip="always", # fresh tip per column to avoid carry-over ) # Step 4: Remove 100 µL from column 12 to equalize volumes multi.pick_up_tip() multi.aspirate(100, plate.columns()[11][0]) multi.drop_tip() protocol.comment("ELISA serial dilution complete — 11 dilution steps, 12 columns") print("Protocol complete: 2-fold dilution series across 96-well plate")
Key Parameters
| Parameter | Module / Function | Default | Range / Options | Effect |
|---|---|---|---|---|
| , , | | , , | Controls tip change strategy; use to prevent cross-contamination |
| | | tuple | Aspirate/dispense in destination well after each dispense to homogenize |
| | | tuple | Aspirate/dispense in source well before each aspirate |
| | | , | Expel residual volume after dispense; set to control where |
| | | –pipette max µL | Insert air gap after aspirate to prevent dripping during robot moves |
| | | –pipette max µL | Extra volume drawn at start to improve dispense accuracy; discarded to trash |
| pipette property | varies by model | – µL/s | Aspirate speed; lower for viscous samples (glycerol, proteins > 5 mg/mL) |
| pipette property | varies by model | – µL/s | Dispense speed; lower for foaming or delicate cell suspensions |
| | — | – mm | Height of magnet tips above plate base; depends on bead/plate geometry |
| | — | – | Number of PCR thermal cycles |
Best Practices
-
Always simulate before running on hardware: Use
to catch labware name errors, tip shortages, volume overflows, and slot conflicts without consuming consumables or robot time.opentrons_simulate protocol.pyopentrons_simulate my_pcr_setup.py # Output shows all commands; errors printed with line numbers -
Prefer compound operations over manual pick-up/aspirate/dispense/drop sequences:
,transfer()
, anddistribute()
handle tip management, air gaps, and blow-out automatically. Reserve low-level calls for operations not supported by compound methods.consolidate() -
Count tips before running: Calculate total tip consumptions (each
transfer costs one tip per well pair). If tips exceed rack capacity, add additional racks tonew_tip="always"
.tip_racks=[]n_transfers = len(source_wells) # one tip per transfer tips_per_rack = 96 racks_needed = -(-n_transfers // tips_per_rack) # ceiling division print(f"Need {racks_needed} tip rack(s) for {n_transfers} transfers") -
Use
anddefine_liquid()
for setup validation: Liquid tracking in the Opentrons App displays color-coded wells with volumes, making it easy to verify correct reagent placement before pressing Run.load_liquid() -
Distinguish OT-2 slots from Flex slots in protocol files: OT-2 uses numeric strings (
through"1"
) while Flex uses grid coordinates ("11"
through"A1"
). Set"D3"
orrequirements = {"robotType": "Flex"}
to catch slot mismatches during simulation."OT-2" -
Adjust flow rates for difficult liquids: Viscous solutions (≥20% glycerol, PEG, protein > 5 mg/mL) require lower aspirate rates (25-50 µL/s). Reduce dispense speed for foaming samples to avoid bubble formation.
-
Use
for manual steps, notprotocol.pause()
:protocol.delay()
stops the robot and notifies the operator; the run resumes on demand.pause()
is for timed waits (incubations, module equilibration) where no human action is needed.delay()
Common Recipes
Recipe: Plate Replication (96-Well to 96-Well)
When to use: Duplicate an entire source plate into a destination plate with fresh tips per well.
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]) # Transfer all 96 wells in one call — pairwise source[i] → dest[i] p300.transfer(100, source.wells(), dest.wells(), new_tip="always") protocol.comment("Plate replicated: 96 wells transferred")
Recipe: Multi-Channel Column-by-Column Fill
When to use: Fill a 96-well plate column by column with a single reagent using a multi-channel pipette and one tip.
from opentrons import protocol_api metadata = {"protocolName": "Multi-Channel Fill", "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]) # distribute() with multi-channel: one pick-up, 12 dispenses across all columns multi.distribute( 100, reservoir["A1"], [plate.columns()[i][0] for i in range(12)], new_tip="once", disposal_volume=10, ) protocol.comment("96-well plate filled: 100 µL per well, single tip")
Recipe: Magnetic Bead Wash Loop
When to use: Automated bead-based cleanup (DNA extraction, IP assay) with repeating wash steps.
from opentrons import protocol_api metadata = {"protocolName": "Magnetic Bead Wash", "apiLevel": "2.19"} def run(protocol: protocol_api.ProtocolContext): mag_mod = protocol.load_module("magnetic module gen2", "4") bead_plate = mag_mod.load_labware("nest_96_wellplate_100ul_pcr_full_skirt") tips = protocol.load_labware("opentrons_96_tiprack_300ul", "1") reservoir = protocol.load_labware("nest_12_reservoir_15ml", "2") waste = protocol.load_labware("nest_12_reservoir_15ml", "5") p300 = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips]) # Engage magnets and remove supernatant mag_mod.engage(height_from_base=6) protocol.delay(seconds=120, msg="Beads pelleting on magnet") p300.transfer(90, bead_plate["A1"].bottom(z=0.5), waste["A1"], new_tip="once") # Wash loop (2 washes) for wash_num in range(2): mag_mod.disengage() protocol.comment(f"Wash {wash_num + 1} of 2") p300.transfer(100, reservoir["A1"], bead_plate["A1"], mix_after=(5, 80), new_tip="always") mag_mod.engage(height_from_base=6) protocol.delay(seconds=90) p300.transfer(100, bead_plate["A1"].bottom(z=0.5), waste["A2"], new_tip="always") # Elute mag_mod.disengage() elution_plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "3") p300.transfer(50, reservoir["A2"], bead_plate["A1"], mix_after=(10, 40), new_tip="always") mag_mod.engage(height_from_base=6) protocol.delay(seconds=120) p300.transfer(45, bead_plate["A1"].bottom(z=0.5), elution_plate["A1"], new_tip="always") mag_mod.disengage() protocol.comment("Bead cleanup complete: eluate in elution_plate A1")
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| Incorrect labware API name string | Look up exact names at labware.opentrons.com; names are case-sensitive (e.g., ) |
during run | Protocol needs more tips than racks provide | Add additional tip racks to ; or call if racks have been reloaded |
| Volume exceeds pipette max capacity | Trying to aspirate/dispense more than the pipette can hold | Use which auto-splits large volumes; switch to for large volumes (up to 1000 µL) |
| Labware placed in overlapping slots | Thermocycler auto-occupies slots 7-11; check output from simulation before running |
Simulation passes but robot fails with | Module not physically connected or wrong model string | Verify USB connection; use exact model strings: , , , |
| Inaccurate volumes, especially near pipette minimum | Pipette at edge of calibrated range or viscous liquid | Use a pipette whose optimal range covers your volume; pre-wet tips with before critical transfers; reduce flow rates |
on with well list length mismatch | Source and destination lists different lengths | Ensure source and destination lists are same length for pairwise transfer, or use a single source with a destination list for 1-to-many |
| OT-2 protocol errors on Flex with slot names | Robot type mismatch (numeric vs grid slots) | Set or to enforce slot naming; Flex slots are strings like , OT-2 slots are - |
Related Skills
- pylabrobot — hardware-agnostic Python API for Hamilton, Tecan, Beckman, and other vendors; use when protocols must run on non-Opentrons hardware
- protocolsio-integration — search and retrieve published wet-lab protocols from protocols.io to adapt into Opentrons Python protocols
- benchling-integration — connect protocol execution to Benchling ELN entries and sample registries
References
- Opentrons Protocol API v2 Documentation — official API reference covering all context methods, labware, and modules
- Opentrons Labware Library — searchable catalog of all supported labware with API name strings
- Protocol API Tutorial — step-by-step guide from metadata through hardware modules
- Opentrons GitHub Repository — source code, protocol examples, and issue tracker
- Opentrons Community Forum — community Q&A for protocol debugging and hardware questions