name: protocol-authoring description: Create valid Opentrons Python API protocols for OT-2 and Flex robots. Use when creating, writing, editing, or helping with protocol files, liquid handling automation, or Opentrons protocol development. Also use when debugging protocol errors to trace into API source code.
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