agentleFS
jkitchinjkitchin/knowhow/skills/laboratory/opentrons-absorbance-reader/SKILL.md

opentrons-absorbance-reader

Skill42 starsChanged 18 days agoMITView on GitHub

Opentrons Absorbance Plate Reader Module for Flex - on-deck microplate spectrophotometry with single/multi-wavelength reading (380-740nm in 10nm increments), automated lid control, and CSV data export for ELISA, cell growth, and colorimetric assays

The file

Show its rules and text

We split this skill into the rules it gives an agent

20 rules. About 0 are common, 14 are distinctive, and 6 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈14

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use the Absorbance Reader skill when:

    line 19
  2. Very specific:

    Critical: Always call close_lid() before initialize(), even if lid is already closed.

    line 106
  3. Distinctive:

    Read up to 6 wavelengths simultaneously:

    line 144
  4. Distinctive:

    Protect column 4: - Never attempt to load labware in staging area

    line 480
  5. Distinctive:

    Check gripper compatibility: - Ensure plates have grippable geometry

    line 482
  6. Distinctive:

    Verify API version is 2.21 or later

    line 552
  7. Very specific:

    Check robotType: "Flex" in requirements

    line 553
  8. Distinctive:

    Ensure module is in column 3 slot (A3-D3)

    line 554
  9. Very specific:

    Always call close_lid() before initialize()

    line 557
  10. Distinctive:

    Ensure column 4 staging area is clear

    line 558
  11. Distinctive:

    Check gripper is functioning properly

    line 559
  12. Very specific:

    Verify lid is closed before read()

    line 562
  13. Distinctive:

    Check plate is on module

    line 563
  14. Very specific:

    Ensure wavelengths match those set in initialize()

    line 564
  15. Distinctive:

    Use correct well names ("A1" not "a1")

    line 568
  16. Distinctive:

    Check data type (dict, not list)

    line 569
  17. Distinctive:

    Verify plate has grippable geometry

    line 572
  18. Distinctive:

    Ensure reader lid is open before moving plate onto/off module

    line 573
  19. Distinctive:

    Check plate is compatible with reader (96-well plates)

    line 574
  20. Very specific:

    File naming: Use descriptive export_filename parameter for easy identification

    line 633

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Opentrons Absorbance Plate Reader Module

Overview

The Absorbance Plate Reader Module is an on-deck microplate spectrophotometer exclusively for Opentrons Flex. It measures light absorbance in 96-well plates at up to 6 wavelengths simultaneously, enabling automated ELISA, cell density measurements, colorimetric assays, and kinetic readings without manual plate transfers.

Core value: Eliminate manual plate reader transfers. Read absorbance mid-protocol to make real-time decisions or collect kinetic data with automated timing and liquid handling.

Platform: Opentrons Flex only (not compatible with OT-2)

When to Use

Use the Absorbance Reader skill when:

  • Running ELISA or other colorimetric assays requiring absorbance reading
  • Measuring bacterial or cell culture growth (OD600)
  • Performing kinetic assays with timed readings
  • Collecting multi-wavelength absorbance data
  • Protocols requiring real-time decision-making based on absorbance values
  • Automating plate-based biochemical assays

Don't use when:

  • Working with OT-2 robot (module is Flex-only)
  • Need wavelengths outside 380-740nm range
  • Require fluorescence or luminescence measurements

Quick Reference

OperationMethodKey Parameters
Load moduleprotocol.load_module()"absorbanceReaderV1", "D3" (or A3-C3)
Close lidclose_lid()-
Open lidopen_lid()-
Check lid statusis_lid_on()Returns bool
Initialize readerinitialize()mode, wavelengths, reference_wavelength
Read plateread()export_filename (optional)

Available wavelengths: 380-740nm in 10nm increments (e.g., 450nm, 562nm, 600nm, 650nm)

Platform Requirements

Opentrons Flex only

  • API version: 2.21 or later
  • Compatible deck slots: A3, B3, C3, D3 (column 3 only)
  • Staging area: Column 4 (entire column reserved for lid storage)

