protocol-authoring

star 504

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 By Opentrons schedule Updated 2/24/2026

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:

  1. Don't ask unnecessary questions. Pick reasonable labware, volumes, and pipettes. Just produce a valid, working protocol.
  2. 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.
  3. Always define liquids with protocol.define_liquid() and well.load_liquid() for all source wells.
  4. 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 plain transfer/distribute/consolidate only for OT-2 or when the user explicitly asks.
  5. Default to Flex unless the user specifies OT-2.
  6. Use the latest API version unless the user specifies otherwise. Look up MAX_SUPPORTED_VERSION in api/src/opentrons/protocols/api_support/definitions.py to get the current value.
  7. Default liquid class: water. Use glycerol_50 or ethanol_80 if the protocol context calls for viscous or volatile liquids.
  8. Reasonable defaults: flex_1channel_1000 pipette, opentrons_flex_96_tiprack_1000ul tip rack, nest_96_wellplate_2ml_deep plate, 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

  1. requirements dictrobotType ("Flex" or "OT-2") and apiLevel
  2. def run(protocol): — entry point receiving ProtocolContext
  3. 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")
  • version1
  • metadata.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_simulate cannot accept RTP files. Protocols with CSV RTPs must be verified with opentrons analyze. See the protocol-verification skill.

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 apiLevel in both metadata and requirements
  • Using numeric slots on Flex or alpha on OT-2
  • Exceeding pipette volume range
  • Using transfer with new_tip="never" without calling pick_up_tip() first
  • Forgetting to define_liquid / load_liquid for source wells
  • Using plain transfer on Flex when transfer_with_liquid_class is available
  • Calling apr.initialize() without apr.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 set group_wells=False
  • Using f-strings or variable references in metadata dict — the parser requires static literals only (no f"...", no {var}, no function calls)
  • Wrapping transfer() in a for loop over wells — transfer() handles iteration internally; pass lists instead
  • Using 8-channel pipette with wells() instead of columns() — 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 use start="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
Install via CLI
npx skills add https://github.com/Opentrons/opentrons --skill protocol-authoring
Repository Details
star Stars 504
call_split Forks 204
navigation Branch main
article Path SKILL.md
More from Creator