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.
npx skills add https://github.com/jaechang-hits/SciAgent-Skills --skill opentrons-integration
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.
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.
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")
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")
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:
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.
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()
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
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()
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
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.
| 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:
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"])
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",
)
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()
| 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).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")
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")
| 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") |
Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).
Automatically creates user-facing changelogs from git commits by analyzing commit history, categorizing changes, and transforming technical commits into clear, customer-friendly release notes. Turns hours of manual changelog writing into minutes of automated generation.
Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).
React Native and Expo best practices for building performant mobile apps. Use when building React Native components, optimizing list performance, implementing animations, or working with native modules. Triggers on tasks involving React Native, Expo, mobile performance, or native platform APIs.
React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling
Use when starting feature work that needs isolation from current workspace or before executing implementation plans - creates isolated git worktrees with smart directory selection and safety verification
Take jaechang-hits/opentrons-integration from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
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.
The instructions reference pip.
Without those the skill loads but fails at the first command.