Important: Cannot load any labware in column 4 (A4, B4, C4, D4) when absorbance reader is present.

Deck Layout

Column 3 (Reader)  Column 4 (Staging - Reserved)
┌─────────────┐    ┌─────────────┐
│     A3      │───▶│  A4 (Lid)   │
│   Reader    │    │  Reserved   │
│   Module    │    │             │
└─────────────┘    └─────────────┘

Reader occupies column 3 - Detection unit and plate holder Column 4 reserved - Lid staging area (gripper moves lid here when open)

Loading the Module

from opentrons import protocol_api

metadata = {'apiLevel': '2.21'}
requirements = {"robotType": "Flex", "apiLevel": "2.21"}

def run(protocol: protocol_api.ProtocolContext):
    # Load Absorbance Reader in column 3
    reader = protocol.load_module("absorbanceReaderV1", "D3")

    # Note: Column 4 is now unavailable for labware

Compatible slots: A3, B3, C3, D3 Recommended: D3 (bottom-right position for easy gripper access)

Lid Control

The gripper manages lid position automatically:

# Close lid (required before initialization)
reader.close_lid()

# Check lid status
if reader.is_lid_on():
    protocol.comment("Lid is on detection unit")

# Open lid (moves to staging area in column 4)
reader.open_lid()

Lid positions:

  • Closed: Lid covers detection unit on module
  • Open: Lid stored in staging area (column 4)

Critical: Always call close_lid() before initialize(), even if lid is already closed.

Initialization

The initialize() method configures the reader with measurement parameters.

Single Wavelength Reading

Simplest mode - read at one wavelength:

# Initialize for single wavelength
reader.close_lid()
reader.initialize(mode="single", wavelengths=[450])
reader.open_lid()

Use case: OD600 bacterial growth, single-color ELISA

Single Wavelength with Reference

Normalize readings against a reference wavelength:

# Initialize with reference wavelength
reader.close_lid()
reader.initialize(
    mode="single",
    wavelengths=[450],
    reference_wavelength=562
)
reader.open_lid()

Use case: Reduce background noise, normalize for plate artifacts

Multi-Wavelength Reading

Read up to 6 wavelengths simultaneously:

# Initialize for multiple wavelengths
reader.close_lid()
reader.initialize(
    mode="multi",
    wavelengths=[450, 562, 600, 650]
)
reader.open_lid()

Use case: Multi-analyte assays, spectral analysis, dual-wavelength ELISA

Available wavelengths: 380-740nm in 10nm increments. Common choices:

  • 450nm - Blue (common ELISA substrate TMB)
  • 562nm - Green (BCA protein assay)
  • 600nm - Orange (bacterial growth OD600)
  • 650nm - Red (alternative reference wavelength)

Maximum: 6 wavelengths per reading

Reading Plates

Basic Reading

# Move plate to reader with gripper
protocol.move_labware(assay_plate, reader, use_gripper=True)

# Close lid
reader.close_lid()

# Read plate
absorbance_data = reader.read()

# Open lid
reader.open_lid()

# Move plate off reader
protocol.move_labware(assay_plate, "C1", use_gripper=True)

Reading with CSV Export

# Read and export data
absorbance_data = reader.read(export_filename="experiment_001_plate1")

# CSV file saved to robot and accessible via Opentrons App

Export format: CSV file with plate layout, metadata (wavelengths, serial number, timestamps)

Accessing Reading Results

Results are returned as nested dictionary: {wavelength: {well: absorbance}}

# Read plate at 450nm
data = reader.read()

# Access specific well at 450nm
absorbance_a1 = data[450]["A1"]

protocol.comment(f"Well A1 absorbance at 450nm: {absorbance_a1}")

# Iterate through all wells at 600nm
for well_name, absorbance in data[600].items():
    protocol.comment(f"{well_name}: {absorbance}")

Value range: 0.0 - 4.0 OD (optical density)

Multi-Wavelength Data Access

# Initialize with multiple wavelengths
reader.initialize(mode="multi", wavelengths=[450, 600])

# Read plate
data = reader.read()

# Access different wavelengths
od450_a1 = data[450]["A1"]
od600_a1 = data[600]["A1"]

