Opentrons Protocol Authoring
Behavior Defaults — READ FIRST
This skill is primarily used by developers, SDETs, and QA who need protocols for testing and development. Follow these defaults unless the user explicitly says otherwise:
- Don't ask unnecessary questions. Pick reasonable labware, volumes, and pipettes. Just produce a valid, working protocol.
- Keep protocols minimal. Use the fewest steps needed to demonstrate the requested behavior. Don't generate dozens of repeated transfers — 2–4 operations are enough to validate a feature.
- Always define liquids with
protocol.define_liquid()andwell.load_liquid()for all source wells. - Default to liquid class functions (
transfer_with_liquid_class,distribute_with_liquid_class,consolidate_with_liquid_class) on Flex with API >= 2.24. Fall back to plaintransfer/distribute/consolidateonly for OT-2 or when the user explicitly asks. - Default to Flex unless the user specifies OT-2.
- Use the latest API version unless the user specifies otherwise. Look up
MAX_SUPPORTED_VERSIONinapi/src/opentrons/protocols/api_support/definitions.pyto get the current value. - Default liquid class:
water. Useglycerol_50orethanol_80if the protocol context calls for viscous or volatile liquids. - Reasonable defaults:
flex_1channel_1000pipette,opentrons_flex_96_tiprack_1000ultip rack,nest_96_wellplate_2ml_deepplate, 100 µL transfer volume.
Quick Start — Flex Protocol (Default)
from opentrons import protocol_api
metadata = {
"protocolName": "Liquid Class Transfer Demo",
"author": "Opentrons",
"description": "Minimal transfer using liquid classes",
}
requirements = {"robotType": "Flex", "apiLevel": "<MAX_SUPPORTED_VERSION>"}
# ^^^ Replace <MAX_SUPPORTED_VERSION> with the value from
# api/src/opentrons/protocols/api_support/definitions.py
def run(protocol: protocol_api.ProtocolContext) -> None:
trash = protocol.load_trash_bin("A3")
tiprack = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "D2")
source_plate = protocol.load_labware("nest_96_wellplate_2ml_deep", "D1")
dest_plate = protocol.load_labware("nest_96_wellplate_2ml_deep", "C1")
pipette = protocol.load_instrument(
"flex_1channel_1000", mount="left", tip_racks=[tiprack]
)
# Define and load liquids
sample = protocol.define_liquid(
name="Sample", description="Aqueous sample", display_color="#0088FF"
)
source_plate["A1"].load_liquid(liquid=sample, volume=500)
source_plate["A2"].load_liquid(liquid=sample, volume=500)
# Use liquid class transfer (default: water)
water = protocol.get_liquid_class(name="water")
pipette.transfer_with_liquid_class(
liquid_class=water,
volume=100,
source=[source_plate["A1"], source_plate["A2"]],
dest=[dest_plate["A1"], dest_plate["A2"]],
new_tip="always",
)
Quick Start — OT-2 Protocol
from opentrons import protocol_api
metadata = {
"protocolName": "OT-2 Transfer Demo",
"author": "Opentrons",
"description": "Minimal transfer for OT-2",
}
requirements = {"robotType": "OT-2", "apiLevel": "<MAX_SUPPORTED_VERSION>"}
def run(protocol: protocol_api.ProtocolContext) -> None:
tiprack = protocol.load_labware("opentrons_96_tiprack_300ul", "1")
source_plate = protocol.load_labware("nest_96_wellplate_2ml_deep", "2")
dest_plate = protocol.load_labware("nest_96_wellplate_2ml_deep", "3")
pipette = protocol.load_instrument(
"p300_single_gen2", mount="left", tip_racks=[tiprack]
)
sample = protocol.define_liquid(
name="Sample", description="Aqueous sample", display_color="#0088FF"
)
source_plate["A1"].load_liquid(liquid=sample, volume=500)
pipette.transfer(100, source_plate["A1"], dest_plate["A1"])
Required Elements
requirementsdict —robotType("Flex"or"OT-2") andapiLeveldef run(protocol):— entry point receivingProtocolContext- Flex only: must load trash bin or waste chute before any
drop_tip
metadata dict is optional but recommended. apiLevel goes in metadata OR requirements, not both.
Look up the current max API version from MAX_SUPPORTED_VERSION in api/src/opentrons/protocols/api_support/definitions.py. Flex requires >= 2.15.
Liquid Classes (Default for Flex)
Available liquid classes (Flex, API >= 2.24):
| Name | Type | When to Use |
|---|---|---|
water |
Aqueous | Default for most protocols |
glycerol_50 |
Viscous | Viscous samples, glycerol solutions |
ethanol_80 |
Volatile | Ethanol, volatile solvents |
water = protocol.get_liquid_class(name="water")
# Transfer (1-to-1)
pipette.transfer_with_liquid_class(
liquid_class=water, volume=100,
source=[plate["A1"]], dest=[plate["B1"]],
new_tip="always",
)
# Distribute (1-to-many)
pipette.distribute_with_liquid_class(
liquid_class=water, volume=50,
source=reservoir["A1"], dest=plate.rows()[0][:4],
new_tip="once",
)
# Consolidate (many-to-1)
pipette.consolidate_with_liquid_class(
liquid_class=water, volume=50,
source=plate.rows()[0][:4], dest=reservoir["A1"],
new_tip="once",
)
Defining Liquids (Always Do This)
sample = protocol.define_liquid(
name="Sample", description="Aqueous sample", display_color="#0088FF"
)
buffer = protocol.define_liquid(
name="Buffer", description="Wash buffer", display_color="#00CC66"
)
reagent = protocol.define_liquid(
name="Reagent", description="Reaction reagent", display_color="#FF4444"
)
source_plate["A1"].load_liquid(liquid=sample, volume=500)
reservoir["A1"].load_liquid(liquid=buffer, volume=10000)
Common display colors: #0088FF (blue/sample), #00CC66 (green/buffer), #FF4444 (red/reagent), #FFB800 (yellow/media), #9933FF (purple/enzyme), #FF6B35 (orange/beads).
OT-2 vs Flex Key Differences
| Feature | OT-2 | Flex |
|---|---|---|
| Deck slots | 1–11 (numeric) | A1–D4 (alphanumeric) |
| Trash | Fixed (slot 12) | Must call load_trash_bin() |
| Liquid classes | Not supported | get_liquid_class() (API 2.24+) |
| Gripper | N/A | move_labware(lw, dest, use_gripper=True) |
| 96-channel | N/A | flex_96channel_1000 |
Flex Deck Layout
1 2 3 4 (staging)
A [ A1 ] [ A2 ] [ A3 ] [ A4 ]
B [ B1 ] [ B2 ] [ B3 ] [ B4 ]
C [ C1 ] [ C2 ] [ C3 ] [ C4 ]
D [ D1 ] [ D2 ] [ D3 ] [ D4 ]
OT-2 Deck Layout
10 11 12(trash)
7 8 9
4 5 6
1 2 3
Pipettes
Flex
| Name | Channels | Range |
|---|---|---|
flex_1channel_50 |
1 | 1–50 µL |
flex_1channel_200 |
1 | 1–200 µL |
flex_1channel_1000 |
1 | 5–1000 µL |
flex_8channel_50 |
8 | 1–50 µL |
flex_8channel_200 |
8 | 1–200 µL |
flex_8channel_1000 |
8 | 5–1000 µL |
flex_96channel_1000 |
96 | 5–1000 µL |
OT-2
| Name | Channels | Range |
|---|---|---|
p20_single_gen2 |
1 | 1–20 µL |
p300_single_gen2 |
1 | 20–300 µL |
p1000_single_gen2 |
1 | 100–1000 µL |
p20_multi_gen2 |
8 | 1–20 µL |
p300_multi_gen2 |
8 | 20–300 µL |
Common Labware
Flex tip racks: opentrons_flex_96_tiprack_50ul, opentrons_flex_96_tiprack_200ul, opentrons_flex_96_tiprack_1000ul
OT-2 tip racks: opentrons_96_tiprack_20ul, opentrons_96_tiprack_300ul, opentrons_96_tiprack_1000ul
Plates: nest_96_wellplate_2ml_deep, corning_96_wellplate_360ul_flat, opentrons_96_wellplate_200ul_pcr_full_skirt, nest_96_wellplate_200ul_flat
Reservoirs: nest_12_reservoir_15ml, nest_1_reservoir_195ml, nest_1_reservoir_290ml
Tube racks: opentrons_24_tuberack_nest_1.5ml_snapcap, opentrons_6_tuberack_nest_50ml_conical
Modules Quick Reference
temp_mod = protocol.load_module("temperature module gen2", "D1")
tc = protocol.load_module("thermocycler module gen2") # A1+B1 on Flex
hs = protocol.load_module("heaterShakerModuleV1", "C1")
mag_block = protocol.load_module("magneticBlockV1", "C1") # Flex only
mag_mod = protocol.load_module("magnetic module gen2", "1") # OT-2 only
apr = protocol.load_module("absorbanceReaderV1", "B3") # Flex, API 2.21+
stacker = protocol.load_module("flexStackerModuleV1", "D4") # Flex, API 2.25+
For detailed module operations, see reference-modules.md.
Runtime Parameters (API 2.18+)
def add_parameters(parameters: protocol_api.Parameters) -> None:
parameters.add_int(variable_name="sample_count", display_name="Samples",
default=8, minimum=1, maximum=96)
parameters.add_bool(variable_name="dry_run", display_name="Dry Run", default=False)
def run(protocol: protocol_api.ProtocolContext) -> None:
count = protocol.params.sample_count
For complete RTP guide, see reference-rtp.md.
Working Directories (Monorepo Root)
All local dev artifacts live in these gitignored directories:
| Directory | Purpose |
|---|---|
tmp-protocols/ |
Protocol .py files |
tmp-custom-labware/ |
Custom labware .json definitions |
tmp-csv/ |
CSV files for RTP inputs |
Custom Labware
Custom labware JSON files go in tmp-custom-labware/. The parameters.loadName in the JSON is the string passed to load_labware().
Creating a Custom Labware Definition
The easiest starting point is copying an existing definition from shared-data/labware/definitions/2/<name>/<version>.json and modifying the key fields:
{
"namespace": "custom",
"version": 1,
"parameters": {
"loadName": "my_custom_plate"
},
"metadata": {
"displayName": "My Custom Plate"
}
...
}
Required changes when deriving from an existing definition:
parameters.loadName→ your unique load name (no spaces, underscores OK)namespace→"custom"(must not be"opentrons")version→1metadata.displayName→ human-readable name
Save as tmp-custom-labware/<loadName>.json (file name convention matches loadName).
Using Custom Labware in a Protocol
plate = protocol.load_labware("my_custom_plate", "D1")
No special import needed — the CLI handles loading the definition at run time.
For a proper custom labware definition from scratch, use the Opentrons Labware Creator
CSV Runtime Parameters
CSV files go in tmp-csv/. They are used exclusively via the add_csv_file RTP type (API 2.20+).
Defining a CSV Parameter
def add_parameters(parameters: protocol_api.Parameters) -> None:
parameters.add_csv_file(
variable_name="transfer_map",
display_name="Transfer Map",
description="CSV with columns: source_well, dest_well, volume_ul",
)
Using the CSV in run()
rows = protocol.params.transfer_map.parse_as_csv()
# rows is a list of lists; rows[0] is the header row
for row in rows[1:]:
src, dst, vol = row[0].strip(), row[1].strip(), float(row[2].strip())
pipette.transfer(vol, source[src], dest[dst])
Example CSV (tmp-csv/transfer_map.csv)
source_well,dest_well,volume_ul
A1,A1,100
A2,A2,150
A3,A3,75
Note:
opentrons_simulatecannot accept RTP files. Protocols with CSV RTPs must be verified withopentrons analyze. See theprotocol-verificationskill.
Additional References
Skill Reference Files (in this directory)
| File | When to use |
|---|---|
| reference-liquid-handling.md | Detailed liquid handling patterns, tip math, transfer anti-patterns |
| reference-modules.md | Module load names, operations, Flex Stacker, APR |
| reference-rtp.md | Runtime parameters — all types, CSV RTPs |
| reference-source-map.md | Source code navigation for debugging |
| reference-labware-deck.md | Common labware load names, deck layout rules (Flex + OT-2), OT-2→Flex migration |
| reference-96channel.md | 96-channel pipette constraints, nozzle configs, tip adapter rules |
| reference-examples-index.md | Index of AI server example docs — what each covers and when to read it |
AI Server Source Docs (read on demand)
Located in opentrons-ai-server/api/storage/docs/. Use reference-examples-index.md to decide which file to read. Do not read all of them — they total ~10,000 lines. Read only what the current task needs.
| File | Contents |
|---|---|
full-examples.md |
Complete production protocols (PCR, reagent transfer, HS) |
casual_examples.md |
Casual NL → protocol mappings, pooling, triplicates |
serial_dilution_examples.md |
Serial dilution patterns (single/multi-channel, row/column-wise) |
pcr_protocols_with_csv.md |
PCR + CSV RTP well mapping, thermocycler profiles |
transfer_function_notes.md |
transfer() deep dive — loops, tip behavior, modules |
out_of_tips_error_219.md |
Tip math, multi-channel capacity, index error prevention |
commands-v0.0.1.md |
Common command patterns and pitfalls |
standard-loadname-info.md |
Full labware catalog (86 items) |
96-channel-pipette.md |
Full 96-channel guide (see reference-96channel.md for summary) |
deck_layout.md |
Full deck rules (see reference-labware-deck.md for summary) |
OT2ToFlex.md |
Full migration guide (see reference-labware-deck.md for summary) |
transfer_with_liquid_class.md |
Liquid class transfer differences and custom properties |
flex_stacker_usage.md |
Flex Stacker patterns (see reference-modules.md for summary) |
runtime_parameters.md |
RTP examples (see reference-rtp.md for summary) |
Keeping This Skill Current
Update this skill whenever you discover something new. These files are the team's shared knowledge base — stale information hurts everyone.
| Trigger | What to update |
|---|---|
| A new API method, parameter, or behavior is used | Add it to the relevant section in SKILL.md or the appropriate reference-*.md |
| A bug or constraint is found via source inspection | Add it to reference-source-map.md under the relevant debugging section |
MAX_SUPPORTED_VERSION changes |
Check api/src/opentrons/protocols/api_support/definitions.py and add any new API-version-gated features to the skill |
| A new labware load name is used | Add it to the Common Labware list |
A new liquid class becomes available in shared-data/liquid-class/definitions/ |
Add it to the Liquid Classes table |
| Actual behavior differs from what this skill says | Correct the skill, not just the protocol |
| A new module is supported | Add it to the Modules section and reference-modules.md |
How to update: use the Write or StrReplace tools on the relevant skill file. Keep edits focused — fix only what changed. Don't rewrite sections that are still accurate.
Common Mistakes
- Forgetting
load_trash_bin()on Flex - Using OT-2 pipette names on Flex or vice versa
- Putting
apiLevelin bothmetadataandrequirements - Using numeric slots on Flex or alpha on OT-2
- Exceeding pipette volume range
- Using
transferwithnew_tip="never"without callingpick_up_tip()first - Forgetting to
define_liquid/load_liquidfor source wells - Using plain
transferon Flex whentransfer_with_liquid_classis available - Calling
apr.initialize()withoutapr.close_lid()first (APR lid must be closed before init) - Passing only the top well (e.g.
plate["A1"]) to 8-channel*_with_liquid_class— must pass full column or setgroup_wells=False - Using f-strings or variable references in
metadatadict — the parser requires static literals only (nof"...", no{var}, no function calls) - Wrapping
transfer()in aforloop over wells —transfer()handles iteration internally; pass lists instead - Using 8-channel pipette with
wells()instead ofcolumns()— 8-channel picks up an entire column at once - Not accounting for 8-channel tip math: one
pick_up_tip()= 8 tips; a single 96-well rack supports only 12 column operations - Loading a 96-channel pipette without
adapter="opentrons_flex_96_tiprack_adapter"for full (ALL) tip pickup - Using
start="A1"for 96-channel COLUMN mode — always usestart="A12"to avoid deck edge collision - Placing a tube rack in a staging area slot (A4–D4) — gripper cannot safely move tube racks
- Loading the Heater-Shaker or Temperature Module in column 2 slots (A2, B2, C2, D2) — forbidden on Flex