In an effort to improve the functionality and reliability of the OT-2, we have rolled out a new version of the API: API Version 2! This version has all of the features available in version 1, with added support for new products. 

Here’s a side-by-side comparison of the protocols written in both versions. We will dive into more details below. 

Metadata and Version Selection

You must include a metadata dictionary at the top of the protocol. The only required element is apiLevel , which defines the major and minor version of the Python Protocol API for which your protocol is designed. You can also specify the protocolName , author , and description  in the metadata, and you would be able to see these information in the App once you upload the protocol.

from opentrons import protocol_api

metadata = {'apiLevel': '2.0',
            'protocolName': 'Your Protocol Name',
            'author': 'Your Name',
            'description': 'Your protocol description'}

def run(protocol: protocol_api.ProtocolContext):

Run Function and Import Statement

One of the most obvious differences between the versions is the introduction of the Run function in APIv2. This function must be present in your protocol and take exactly one mandatory argument. 

from opentrons import protocol_api

def run(protocol: protocol_api.ProtocolContext):
    # the rest of your code goes here

The argument that is passed in the Run function is an instance of the ProtocolContext class. You can name the argument anything you would like, but we suggest protocol  because it actually represents the protocol that would be executed by the robot.

Note the import statement is optional. The addition of the import statement could allow your text editor to provide you with autocompletion. But if this is not a concern, you can simply write your protocols this way:

def run(protocol):
    # your code


As always, you still need to define the labware required for your protocol. In APIv2, you do this by calling the method on your protocol  object, such as

protocol.load_labware('labware_name', slot_number)

Do you know about our Labware Library? It holds information of all of our default and validated labware. Just copy and paste the API name of the appropriate labware in your protocol.


Similar to labware, pipettes are defined using a new method:

protocol.load_instrument('pipette_name', mount, tip_racks)

Here is a list of valid pipette names:

  • p10_single 

  • p10_multi

  • p300_single 

  • p300_multi

  • p1000_single 

  • p20_single_gen2 

  • p20_multi_gen2

  • p300_single_gen2 

  • p300_multi_gen2

  • p1000_single_gen2 

Were you ever frustrated because you could not easily assign the same tip rack for your two pipettes? You can do that now in APIv2! The tip tracking function is more powerful than ever and can keep track of how many tips are left in each column of the tip rack.


The protocol commands are mostly very similar to that of APIv1. Here are the two main categories of our protocol-related commands:

  1. Building Block Commands

  2. Complex Commands

They are parallel to the Atomic and Complex Liquid Handling Commands in version 1 for the most part. Though you might notice some behavioral changes for certain commands:

No More Optional Arguments!

In APIv1, you could use positional arguments to specify either volume or location (and repetitions for mix). We have removed this feature in APIv2. This means you must add in all of the missing arguments for each command, or else it would raise an error.


The pipette now moves above liquid level and resets the plunger at the top of the well between each dispense and the following aspirate during mix. This helps prevent the pipette from over-aspirating when mixing.

Move To

In APIv1, pipette.move_to(well)  would move the pipette to the top of the specified well. In APIv2, you must state the location of the well, such as pipette.move_to(  or it will cause an error.

Utility Commands

Previously in APIv1, we used  pipette.delay()  and robot.pause() , which could cause some confusion. We have fixed this inconsistency in APIv2 to make our API more intuitive by making these utility commands ProtocolContext  methods.

protocol.pause("Pause the protocol and await for user to resume.")

protocol.delay(minutes=1, seconds=3) # pause for 1 minute, 3 seconds 

Read more about other Utility Commands in our docs here here.

Hardware Modules

Rather than passing the argument share=True  in labware.load(), labware are loaded directly to the module object using load_labware() .

Below are some examples on how to use the hardware modules with the new API.

Magnetic Module

mag_mod = protocol.load_module('Magnetic Module', 4)
mag_plate = mag_mod.load_labware(

mag_mod.engage()     # raise magnetic stage
mag_mod.disengage()  # lower magnetic stage

Temperature Module

temp_mod = protocol.load_module('Temperature Module', 7)
temp_plate = temp_mod.load_labware(

temp_mod.set_temperature(4)  # set target temperature to 4°C
temp_mod.wait_for_temp()     # halt protocol until 4°C is reached
temp_mod.deactivate()        # stop cooling

You can read the target and current real-temperature of the module via the following:

Thermocycler Module

Our Thermocycler Module is only supported in APIv2. If your workflow requires a thermocycler, you now have the option to automate the thermocycling steps:

tc_mod = protocol.load_module('Thermocycler Module')
tc_plate = tc_mod.load_labware(

# lid motion control

tc_mod.set_lid_temperature(105)  # set lid temperature

tc_mod.set_block_temperature(4)   # set aluminum block temperature
tc_mod.set_block_temperature(10,  # inside the thermocycler


Read more about Thermocycler Module commands here.

Did this answer your question?