protocol.comment(f"A1: OD450={od450_a1}, OD600={od600_a1}")

# Calculate ratio
if od600_a1 > 0:
    ratio = od450_a1 / od600_a1
    protocol.comment(f"450/600 ratio: {ratio}")

Data Formats

In-Protocol Dictionary Format

{
    450: {
        "A1": 0.123,
        "A2": 0.145,
        "A3": 0.167,
        # ... all 96 wells
        "H12": 0.089
    },
    600: {
        "A1": 0.456,
        # ... all 96 wells
    }
}

Structure:

  • Top level: Wavelengths (450, 562, 600, 650)
  • Second level: Well names ("A1" - "H12")
  • Values: Absorbance (0.0 - 4.0 OD)

CSV Export Format

Opentrons Flex Absorbance Plate Reader
Serial: ABC123456
Wavelength: 450nm
Date: 2024-11-09
Time: 14:32:01

    1      2      3      4      5      6  ...  12
A  0.123  0.145  0.167  0.189  0.201  0.223  ...  0.089
B  0.234  0.256  0.278  0.290  0.312  0.334  ...  0.190
C  0.345  0.367  0.389  0.401  0.423  0.445  ...  0.291
...
H  0.456  0.478  0.490  0.512  0.534  0.556  ...  0.392

Metadata included:

  • Module serial number
  • Wavelength(s) measured
  • Timestamp
  • Plate layout with row/column labels

Complete Workflow Pattern

Standard Reading Workflow

# 1. Load and setup
reader = protocol.load_module("absorbanceReaderV1", "D3")
plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "B1")
pipette = protocol.load_instrument("flex_8channel_1000", "left")

# 2. Initialize reader (empty)
reader.close_lid()
reader.initialize(mode="single", wavelengths=[450])
reader.open_lid()

# 3. Prepare samples
# ... pipetting operations ...

# 4. Move plate to reader
protocol.move_labware(plate, reader, use_gripper=True)

# 5. Read plate
reader.close_lid()
data = reader.read(export_filename="my_assay_data")
reader.open_lid()

# 6. Process results or continue protocol
for well in plate.wells():
    if data[450][well.well_name] > 1.0:
        protocol.comment(f"{well.well_name} is positive")

# 7. Move plate off reader
protocol.move_labware(plate, "C1", use_gripper=True)

Common Patterns

ELISA Protocol

# Setup
reader = protocol.load_module("absorbanceReaderV1", "D3")
elisa_plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "C2")
pipette = protocol.load_instrument("flex_8channel_1000", "left")

# Initialize reader
reader.close_lid()
reader.initialize(mode="single", wavelengths=[450], reference_wavelength=562)
reader.open_lid()

# 1. Coat plate, incubate, wash (not shown)

# 2. Add samples
pipette.transfer(100, samples, elisa_plate.wells()[:24])

# 3. Incubate
protocol.delay(minutes=60)

# 4. Wash steps
for _ in range(3):
    # Wash logic (not shown)
    pass

# 5. Add substrate
pipette.transfer(100, substrate, elisa_plate.wells()[:24])

# 6. Incubate
protocol.delay(minutes=15)

# 7. Add stop solution
pipette.transfer(50, stop_solution, elisa_plate.wells()[:24])

# 8. Read plate
protocol.move_labware(elisa_plate, reader, use_gripper=True)
reader.close_lid()
elisa_data = reader.read(export_filename="elisa_450nm_results")
reader.open_lid()

# 9. Analyze results
for well in elisa_plate.wells()[:24]:
    od450 = elisa_data[450][well.well_name]
    if od450 > 0.5:
        protocol.comment(f"{well.well_name}: POSITIVE (OD450={od450:.3f})")
    else:
        protocol.comment(f"{well.well_name}: NEGATIVE (OD450={od450:.3f})")

Bacterial Growth Kinetics

# Setup
reader = protocol.load_module("absorbanceReaderV1", "D3")
culture_plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "C2")
hs_mod = protocol.load_module("heaterShakerModuleV1", "D1")

