Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Opentrons Protocol API v2 for OT-2/Flex: Python protocols for pipetting, serial dilutions, PCR, plate replication; control thermocycler, heater-shaker, magnetic, temperature modules. Use pylabrobot for multi-vendor.
.claude/skills/jaechang-hits-opentrons-integration/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 136% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 151% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 224% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 498% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 209% | 0% |
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.
bashpip 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.
pythonfrom 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")
Every Opentrons protocol follows a required structure: metadata dict + run() function.
pythonfrom 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")
Load labware (plates, reservoirs, tip racks) onto deck slots and optionally onto adapters.
pythondef 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:
p20_single_gen2, p300_single_gen2, p1000_single_gen2, p20_multi_gen2, p300_multi_gen2p50_single_flex, p1000_single_flex, p50_multi_flex, p1000_multi_flexBasic, compound, and advanced liquid handling operations.
pythondef 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()
Navigate wells by name, index, row, or column. Control vertical position within wells.
pythondef 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
Control temperature, magnetic, heater-shaker, and thermocycler modules.
pythondef 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()
Pause, delay, comment, liquid tracking, and simulation detection.
pythondef 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
All Opentrons protocols are Python files with this required structure:
┌─ metadata dict ──────────────── protocolName, apiLevel, author
├─ requirements dict (optional) ── robotType
└─ def run(protocol): ─────────── All robot commandsThe 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.
| Feature | OT-2 | Flex | |---------|------|------| | Deck slots | 1-11 (numeric) | A1-D3 (grid) | | Pipettes | Gen2 (p20, p300, p1000) | Flex (p50, p1000, 96-channel) | | Max channels | 8-channel multi | 96-channel | | Modules | Gen1/Gen2 | V2 modules | | Adapters | Not supported | Supported (tiprack, flat) |
When using multi-channel pipettes, referencing a single well accesses the entire column:
pythonmulti = 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"])
pythonfrom 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", )
pythonfrom 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()
| Parameter | Function | Default | Range | Effect | |-----------|----------|---------|-------|--------| | volume | aspirate, dispense, transfer | — | 1–1000 µL | Liquid volume | | new_tip | transfer, distribute, consolidate | "always" | "always", "once", "never" | Tip change strategy | | mix_after | transfer | None | (reps, vol) tuple | Post-dispense mixing | | mix_before | transfer | None | (reps, vol) tuple | Pre-aspirate mixing | | blow_out | transfer | False | True/False | Blow out after dispense | | touch_tip | transfer | False | True/False | Touch tip after dispense | | air_gap | transfer | 0 | 0–pipette max µL | Air gap volume | | flow_rate.aspirate | pipette property | varies | 1–1000 µL/s | Aspirate speed | | flow_rate.dispense | pipette property | varies | 1–1000 µL/s | Dispense speed | | height_from_base | mag_module.engage | — | 0–20 mm | Magnet engagement height |
opentrons_simulate my_protocol.py before uploading to the robot. Catches labware conflicts, volume errors, and tip shortages without wasting consumables.transfer(), distribute(), consolidate() over manual pick_up_tip/aspirate/dispense/drop_tip sequences — they handle tip management automatically.define_liquid() and load_liquid() to enable volume tracking in the Opentrons App.requirements["robotType"] to ensure compatibility.protocol.pause(msg=...) is safer than protocol.delay() when you need user action (e.g., adding reagent, sealing plate).pythonfrom 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")
pythonfrom 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")
| Problem | Cause | Solution | |---------|-------|----------| | OutOfTipsError | Protocol needs more tips than available | Add multiple tip racks to tip_racks= list, or reload tips with pipette.reset_tipracks() | | Labware collision on deck | Two items assigned to overlapping slots | Check deck map — thermocycler auto-occupies multiple slots; use protocol.deck to inspect | | Volume exceeds pipette capacity | Attempting to aspirate/dispense > max volume | Use distribute() which auto-splits volumes, or switch to a larger pipette | | LabwareNotFoundError | 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 protocol.delay() between temperature changes; increase magnet engage time | | Inaccurate volumes | Pipette calibration or air bubbles | Recalibrate pipette; pre-wet tips with mix(); adjust flow rates for viscous liquids | | ModuleNotAttachedError | Module not connected or wrong model string | Verify module serial connection; use exact model strings ("temperature module gen2") |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 17,469 | 22,557 | +29% | 1 | 1 | 0% | 3,403 | 8,032 | +136% | 0 | 0 | — |
case-02 | fail→pass | 18,414 | 17,908 | -3% | 1 | 1 | 0% | 3,749 | 9,394 | +151% | 0 | 0 | — |
case-03 | fail→pass | 12,611 | 11,316 | -10% | 1 | 1 | 0% | 2,432 | 7,881 | +224% | 0 | 0 | — |
case-04 | pass→pass | 7,274 | 6,037 | -17% | 1 | 1 | 0% | 1,381 | 6,688 | +384% | 0 | 0 | — |
case-05 | fail→pass | 6,017 | 6,705 | +11% | 1 | 1 | 0% | 1,132 | 6,765 | +498% | 0 | 0 | — |
case-06 | pass→pass | 6,094 | 5,071 | -17% | 1 | 1 | 0% | 1,186 | 6,455 | +444% | 0 | 0 | — |
case-07 | pass→pass | 6,565 | 4,462 | -32% | 1 | 1 | 0% | 1,218 | 6,349 | +421% | 0 | 0 | — |
case-08 | fail→pass | 11,579 | 5,906 | -49% | 1 | 1 | 0% | 2,102 | 6,491 | +209% | 0 | 0 | — |
case-13 | pass→pass | 3,463 | 3,183 | -8% | 1 | 1 | 0% | 564 | 5,934 | +952% | 0 | 0 | — |
case-09 | pass→pass | 9,179 | 5,923 | -35% | 1 | 1 | 0% | 1,666 | 6,647 | +299% | 0 | 0 | — |
case-10 | pass→pass | 6,945 | 9,020 | +30% | 1 | 1 | 0% | 1,298 | 7,230 | +457% | 0 | 0 | — |
case-11 | pass→pass | 8,249 | 4,804 | -42% | 1 | 1 | 0% | 1,571 | 6,420 | +309% | 0 | 0 | — |
case-12 | pass→pass | 8,754 | 4,715 | -46% | 1 | 1 | 0% | 1,457 | 6,140 | +321% | 0 | 0 | — |
case-14 | pass→pass | 10,722 | 9,602 | -10% | 1 | 1 | 0% | 2,111 | 7,453 | +253% | 0 | 0 | — |
case-15 | pass→pass | 9,572 | 8,361 | -13% | 1 | 1 | 0% | 1,877 | 7,159 | +281% | 0 | 0 | — |
case-16 | pass→pass | 8,183 | 3,801 | -54% | 1 | 1 | 0% | 1,428 | 6,026 | +322% | 0 | 0 | — |
case-17 | pass→pass | 10,464 | 7,291 | -30% | 1 | 1 | 0% | 2,019 | 7,008 | +247% | 0 | 0 | — |
case-18 | pass→pass | 5,550 | 4,213 | -24% | 1 | 1 | 0% | 1,082 | 6,191 | +472% | 0 | 0 | — |
case-19 | pass→pass | 3,713 | 2,944 | -21% | 1 | 1 | 0% | 584 | 5,887 | +908% | 0 | 0 | — |
case-20 | pass→pass | 27,946 | 13,141 | -53% | 1 | 1 | 0% | 3,838 | 7,936 | +107% | 0 | 0 | — |
case-21 | pass→pass | 15,593 | 10,562 | -32% | 1 | 1 | 0% | 2,487 | 7,217 | +190% | 0 | 0 | — |
case-22 | pass→pass | 18,984 | 17,194 | -9% | 1 | 1 | 0% | 3,638 | 8,888 | +144% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +23 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.