skills/opentrons-integration/references/migration-api-2-19-to-2-29.md
This guide updates protocols written around robot software 7.3.1 and Protocol API 2.19 to the 2026-07-23 baseline:
opentrons==9.1.1.opentrons==9.0.0, followed by analysis
in the current OT-2 App.Do not mechanically change only the API string. Newer levels can change command validation and behavior.
API 2.29 is not supported on OT-2 at this baseline.
# Flex
requirements = {"robotType": "Flex", "apiLevel": "2.29"}
# OT-2
requirements = {"robotType": "OT-2", "apiLevel": "2.28"}
Use one apiLevel declaration. Older files often put it in both metadata and
requirements; current analysis rejects that.
Before:
metadata = {"apiLevel": "2.19", "protocolName": "Example"}
requirements = {"robotType": "Flex", "apiLevel": "2.19"}
After:
metadata = {"protocolName": "Example"}
requirements = {"robotType": "Flex", "apiLevel": "2.29"}
If the protocol must remain compatible with older robot software, keep 2.19 and apply only changes available at that level.
Current Flex load names describe channels and range:
| Old or incorrect pattern | Current choice |
|---|---|
p50_single_flex | flex_1channel_50 |
p50_multi_flex | flex_8channel_50 |
p1000_single_flex | flex_1channel_1000 |
p1000_multi_flex | flex_8channel_1000 |
p300_single_flex | No direct equivalent; choose flex_1channel_50 or flex_1channel_1000 from validated volume needs |
p300_multi_flex | No direct equivalent; choose flex_8channel_50 or flex_8channel_1000 |
Also available:
flex_96channel_200 — API 2.25+.flex_96channel_1000.Do not choose solely by the largest transfer. Check every operation against the pipette's lower and upper range and compatible tip capacities.
OT-2 GEN2 names remain p20_*_gen2, p300_*_gen2, and
p1000_single_gen2.
Flex API 2.16+ protocols should load the fixture actually installed:
trash = protocol.load_trash_bin("A3")
Or:
chute = protocol.load_waste_chute()
OT-2 keeps its fixed trash in slot 12 and does not call load_trash_bin().
If both a trash bin and waste chute exist, set the intended pipette trash
container or pass the documented trash_location to complex commands.
Well.load_liquid() is deprecated, and Well.load_empty() does not exist in
the current package.
Before:
reservoir["A1"].load_liquid(liquid=buffer, volume=10_000)
plate["A1"].load_empty()
After:
reservoir.load_liquid(
wells=["A1"],
volume=10_000,
liquid=buffer,
)
plate.load_empty(wells=["A1"])
For varying volumes:
plate.load_liquid_by_well(
volumes={"A1": 20, "B1": 30},
liquid=sample,
)
ProtocolContext.load_labware_on_adapter() is not a current method.
Load the adapter, then call the adapter's method:
adapter = protocol.load_adapter(
"opentrons_96_well_aluminum_block",
"D1",
)
plate = adapter.load_labware(
"opentrons_96_wellplate_200ul_pcr_full_skirt"
)
Some load_labware() calls also accept an adapter= load name for supported
stacks. Use the pattern shown for the exact hardware in current documentation.
The powered Magnetic Module is OT-2-only.
Old Flex pattern:
magnetic_module = protocol.load_module(
"magnetic module gen2",
"C2",
)
magnetic_module.engage(height_from_base=6.5)
Current Flex pattern:
magnetic_block = protocol.load_module("magneticBlockV1", "C2")
protocol.move_labware(
labware=plate,
new_location=magnetic_block,
use_gripper=True,
)
protocol.delay(minutes=5)
protocol.move_labware(
labware=plate,
new_location="B2",
use_gripper=True,
)
The Magnetic Block is passive and has no engage() or disengage() method.
The reader was added in API 2.21 and is Flex-only. It does not accept
read(wavelengths=[...]).
Incorrect:
result = plate_reader.read(wavelengths=[450, 650])
Correct sequence:
reader = protocol.load_module("absorbanceReaderV1", "D3")
reader.close_lid()
reader.initialize(mode="multi", wavelengths=[450, 650])
reader.open_lid()
protocol.move_labware(
labware=plate,
new_location=reader,
use_gripper=True,
)
reader.close_lid()
result = reader.read(export_filename="absorbance")
The plate reader returns zeros during simulation. Avoid divide-by-zero logic in the simulation branch.
Do not preserve options merely because an old reference listed them.
For example, gradient=(start, end) is not a supported generic transfer()
option in the current API. Build a validated volume list explicitly:
volumes = [10, 20, 30, 40]
pipette.transfer(
volume=volumes,
source=reservoir["A1"],
dest=plate.wells()[:4],
new_tip="always",
)
Check uncommon options against the exact current method and API level. The supported options for standard and liquid-class commands are not identical.
mix() cycle.Well.load_liquid() deprecated.transfer_with_liquid_class(), distribute_with_liquid_class(), and
consolidate_with_liquid_class().set_empty() tip-rack state.touch_tip() use in large spaces.Check for:
API analysis has improved, so a newly raised deck-conflict error may reveal an old protocol assumption that was never physically safe.
Do not assume newer pipetting behavior produces assay-equivalent results.
opentrons==9.1.1 for Flex or
opentrons==9.0.0 for OT-2.Do not claim a migration is equivalent solely from a successful simulation.