# Initialize reader for OD600
reader.close_lid()
reader.initialize(mode="single", wavelengths=[600])
reader.open_lid()

# Inoculate cultures (not shown)

# Kinetic reading loop
for timepoint in range(8):  # 8 timepoints
    # Incubate on heater-shaker
    protocol.move_labware(culture_plate, hs_mod, use_gripper=True)
    hs_mod.set_and_wait_for_temperature(37)
    hs_mod.close_labware_latch()
    hs_mod.set_and_wait_for_shake_speed(300)
    protocol.delay(hours=1)
    hs_mod.deactivate_shaker()
    hs_mod.deactivate_heater()
    hs_mod.open_labware_latch()

    # Read OD600
    protocol.move_labware(culture_plate, reader, use_gripper=True)
    reader.close_lid()
    od_data = reader.read(export_filename=f"growth_curve_t{timepoint}")
    reader.open_lid()

    # Log growth
    for well in culture_plate.wells()[:8]:
        od600 = od_data[600][well.well_name]
        protocol.comment(f"T{timepoint}h {well.well_name}: OD600={od600:.3f}")

# Return plate to deck
protocol.move_labware(culture_plate, "B1", use_gripper=True)

Multi-Wavelength Protein Assay

# Setup for BCA or Bradford assay
reader = protocol.load_module("absorbanceReaderV1", "D3")
assay_plate = protocol.load_labware("corning_96_wellplate_360ul_flat", "C2")

# Initialize for multi-wavelength
reader.close_lid()
reader.initialize(mode="multi", wavelengths=[562, 600])
reader.open_lid()

# Prepare standard curve + samples (not shown)

# Incubate
protocol.delay(minutes=30)

# Read at multiple wavelengths
protocol.move_labware(assay_plate, reader, use_gripper=True)
reader.close_lid()
absorbance = reader.read(export_filename="protein_assay_multi_wl")
reader.open_lid()

# Calculate protein concentrations using standard curve
for well in assay_plate.wells()[:24]:
    od562 = absorbance[562][well.well_name]
    od600 = absorbance[600][well.well_name]

    # Example: simple linear standard curve
    protein_conc = od562 * 100  # µg/mL (simplified)
    protocol.comment(f"{well.well_name}: {protein_conc:.1f} µg/mL")

Real-Time Decision Making

# Read plate and perform conditional operations
protocol.move_labware(screening_plate, reader, use_gripper=True)
reader.close_lid()
screen_data = reader.read()
reader.open_lid()
protocol.move_labware(screening_plate, "C1", use_gripper=True)

# Identify hits and cherry-pick
hit_wells = []
for well_name, absorbance in screen_data[450].items():
    if absorbance > 2.0:  # Hit threshold
        hit_wells.append(screening_plate.wells_by_name()[well_name])
        protocol.comment(f"HIT: {well_name} (OD450={absorbance:.3f})")

# Transfer hits to new plate for follow-up
if hit_wells:
    pipette.transfer(50, hit_wells, hit_plate.wells()[:len(hit_wells)])
    protocol.comment(f"Found {len(hit_wells)} hits - transferred for validation")

Best Practices

  1. Always close lid before initialization - Even if already closed
  2. Export data with descriptive filenames - Include experiment ID, date, plate number
  3. Use reference wavelengths - Reduces background and plate artifacts
  4. Read blank wells - Establish baseline for background subtraction
  5. Include standard curves - For quantitative assays
  6. Plan deck layout carefully - Column 4 is reserved for lid storage
  7. Protect column 4 - Never attempt to load labware in staging area
  8. Allow plate equilibration - Brief delay after moving to reader prevents condensation issues
  9. Check gripper compatibility - Ensure plates have grippable geometry
  10. Document wavelength selection - Comment why specific wavelengths were chosen

Common Mistakes

❌ Initializing with lid open:

reader.initialize(mode="single", wavelengths=[450])  # Error: lid must be closed

✅ Correct:

reader.close_lid()
reader.initialize(mode="single", wavelengths=[450])
reader.open_lid()

❌ Loading labware in column 4:

