Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Python API v2 for Opentrons OT-2/Flex liquid handlers: protocols as Python files with metadata and run(); control pipettes, labware, and modules (thermocycler, heater-shaker, magnetic, temperature). Simulate via opentrons_simulate then upload. Use PyLabRobot for vendor-agnostic scripts (Hamilton, Tecan).
.claude/skills/jaechang-hits-opentrons-protocol-api/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 229% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 693% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 627% | 0% |
| case-21 | ✗→✓ | ▲ Improved | 291% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 228% | 0% |
The Opentrons Protocol API v2 lets you write liquid handling protocols as plain Python files that run on OT-2 or Flex robots. Every protocol defines a metadata dictionary, an optional requirements dictionary, and a run(protocol) function. The ProtocolContext object passed to run() exposes all deck setup, pipette operations, module control, and utility methods. Protocols can be simulated on any computer with opentrons_simulate before uploading to the robot through the Opentrons App or HTTP API.
PyLabRobot instead when writing protocols that must run on Hamilton STAR, Tecan Freedom EVO, or other vendors without Opentrons-specific hardware; for Opentrons-only workflows the native Protocol API provides tighter integration and module support.protocolsio-integration to search protocols.io alongside this skill.opentronsopentrons_simulate ships with the package for local testingbashpip install opentrons # Verify installation and simulate a protocol locally opentrons_simulate my_protocol.py
A minimal protocol showing all required elements — metadata, labware, instrument, and a transfer:
pythonfrom opentrons import protocol_api metadata = { "protocolName": "Simple Reagent Distribution", "author": "Lab Automation Team", "apiLevel": "2.19", } def run(protocol: protocol_api.ProtocolContext): # Load labware onto deck slots tips = protocol.load_labware("opentrons_96_tiprack_300ul", "1") source = protocol.load_labware("nest_12_reservoir_15ml", "2") plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "3") # Load pipette and attach tip rack pipette = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips]) # Distribute 50 µL from reservoir A1 to first 12 wells using one tip pipette.distribute(50, source["A1"], plate.wells()[:12], new_tip="once") protocol.comment("Distribution complete")
bash# Simulate locally — no robot needed opentrons_simulate simple_reagent_distribution.py
Every protocol requires a metadata dict specifying at minimum apiLevel. The optional requirements dict sets the target robot type. All labware and instruments are loaded through the ProtocolContext.
pythonfrom opentrons import protocol_api # Minimum required metadata metadata = { "protocolName": "My Assay Protocol", "author": "Jane Smith <jane@lab.org>", "description": "96-well assay setup with temperature control", "apiLevel": "2.19", } # Optional: target a specific robot type (Flex or OT-2) requirements = {"robotType": "OT-2", "apiLevel": "2.19"} def run(protocol: protocol_api.ProtocolContext): # OT-2: slots numbered 1-11 in a 3×4 grid tips_300 = protocol.load_labware("opentrons_96_tiprack_300ul", "1") tips_20 = protocol.load_labware("opentrons_96_tiprack_20ul", "4") source = protocol.load_labware("nest_12_reservoir_15ml", "2", label="Buffer Reservoir") plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "3") tube_rack = protocol.load_labware("opentrons_24_tuberack_nest_1.5ml_snapcap", "5") # Load both pipettes (optional: one or two mounts) p300 = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips_300]) p20 = protocol.load_instrument("p20_single_gen2", "right", tip_racks=[tips_20]) print(f"Deck has {len(protocol.deck)} slots; pipettes: {[p300.name, p20.name]}")
OT-2 deck layout (3 columns × 4 rows, numbered left-to-right, bottom-to-top):
Slot map (OT-2): Slot map (Flex, A-D rows, 1-3 cols):
10 | 11 | Trash D1 | D2 | D3
7 | 8 | 9 C1 | C2 | C3
4 | 5 | 6 B1 | B2 | B3
1 | 2 | 3 A1 | A2 | A3Common OT-2 pipette names: p20_single_gen2, p300_single_gen2, p1000_single_gen2, p20_multi_gen2, p300_multi_gen2. Common Flex pipette names: p50_single_flex, p1000_single_flex, p50_multi_flex, p1000_multi_flex, flex_96channel_1000.
Low-level aspirate/dispense/blow-out operations for precise step-by-step control.
pythondef run(protocol: protocol_api.ProtocolContext): tips = protocol.load_labware("opentrons_96_tiprack_300ul", "1") source = protocol.load_labware("nest_12_reservoir_15ml", "2") dest = protocol.load_labware("corning_96_wellplate_360ul_flat", "3") p300 = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips]) p300.pick_up_tip() # Aspirate and dispense — basic liquid movement p300.aspirate(100, source["A1"]) # draw 100 µL from reservoir p300.dispense(100, dest["A1"]) # expel into plate well # Air gap to prevent dripping during transport p300.aspirate(80, source["A2"]) p300.air_gap(20) # draw 20 µL air to cap the tip p300.dispense(100, dest["A2"]) # dispenses liquid + air # Mix in place (repetitions, volume) p300.mix(3, 60, dest["A1"]) # mix 60 µL × 3 times # Remove exterior droplets / expel residual p300.touch_tip(dest["A1"]) # wipe tip on well rim p300.blow_out(dest["A1"].top()) # expel last drop at top p300.drop_tip() protocol.comment("Low-level operations complete")
pythondef run(protocol: protocol_api.ProtocolContext): tips = protocol.load_labware("opentrons_96_tiprack_300ul", "1") p300 = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips]) # Adjust flow rates (µL/s) for viscous or sensitive samples p300.flow_rate.aspirate = 50 # slow down for viscous liquids (default ~150) p300.flow_rate.dispense = 150 # default dispense speed p300.flow_rate.blow_out = 300 # fast blow-out for complete expulsion print(f"Aspirate rate: {p300.flow_rate.aspirate} µL/s")
transfer(), distribute(), and consolidate() handle tip management automatically and accept mix, blow-out, and air-gap options.
pythondef run(protocol: protocol_api.ProtocolContext): tips = protocol.load_labware("opentrons_96_tiprack_300ul", "1") source = protocol.load_labware("corning_96_wellplate_360ul_flat", "2") dest = protocol.load_labware("corning_96_wellplate_360ul_flat", "3") p300 = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips]) # transfer(): one source → one destination, with optional per-well tip changes p300.transfer( 100, source["A1"], dest["A1"], new_tip="always", # options: "always", "once", "never" mix_after=(3, 50), # mix 50 µL × 3 reps after each dispense blow_out=True, touch_tip=True, ) # transfer() with lists: pairwise source-destination mapping sources = source.wells()[:8] dests = dest.wells()[:8] p300.transfer(75, sources, dests, new_tip="always") # distribute(): one source → many destinations (single tip, multi-dispense) p300.distribute( 50, source["A1"], dest.wells()[:12], new_tip="once", # use one tip for all destinations disposal_volume=10, # extra volume drawn to ensure accuracy ) # consolidate(): many sources → one destination (collect, then dispense) p300.consolidate( 50, source.wells()[:8], dest["A1"], mix_after=(3, 100), ) print("Compound transfer operations complete")
Load labware from the library, navigate wells by name/row/column, and define liquids for visual tracking in the Opentrons App.
pythondef run(protocol: protocol_api.ProtocolContext): plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "1") p300 = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[protocol.load_labware("opentrons_96_tiprack_300ul", "2")]) # Access wells by alphanumeric name well_a1 = plate["A1"] # Access all wells (column-major order: A1, B1, C1, ..., H1, A2, ...) all_wells = plate.wells() print(f"Total wells: {len(all_wells)}") # 96 # Access by row (8 rows, A-H; each row has 12 wells) row_a = plate.rows()[0] # [A1, A2, ..., A12] row_b = plate.rows()[1] # [B1, B2, ..., B12] # Access by column (12 columns, 1-12; each column has 8 wells) col_1 = plate.columns()[0] # [A1, B1, C1, D1, E1, F1, G1, H1] # Vertical position control within a well p300.pick_up_tip() p300.aspirate(80, well_a1.bottom(z=1)) # 1 mm above well bottom p300.dispense(80, well_a1.top(z=-2)) # 2 mm below well top p300.aspirate(80, well_a1.center()) # geometric center p300.drop_tip()
pythondef run(protocol: protocol_api.ProtocolContext): reservoir = protocol.load_labware("nest_12_reservoir_15ml", "1") plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "2") # Define liquids for visual tracking in Opentrons App pbs = protocol.define_liquid(name="1× PBS", description="Phosphate buffered saline", display_color="#0077BB") sample = protocol.define_liquid(name="Sample", description="Cell lysate, 1 mg/mL protein", display_color="#EE7733") # Assign liquids to wells with known starting volumes (µL) reservoir["A1"].load_liquid(liquid=pbs, volume=10000) reservoir["A2"].load_liquid(liquid=sample, volume=5000) # Mark destination wells as empty for well in plate.wells(): well.load_empty() print("Liquids defined and assigned")
Control temperature, magnetic, thermocycler, and heater-shaker modules. Each module is loaded by its model name string and occupies specific deck slots.
pythondef run(protocol: protocol_api.ProtocolContext): # --- Temperature Module (Gen2) --- temp_mod = protocol.load_module("temperature module gen2", "3") temp_plate = temp_mod.load_labware("corning_96_wellplate_360ul_flat") temp_mod.set_temperature(celsius=4) # blocks until target reached print(f"Temp module: {temp_mod.temperature}°C") # temp_mod.deactivate() # turn off at end # --- Magnetic Module (Gen2) --- mag_mod = protocol.load_module("magnetic module gen2", "6") mag_plate = mag_mod.load_labware("nest_96_wellplate_100ul_pcr_full_skirt") mag_mod.engage(height_from_base=10) # raise magnets 10 mm from plate base protocol.delay(seconds=300) # hold beads for 5 min mag_mod.disengage() # --- Heater-Shaker Module --- hs_mod = protocol.load_module("heaterShakerModuleV1", "1") hs_plate = hs_mod.load_labware("corning_96_wellplate_360ul_flat") hs_mod.close_labware_latch() hs_mod.set_target_temperature(celsius=37) hs_mod.wait_for_temperature() hs_mod.set_and_wait_for_shake_speed(rpm=500) protocol.delay(minutes=30) hs_mod.deactivate_shaker() hs_mod.deactivate_heater() hs_mod.open_labware_latch() print("Heater-shaker cycle complete")
pythondef run(protocol: protocol_api.ProtocolContext): # --- Thermocycler Module (Gen2) --- # Auto-occupies slots 7-11 on OT-2; no slot argument needed tc_mod = protocol.load_module("thermocyclerModuleV2") tc_plate = tc_mod.load_labware("nest_96_wellplate_100ul_pcr_full_skirt") tc_mod.open_lid() tc_mod.set_lid_temperature(celsius=105) # pre-heat lid to prevent condensation # Initial denaturation tc_mod.set_block_temperature(95, hold_time_seconds=180) # PCR cycling profile profile = [ {"temperature": 95, "hold_time_seconds": 15}, # denaturation {"temperature": 60, "hold_time_seconds": 30}, # annealing {"temperature": 72, "hold_time_seconds": 30}, # extension ] tc_mod.execute_profile(steps=profile, repetitions=35, block_max_volume=25) # Final extension and hold tc_mod.set_block_temperature(72, hold_time_minutes=5) tc_mod.set_block_temperature(4) # hold at 4°C indefinitely tc_mod.deactivate_lid() tc_mod.open_lid() print("PCR complete; plate held at 4°C")
Pause for user interaction, log comments visible in the app, control rail lights, and detect simulation mode.
pythondef run(protocol: protocol_api.ProtocolContext): # Pause and prompt the user (robot stops, app shows message) protocol.pause(msg="Add 10 µL of enzyme to tube A1, then resume") # Timed delay (robot waits without user action) protocol.delay(seconds=30, msg="Waiting 30s for reaction incubation") protocol.delay(minutes=5) # Log a comment visible in Opentrons App run log protocol.comment("Starting serial dilution — columns 1 to 11") # Rail lights for visual status indication protocol.set_rail_lights(True) # lights on protocol.set_rail_lights(False) # lights off # Home all axes (useful after an error or before finishing) protocol.home() # Detect simulation vs. physical run — skip slow waits in simulation if protocol.is_simulating(): protocol.comment("Running in simulation mode — skipping 10-min incubation") else: protocol.delay(minutes=10) # Load waste bin (Flex only — OT-2 uses fixed trash) # trash = protocol.load_trash_bin("A3") print("Protocol control features demonstrated")
Goal: Transfer master mix from a tube rack into a PCR plate on the thermocycler, add template DNA from individual samples, then run a complete PCR cycling program.
pythonfrom opentrons import protocol_api metadata = { "protocolName": "PCR Setup and Run", "author": "Lab Automation", "apiLevel": "2.19", } def run(protocol: protocol_api.ProtocolContext): # Hardware setup tc_mod = protocol.load_module("thermocyclerModuleV2") tc_plate = tc_mod.load_labware("nest_96_wellplate_100ul_pcr_full_skirt") tips_300 = protocol.load_labware("opentrons_96_tiprack_300ul", "1") tips_20 = protocol.load_labware("opentrons_96_tiprack_20ul", "4") reagents = protocol.load_labware("opentrons_24_tuberack_nest_1.5ml_snapcap", "2") p300 = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips_300]) p20 = protocol.load_instrument("p20_single_gen2", "right", tip_racks=[tips_20]) # Define liquids master_mix = protocol.define_liquid("Master Mix", "2× PCR master mix", "#33BBEE") template = protocol.define_liquid("Template", "gDNA 10 ng/µL", "#EE3377") reagents["A1"].load_liquid(master_mix, volume=500) for i in range(8): reagents.wells()[i + 1].load_liquid(template, volume=50) # Step 1: Open lid and distribute master mix (20 µL per well, 8 wells) tc_mod.open_lid() protocol.comment("Distributing master mix") p300.distribute( 20, reagents["A1"], tc_plate.wells()[:8], new_tip="once", blow_out=True, blowout_location="source well", ) # Step 2: Add template DNA (5 µL per well, fresh tip each time) protocol.comment("Adding template DNA") for i in range(8): p20.transfer( 5, reagents.wells()[i + 1], tc_plate.wells()[i], new_tip="always", mix_after=(2, 10), ) # Step 3: Run PCR tc_mod.close_lid() tc_mod.set_lid_temperature(105) tc_mod.set_block_temperature(95, hold_time_seconds=180) # initial denaturation profile = [ {"temperature": 95, "hold_time_seconds": 15}, {"temperature": 60, "hold_time_seconds": 30}, {"temperature": 72, "hold_time_seconds": 30}, ] tc_mod.execute_profile(steps=profile, repetitions=35, block_max_volume=25) tc_mod.set_block_temperature(72, hold_time_minutes=5) # final extension tc_mod.set_block_temperature(4) # hold tc_mod.deactivate_lid() tc_mod.open_lid() protocol.comment("PCR complete — 8 reactions in wells A1:H1")
Goal: Use a multi-channel pipette to add diluent to columns 2-12, perform 2-fold serial dilutions across the plate, and add detection reagent to all wells in a single pass.
pythonfrom opentrons import protocol_api metadata = { "protocolName": "ELISA Serial Dilution", "author": "Lab Automation", "apiLevel": "2.19", } def run(protocol: protocol_api.ProtocolContext): # Deck layout tips_300 = protocol.load_labware("opentrons_96_tiprack_300ul", "1") tips_300b = protocol.load_labware("opentrons_96_tiprack_300ul", "4") # extra rack reservoir = protocol.load_labware("nest_12_reservoir_15ml", "2") plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "3") multi = protocol.load_instrument("p300_multi_gen2", "left", tip_racks=[tips_300, tips_300b]) # Define liquids diluent = protocol.define_liquid("Diluent", "PBS + 1% BSA", "#0077BB") sample = protocol.define_liquid("Sample", "Serum 1:10", "#EE7733") reservoir["A1"].load_liquid(diluent, volume=50000) reservoir["A2"].load_liquid(sample, volume=5000) # Step 1: Load column 1 with undiluted sample (all 8 rows at once) protocol.comment("Loading undiluted sample into column 1") multi.transfer(100, reservoir["A2"], plate.columns()[0], new_tip="once") # Step 2: Add diluent to columns 2-12 protocol.comment("Adding diluent to columns 2-12") multi.distribute( 100, reservoir["A1"], [col[0] for col in plate.columns()[1:]], # A2 through A12 (multi-channel reads full column) new_tip="once", disposal_volume=10, ) # Step 3: Serial dilution — transfer 100 µL from each column to the next, mix protocol.comment("Performing 2-fold serial dilution across columns 1→11") multi.transfer( 100, [col[0] for col in plate.columns()[:11]], # cols 1-11 as source [col[0] for col in plate.columns()[1:]], # cols 2-12 as destination mix_after=(5, 80), # mix 80 µL × 5 reps after each dispense new_tip="always", # fresh tip per column to avoid carry-over ) # Step 4: Remove 100 µL from column 12 to equalize volumes multi.pick_up_tip() multi.aspirate(100, plate.columns()[11][0]) multi.drop_tip() protocol.comment("ELISA serial dilution complete — 11 dilution steps, 12 columns") print("Protocol complete: 2-fold dilution series across 96-well plate")
| Parameter | Module / Function | Default | Range / Options | Effect | |-----------|-------------------|---------|-----------------|--------| | new_tip | transfer, distribute, consolidate | "always" | "always", "once", "never" | Controls tip change strategy; use "always" to prevent cross-contamination | | mix_after | transfer | None | (repetitions, volume) tuple | Aspirate/dispense in destination well after each dispense to homogenize | | mix_before | transfer | None | (repetitions, volume) tuple | Aspirate/dispense in source well before each aspirate | | blow_out | transfer | False | True, False | Expel residual volume after dispense; set blowout_location to control where | | air_gap | transfer | 0 | 0–pipette max µL | Insert air gap after aspirate to prevent dripping during robot moves | | disposal_volume | distribute | 0 | 0–pipette max µL | Extra volume drawn at start to improve dispense accuracy; discarded to trash | | flow_rate.aspirate | pipette property | varies by model | 1–1000 µL/s | Aspirate speed; lower for viscous samples (glycerol, proteins > 5 mg/mL) | | flow_rate.dispense | pipette property | varies by model | 1–1000 µL/s | Dispense speed; lower for foaming or delicate cell suspensions | | height_from_base | mag_mod.engage() | — | 0–20 mm | Height of magnet tips above plate base; depends on bead/plate geometry | | repetitions | tc_mod.execute_profile() | — | 1–99 | Number of PCR thermal cycles |
opentrons_simulate protocol.py to catch labware name errors, tip shortages, volume overflows, and slot conflicts without consuming consumables or robot time.bash opentrons_simulate my_pcr_setup.py # Output shows all commands; errors printed with line numbers
transfer(), distribute(), and consolidate() handle tip management, air gaps, and blow-out automatically. Reserve low-level calls for operations not supported by compound methods.new_tip="always" transfer costs one tip per well pair). If tips exceed rack capacity, add additional racks to tip_racks=[].python n_transfers = len(source_wells) # one tip per transfer tips_per_rack = 96 racks_needed = -(-n_transfers // tips_per_rack) # ceiling division print(f"Need {racks_needed} tip rack(s) for {n_transfers} transfers")
define_liquid() and load_liquid() for setup validation: Liquid tracking in the Opentrons App displays color-coded wells with volumes, making it easy to verify correct reagent placement before pressing Run."1" through "11") while Flex uses grid coordinates ("A1" through "D3"). Set requirements = {"robotType": "Flex"} or "OT-2" to catch slot mismatches during simulation.protocol.pause() for manual steps, not protocol.delay(): pause() stops the robot and notifies the operator; the run resumes on demand. delay() is for timed waits (incubations, module equilibration) where no human action is needed.When to use: Duplicate an entire source plate into a destination plate with fresh tips per well.
pythonfrom opentrons import protocol_api metadata = {"protocolName": "Plate Replication", "apiLevel": "2.19"} def run(protocol: protocol_api.ProtocolContext): tips = protocol.load_labware("opentrons_96_tiprack_300ul", "1") source = protocol.load_labware("corning_96_wellplate_360ul_flat", "2") dest = protocol.load_labware("corning_96_wellplate_360ul_flat", "3") p300 = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips]) # Transfer all 96 wells in one call — pairwise source[i] → dest[i] p300.transfer(100, source.wells(), dest.wells(), new_tip="always") protocol.comment("Plate replicated: 96 wells transferred")
When to use: Fill a 96-well plate column by column with a single reagent using a multi-channel pipette and one tip.
pythonfrom opentrons import protocol_api metadata = {"protocolName": "Multi-Channel Fill", "apiLevel": "2.19"} def run(protocol: protocol_api.ProtocolContext): tips = protocol.load_labware("opentrons_96_tiprack_300ul", "1") reservoir = protocol.load_labware("nest_12_reservoir_15ml", "2") plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "3") multi = protocol.load_instrument("p300_multi_gen2", "left", tip_racks=[tips]) # distribute() with multi-channel: one pick-up, 12 dispenses across all columns multi.distribute( 100, reservoir["A1"], [plate.columns()[i][0] for i in range(12)], new_tip="once", disposal_volume=10, ) protocol.comment("96-well plate filled: 100 µL per well, single tip")
When to use: Automated bead-based cleanup (DNA extraction, IP assay) with repeating wash steps.
pythonfrom opentrons import protocol_api metadata = {"protocolName": "Magnetic Bead Wash", "apiLevel": "2.19"} def run(protocol: protocol_api.ProtocolContext): mag_mod = protocol.load_module("magnetic module gen2", "4") bead_plate = mag_mod.load_labware("nest_96_wellplate_100ul_pcr_full_skirt") tips = protocol.load_labware("opentrons_96_tiprack_300ul", "1") reservoir = protocol.load_labware("nest_12_reservoir_15ml", "2") waste = protocol.load_labware("nest_12_reservoir_15ml", "5") p300 = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips]) # Engage magnets and remove supernatant mag_mod.engage(height_from_base=6) protocol.delay(seconds=120, msg="Beads pelleting on magnet") p300.transfer(90, bead_plate["A1"].bottom(z=0.5), waste["A1"], new_tip="once") # Wash loop (2 washes) for wash_num in range(2): mag_mod.disengage() protocol.comment(f"Wash {wash_num + 1} of 2") p300.transfer(100, reservoir["A1"], bead_plate["A1"], mix_after=(5, 80), new_tip="always") mag_mod.engage(height_from_base=6) protocol.delay(seconds=90) p300.transfer(100, bead_plate["A1"].bottom(z=0.5), waste["A2"], new_tip="always") # Elute mag_mod.disengage() elution_plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "3") p300.transfer(50, reservoir["A2"], bead_plate["A1"], mix_after=(10, 40), new_tip="always") mag_mod.engage(height_from_base=6) protocol.delay(seconds=120) p300.transfer(45, bead_plate["A1"].bottom(z=0.5), elution_plate["A1"], new_tip="always") mag_mod.disengage() protocol.comment("Bead cleanup complete: eluate in elution_plate A1")
| Problem | Cause | Solution | |---------|-------|----------| | LabwareNotFoundError: [labware name] | Incorrect labware API name string | Look up exact names at labware.opentrons.com; names are case-sensitive (e.g., "corning_96_wellplate_360ul_flat") | | OutOfTipsError during run | Protocol needs more tips than racks provide | Add additional tip racks to tip_racks=[]; or call pipette.reset_tipracks() if racks have been reloaded | | Volume exceeds pipette max capacity | Trying to aspirate/dispense more than the pipette can hold | Use distribute() which auto-splits large volumes; switch to p1000_single_gen2 for large volumes (up to 1000 µL) | | DeckConflictError | Labware placed in overlapping slots | Thermocycler auto-occupies slots 7-11; check protocol.deck output from simulation before running | | Simulation passes but robot fails with ModuleNotAttachedError | Module not physically connected or wrong model string | Verify USB connection; use exact model strings: "temperature module gen2", "magnetic module gen2", "thermocyclerModuleV2", "heaterShakerModuleV1" | | Inaccurate volumes, especially near pipette minimum | Pipette at edge of calibrated range or viscous liquid | Use a pipette whose optimal range covers your volume; pre-wet tips with mix() before critical transfers; reduce flow rates | | TypeError on transfer() with well list length mismatch | Source and destination lists different lengths | Ensure source and destination lists are same length for pairwise transfer, or use a single source with a destination list for 1-to-many | | OT-2 protocol errors on Flex with slot names | Robot type mismatch (numeric vs grid slots) | Set requirements = {"robotType": "Flex"} or "OT-2" to enforce slot naming; Flex slots are strings like "A1", OT-2 slots are "1"-"11" |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 20,602 | 28,220 | +37% | 1 | 1 | 0% | 4,202 | 13,804 | +229% | 0 | 0 | — |
case-02 | pass→pass | 19,361 | 17,907 | -8% | 1 | 1 | 0% | 4,072 | 13,374 | +228% | 0 | 0 | — |
case-03 | pass→pass | 9,617 | 11,889 | +24% | 1 | 1 | 0% | 1,757 | 11,584 | +559% | 0 | 0 | — |
case-04 | pass→pass | 11,751 | 17,937 | +53% | 1 | 1 | 0% | 2,231 | 11,261 | +405% | 0 | 0 | — |
case-05 | pass→pass | 11,526 | 14,287 | +24% | 1 | 1 | 0% | 2,173 | 12,052 | +455% | 0 | 0 | — |
case-06 | pass→pass | 5,625 | 5,572 | -1% | 1 | 1 | 0% | 1,026 | 10,527 | +926% | 0 | 0 | — |
case-16 | pass→pass | 48,885 | 5,449 | -89% | 1 | 1 | 0% | 1,326 | 10,649 | +703% | 0 | 0 | — |
case-07 | pass→pass | 6,743 | 4,372 | -35% | 1 | 1 | 0% | 1,298 | 10,269 | +691% | 0 | 0 | — |
case-08 | fail→pass | 7,001 | 12,205 | +74% | 1 | 1 | 0% | 1,391 | 11,030 | +693% | 0 | 0 | — |
case-09 | pass→pass | 8,041 | 9,743 | +21% | 1 | 1 | 0% | 1,684 | 11,566 | +587% | 0 | 0 | — |
case-10 | pass→pass | 8,849 | 9,267 | +5% | 1 | 1 | 0% | 1,895 | 11,444 | +504% | 0 | 0 | — |
case-11 | pass→pass | 8,699 | 8,684 | -0% | 1 | 1 | 0% | 1,541 | 11,246 | +630% | 0 | 0 | — |
case-12 | pass→pass | 5,782 | 7,344 | +27% | 1 | 1 | 0% | 1,075 | 11,013 | +924% | 0 | 0 | — |
case-13 | pass→pass | 6,057 | 4,565 | -25% | 1 | 1 | 0% | 1,220 | 10,475 | +759% | 0 | 0 | — |
case-14 | pass→pass | 6,163 | 6,213 | +1% | 1 | 1 | 0% | 1,338 | 10,787 | +706% | 0 | 0 | — |
case-15 | pass→pass | 12,469 | 28,138 | +126% | 1 | 1 | 0% | 2,668 | 11,044 | +314% | 0 | 0 | — |
case-17 | pass→pass | 7,186 | 6,955 | -3% | 1 | 1 | 0% | 1,474 | 10,886 | +639% | 0 | 0 | — |
case-18 | fail→pass | 7,605 | 8,153 | +7% | 1 | 1 | 0% | 1,532 | 11,135 | +627% | 0 | 0 | — |
case-19 | pass→pass | 5,497 | 9,408 | +71% | 1 | 1 | 0% | 1,113 | 11,477 | +931% | 0 | 0 | — |
case-20 | pass→pass | 8,646 | 6,688 | -23% | 1 | 1 | 0% | 1,686 | 10,895 | +546% | 0 | 0 | — |
case-21 | fail→pass | 14,642 | 10,701 | -27% | 1 | 1 | 0% | 2,981 | 11,648 | +291% | 0 | 0 | — |
case-22 | pass→pass | 16,171 | 15,446 | -4% | 1 | 1 | 0% | 3,375 | 13,103 | +288% | 0 | 0 | — |
case-23 | pass→pass | 5,341 | 5,194 | -3% | 1 | 1 | 0% | 1,048 | 10,507 | +903% | 0 | 0 | — |
case-24 | pass→pass | 8,643 | 6,441 | -25% | 1 | 1 | 0% | 1,863 | 10,883 | +484% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 24 cases were attempted. The headline lift of +17 percentage points is the difference between those two pass rates over the 24 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.