mcpbeat

Opentrons Integration

jaechang-hits/opentrons-integration

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.

4k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
294
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/jaechang-hits/SciAgent-Skills --skill opentrons-integration

The instruction itself

28 sections, as written by the author

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", 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

Key Concepts

Protocol File Structure

All Opentrons protocols are Python files with this required structure:

┌─ metadata dict ──────────────── protocolName, apiLevel, author
├─ requirements dict (optional) ── robotType
└─ def run(protocol): ─────────── All robot commands

The 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.

OT-2 vs Flex Differences

| 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) |

Multi-Channel Pipette Behavior

When using multi-channel pipettes, referencing a single well accesses the entire column:

multi = 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"])

Common Workflows

Workflow: Serial Dilution

from 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",
    )

Workflow: PCR Setup with Thermocycler

from 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()

Workflow: Magnetic Bead Cleanup

  • Load magnetic module with deep-well plate, reservoir with wash buffers
  • Engage magnets → aspirate supernatant → dispense to waste
  • Disengage magnets → add wash buffer → mix → engage → remove wash (repeat 2x)
  • Disengage → add elution buffer → mix → engage → transfer eluate to clean plate

Key Parameters

| 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 |

Best Practices

  • Always simulate first: Run opentrons_simulate my_protocol.py before uploading to the robot. Catches labware conflicts, volume errors, and tip shortages without wasting consumables.
  • Use compound operations over basic: Prefer transfer(), distribute(), consolidate() over manual pick_up_tip/aspirate/dispense/drop_tip sequences — they handle tip management automatically.
  • Anti-pattern — ignoring tip count: A protocol that runs out of tips will error mid-run. Count total tip uses vs rack capacity before running.
  • Track liquids for setup validation: Use define_liquid() and load_liquid() to enable volume tracking in the Opentrons App.
  • Anti-pattern — hardcoding deck slots across robot types: Flex uses grid coordinates (A1-D3), OT-2 uses numbers (1-11). Write separate metadata or use requirements["robotType"] to ensure compatibility.
  • Control flow rates for difficult liquids: Reduce aspirate speed for viscous solutions (glycerol, PEG), increase for water-like liquids.
  • Use pauses for manual intervention: protocol.pause(msg=...) is safer than protocol.delay() when you need user action (e.g., adding reagent, sealing plate).

Common Recipes

Recipe: Plate Replication

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])
    p300.transfer(100, source.wells(), dest.wells(), new_tip="always")

Recipe: Reagent Distribution with Multi-Channel

from 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")

Troubleshooting

| 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") |

  • pylabrobot — multi-vendor lab automation (Hamilton, Beckman, Tecan) for cross-platform protocols
  • biopython-molecular-biology — sequence design and primer tools for PCR protocol inputs

References

How to use it

Copy the folder

Take jaechang-hits/opentrons-integration from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.

Install what it needs

The instructions reference pip. Without those the skill loads but fails at the first command.