reader = protocol.load_module("absorbanceReaderV1", "D3")
tips = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "D4")  # Error: reserved

✅ Correct:

reader = protocol.load_module("absorbanceReaderV1", "D3")
tips = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "D2")  # Use different column

❌ Reading without closing lid:

protocol.move_labware(plate, reader, use_gripper=True)
data = reader.read()  # Error: lid must be closed for reading

✅ Correct:

protocol.move_labware(plate, reader, use_gripper=True)
reader.close_lid()
data = reader.read()
reader.open_lid()

❌ Using unsupported wavelength:

reader.initialize(mode="single", wavelengths=[550])  # Error: not supported

✅ Correct:

reader.initialize(mode="single", wavelengths=[562])  # Use any wavelength 380-740nm in 10nm steps

❌ Accessing wrong wavelength in data:

reader.initialize(mode="single", wavelengths=[450])
data = reader.read()
od600 = data[600]["A1"]  # Error: only 450nm was measured

✅ Correct:

reader.initialize(mode="single", wavelengths=[450])
data = reader.read()
od450 = data[450]["A1"]  # Access wavelength that was measured

Troubleshooting

Module not loading:

  • Verify API version is 2.21 or later
  • Check robotType: "Flex" in requirements
  • Ensure module is in column 3 slot (A3-D3)

Lid errors:

  • Always call close_lid() before initialize()
  • Ensure column 4 staging area is clear
  • Check gripper is functioning properly

Reading errors:

  • Verify lid is closed before read()
  • Check plate is on module
  • Ensure wavelengths match those set in initialize()

Data access errors:

  • Access wavelengths that were initialized
  • Use correct well names ("A1" not "a1")
  • Check data type (dict, not list)

Gripper cannot move plate:

  • Verify plate has grippable geometry
  • Ensure reader lid is open before moving plate onto/off module
  • Check plate is compatible with reader (96-well plates)

Integration with Other Modules

With Heater-Shaker Module

# Incubate with shaking, read absorbance
hs_mod = protocol.load_module("heaterShakerModuleV1", "D1")
reader = protocol.load_module("absorbanceReaderV1", "D3")

# Setup plate on heater-shaker
protocol.move_labware(assay_plate, hs_mod, use_gripper=True)
hs_mod.set_and_wait_for_temperature(37)
hs_mod.close_labware_latch()
hs_mod.set_and_wait_for_shake_speed(400)
protocol.delay(minutes=30)
hs_mod.deactivate_shaker()
hs_mod.deactivate_heater()
hs_mod.open_labware_latch()

# Move to reader
protocol.move_labware(assay_plate, reader, use_gripper=True)
reader.close_lid()
data = reader.read()
reader.open_lid()

With Temperature Module

# Cool samples, then read
temp_mod = protocol.load_module("temperature module gen2", "D1")
reader = protocol.load_module("absorbanceReaderV1", "C3")

# Cool plate
protocol.move_labware(assay_plate, temp_mod, use_gripper=True)
temp_mod.set_temperature(4)
protocol.delay(minutes=5)

# Read cooled plate
protocol.move_labware(assay_plate, reader, use_gripper=True)
reader.close_lid()
data = reader.read()
reader.open_lid()

temp_mod.deactivate()

CSV Data Export

Exported CSV files are stored on the robot and accessible via the Opentrons App:

  1. Complete protocol run
  2. Open Opentrons App
  3. Navigate to completed protocol run
  4. Download exported CSV files
  5. Analyze in Excel, Python, R, or other tools

File naming: Use descriptive export_filename parameter for easy identification

data = reader.read(export_filename=f"experiment_{exp_id}_plate_{plate_num}_timepoint_{t}")

API Version Requirements

  • Minimum API version: 2.21
  • Robot type: Opentrons Flex only
  • Recommended: Latest API version for full feature support

Additional Resources

Related Skills

  • opentrons - Main Opentrons Python API skill
  • opentrons-gripper - Automated labware movement (required for plate reader)
  • opentrons-heater-shaker - Temperature control with mixing (common integration)
  • opentrons-temperature-module - Temperature control for assays

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.