A checker for liquid-handling protocols.
A liquid handler does what the protocol says. If the protocol asks a 20–300 µL pipette for 15 µL, or takes 60 µL from a tube with 40 in it, the robot just goes ahead. Usually you notice when the plate looks wrong.
LabCheck runs the protocol in software before the robot does. It keeps count of the liquid in every tube and well and lists the steps that can't work as written. You don't need a robot or any lab data to use it.
Reagent left
Log
PIPETTE_RANGE: the volume is within the pipette's range.REAGENT_EMPTY: the source still has enough left when the step runs.WELL_OVERFLOW: no well ends up over its capacity.WELL_MISSING: the well exists on that plate (there is no I1 on a 96-well plate).CONTROL_MISSING: the negative and positive control wells get filled.DEAD_VOLUME: optional. A source isn't emptied below the volume the pipette can't reach. Off in the figure above.Each failure is listed with its step number and a one-line reason.
I'm building this over four weeks. It starts from the Laboratory Automation Protocol format in this ACS Synthetic Biology paper. First I reproduce the paper's format, then I add the rules, then I break protocols on purpose and count how many the checker catches. The plan is on the report page, sample protocols and reference numbers are on data, and the benchmark so far is on results.
To make your own protocol, click wells on the Build page, or paste JSON into Try it. The glossary explains the lab words.
Paste a protocol as JSON and run the checks. Nothing leaves your browser.
reagents: µL available · pipette: min, max · well_capacity · dead_volume · plate: 96 or 384 · controls · steps: from, to, ul
Results appear here.
Protocol in, verdict and list of failures out. See the report page for the full methodology including protocol schema, simulation algorithm, rule trigger conditions, and evaluation design.
{
"plate": 96,
"reagents": {"buffer": 1000, "dye": 40, "sample": 120},
"pipette": {"min": 20, "max": 300},
"well_capacity": 200,
"dead_volume": 0,
"controls": {"A1": "negative", "A12": "positive"},
"steps": [
{"from": "buffer", "to": "A1", "ul": 100},
{"from": "dye", "to": "B1", "ul": 20}
]
}
stock = reagents.copy()
wells = {}
for step in steps:
stock[step.from] -= step.ul
wells[step.to] = (wells[step.to] || 0) + step.ul
check_rules(step, stock, wells)
check_controls(wells, controls)
| Code | Fails when | Message |
|---|---|---|
PIPETTE_RANGE | ul < min ∨ ul > max | {ul} µL outside {min}–{max} µL range |
REAGENT_EMPTY | stock[from] < 0 | {from} short by {−stock} µL |
WELL_OVERFLOW | wells[to] > capacity | {to} would hold {wells[to]} µL, max {cap} µL |
WELL_MISSING | to ∉ plate grid | {to} does not exist on this plate |
CONTROL_MISSING | Control well never filled | {type} control {well} never dispensed into |
DEAD_VOLUME | 0 ≤ stock[r] < dead_volume (and stock[r] ≥ 0, i.e. not already empty) | {r} left with {stock} µL, below {dead} µL dead volume |
reagents:
buffer: {labware: reservoir, well: A1, ul: 1000}
dye: {labware: tubes, well: A1, ul: 40}
controls: {negative: A1, positive: A12}
steps:
- id: 4
transfer: {from: dye, to: B1, ul: 15}
# step 4 fails: 15 µL is below the 20 µL minimum
Error injection: start from valid generated protocols, inject one known mistake per protocol, measure whether the corresponding rule fires. Two injection types (WRONG_REAGENT, WRONG_WELL) have no dedicated rule—any detection is incidental.
Benchmark generation: 8–24 steps, 3 reagents (buffer 4000µL, sample 800µL, dye 400µL), random well/reagent/volume respecting all constraints.
Metrics: detection rate per error type (Wilson 95% CI), multi-error detection (all injected codes reported), false alarm rate on clean protocols, timing.
Real-protocol panel: four transcribed OT-2 protocols (serial-dilution subset and full run, restriction digest, PCR distribution) with the libraries' own volumes, labware and pipettes, checked as transcribed — any report counts as a false alarm on real data. Retrospective: two shipped library faults — the 649b67 example input overdraw (caught: REAGENT_EMPTY) and the 052d03 diluent-planning fix (miss: reservoir-well planning has no rule yet). Sources and transcription notes are on the data page.
Rates are binomial proportions (caught / n). Intervals are Wilson score intervals at 95% (z = 1.96):
(p + z²/2n ± z·√(p(1−p)/n + z²/4n²)) / (1 + z²/n)
I use Wilson rather than the textbook Wald interval (p ± z·√(p(1−p)/n)) because Wald collapses to zero width at 0% and 100% — exactly where most of these rates sit — and under-covers near the edges. Wilson holds ~95% coverage across the range. No p-values are reported; with n = 300 the intervals speak for themselves.
Reproducibility: base protocols come from one pseudorandom stream; each injected fault type draws from its own independent stream derived from the seed. Same seed and count replay the same tables bit-for-bit. The Atlas page re-runs five seeds to show the numbers do not depend on a lucky draw.
Checking is linear: each step does constant work (update two counters, test six rules), so time is O(S) in the number of steps and memory is O(W) in filled wells plus O(R) reagent stocks. The full cost table is on the atlas page. Timing uses the median of repeated runs to resist garbage-collection pauses.
It knows nothing about viscosity, evaporation, collisions on the deck, timing, or whether the experiment makes sense biologically. A pass only means the protocol agrees with itself. The assay could still fail.
Planned stack: Python, pytest, YAML/JSON protocol files, and the Opentrons API where it fits.
What the checker catches in generated protocols.
Two evaluations. The verdicts that matter are the two real-data panels below: valid OT-2 protocols that must pass, and shipped library faults that must (or demonstrably cannot yet) be caught. The injection benchmark further down is synthetic by design — regression tests for code correctness, not field performance: random-generator protocols with script-injected errors.
Each protocol gets one mistake. "Caught" means the checker reported that rule's code. For the two rows with no rule, it means the checker reported anything.
| Injected error | Target | n | Caught | Rate | 95% interval |
|---|
Interval: Wilson score (95%). Same seed and protocol count reproduce the same table: base protocols come from one stream and each error type gets its own independent stream. Swapped reagent and moved dispense have no rule, so any catch there is by accident: the change happened to cause an overflow, an empty source or a missing control.
Two or three different mistakes in one protocol. It only counts if every injected code is reported.
| Injected together | n | All found | Rate | 95% interval |
|---|
For each kind of injection, how many runs reported each code. A run can report more than one. Bold marks the code the injection was aimed at.
| Injected | PIPETTE_RANGE | REAGENT_EMPTY | WELL_OVERFLOW | WELL_MISSING | CONTROL_MISSING | nothing |
|---|
The four transcribed real OT-2 protocols from the data page, checked as transcribed with their own pipette ranges and well limits. They are valid runs, so they should all pass — any report here is a false alarm on real data.
| Protocol | Steps | Result |
|---|
Shipped faults found by lab users (sources on the data page), not injected by me. H1 is a fault inside the checker's scope; H2 is outside it — reservoir-well planning has no rule yet — and is counted as a miss so the gap stays visible.
| Case | Expected | LabCheck | Verdict |
|---|
Partly done: four transcribed valid OT-2 protocols, one shipped fault caught, one scoped miss. Still to do: a second in-scope historical fault, error types chosen by someone other than me, and running the checker prospectively on new protocols before they run.
The protocols and reference numbers the checker works with.
Nine small protocols I wrote by hand. The checker runs on each when you open this page, so the result column isn't typed in. "Open in Build" loads one into the builder.
| # | Protocol | Steps | µL moved | Wells | Result |
|---|
Four subsets transcribed from published, experimentally validated OT-2 protocols in the Opentrons Protocol Library. Volumes, labware and pipettes are the libraries' own defaults — not my invention. The checker runs on each when you open this page, so the result column isn't typed in. "Open in Try it" loads the full JSON (with its real pipette range and well limit) into the validator.
| # | Protocol | Steps | µL moved | Wells | Result |
|---|
Transcription notes. LabCheck v1 models only reagent → well dispenses on one plate with one pipette range, so each entry is the reagent → well phase of its source: (1) serial dilution keeps the robot's 100 µL diluent distribution (R1: first 6 wells, R4: all 88 wells of columns 2–12); the 50 µL well-to-well serial transfers are out of scope and noted as a limitation. (2) restriction digest collapses per-sample DNA tubes and the three enzymes each into one stock but keeps every real per-well volume and the real transfer order (water → buffer → sample → enzyme); the real run splits volumes across a P20 (≤20 µL) and a P300 (>20 µL), modelled here as their combined 1–300 µL envelope. (3) PCR distribution is exact at defaults. (4) the 649b67 fault case models the 6-well plate as a 96 grid (B3 is a valid name in both) with the real 16.8 mL well limit. None of the sources declares control wells, so controls is {} and the control rule is not evaluated for them.
Shipped protocol versions and example inputs from the Protocol Library's own history — faults found by lab users, not injected by me. Expected verdict and live result side by side; H2 is a miss by scope and says so.
| # | Case | Steps | µL moved | Expected | LabCheck | Verdict |
|---|
Single-channel ranges for the Opentrons OT-2 GEN2 pipettes, from Opentrons' pipette specifications (also in each protocol's fields.json). The checker's default 20–300 µL range comes from the P300.
| Pipette | Min µL | Max µL |
|---|---|---|
| P20 | 1 | 20 |
| P300 | 20 | 300 |
| P1000 | 100 | 1000 |
Well maxima from labware definitions and manufacturer datasheets — not guesses. The demo default of 200 µL is the NEST 96-well 200 µL flat (Opentrons Labware Library). The 96-well 360 µL figure is the Corning 3997 flat-bottom plate (360 µL total, 75–200 µL working volume). The 384-well 112 µL figure is the Corning 384-well flat (Opentrons Labware Library). For any other plate, use its datasheet.
| Format | Rows | Columns | Wells | Max µL per well |
|---|---|---|---|---|
| 96-well (NEST 200 µL flat) | A–H (8) | 1–12 | 96 | 200 |
| 96-well (Corning 360 µL flat) | A–H (8) | 1–12 | 96 | 360 |
| 384-well (Corning 112 µL flat) | A–P (16) | 1–24 | 384 | 112 |
LabCheck: a static checker for liquid-handling protocols. Version 1.0 — October 2026.
Liquid handlers run protocols exactly as written, so a mistake in the protocol becomes a mistake in the experiment. LabCheck simulates a structured protocol, checks it against six rules before it runs, and explains each failure. I measure how well it works by injecting known errors into valid protocols. The browser implementation validates protocols in under 2 ms for typical workflows (≤25 steps) and catches >95% of injected errors for the five rule-covered failure modes.
The Laboratory Automation Protocol (LAP) format, described in ACS Synthetic Biology (2023), gives automated experiments a shared structure: reagents with volumes, labware definitions, pipette specifications, control wells, and an ordered list of transfer steps. LabCheck adds a validation step on top of LAP. It is meant for lab automation and assay development, where the run only works if the protocol was right to begin with.
A protocol is a JSON or YAML document with these fields:
{"buffer": 1000, "dye": 40}){min, max} in µL (default 20–300 for P300)"negative" or "positive" (default A1 negative, A12 positive){from, to, ul} where from is a reagent name, to is a well (e.g., "B3"), ul is volumeThe simulator maintains running state for every source and well:
stock[r] = starting volume for each reagentwells[w] = 0 for each well on the plate(from, to, ul) in order:
stock[from] -= ulwells[to] += ulcontrols received ≥1 µL| Code | Trigger condition | Message template |
|---|---|---|
PIPETTE_RANGE | ul < min ∨ ul > max | {ul} µL is outside the {min}–{max} µL pipette range |
REAGENT_EMPTY | stock[from] < 0 | {from} is short by {−stock} µL |
WELL_OVERFLOW | wells[to] > well_capacity | {to} would hold {wells[to]} µL, max {cap} µL |
WELL_MISSING | to ∉ plate grid | {to} does not exist on this plate |
CONTROL_MISSING | Control well never received liquid | {type} control {well} is never dispensed into |
DEAD_VOLUME | 0 ≤ stock[from] < dead_volume (opt-in; empty stock reports REAGENT_EMPTY instead) | {from} is left with {stock} µL, below its {dead} µL dead volume |
To measure detection rates without real-world error data, I start from valid generated protocols and inject one known mistake per protocol:
The last two have no dedicated rule; any detection there is incidental (the swap happens to cause overflow, empty source, or missing control).
Valid protocols are generated by randomly sampling transfers that respect all constraints:
Error injection gives a detection rate for each error type, and clean protocols give the false alarm count. Full results with 95% Wilson intervals are on the results page. Visualizations of detection rates, timing, and limit sensitivity are on the analysis page. The real-data evaluation transcribes published OT-2 protocols and shipped library faults: four valid protocols with zero false alarms, one in-scope historical fault caught, one out-of-scope miss kept visible; sources and transcription notes are on the data page.
The checker only finds what its rules describe. It doesn't model liquid properties (viscosity, evaporation, foaming), deck layout (tip rack positions, labware coordinates, collisions), timing (incubation, settling), or biology (whether the assay design makes sense). A pass only means the protocol is internally consistent with respect to the six rules—nothing more.
| Week | Goal | Status |
|---|---|---|
| 1 | Reproduce the paper's protocol representation and one example workflow | complete |
| 2 | Implement the validation rules and unit tests | complete |
| 3 | Add simulated execution, error injection and audit logs | complete |
| 4 | Benchmark detection, write up limitations, publish the repository | complete |
| Date | Entry |
|---|---|
| 14 Oct 2026 | Project page published. Browser reference checker, simulator, builder and validator working. Python implementation not started. |
| 15 Oct 2026 | Added sample protocols, reference tables, a benchmark on generated protocols (seven kinds of error), and an analysis page with charts and timing. |
| 16 Oct 2026 | Expanded report with full methodology, protocol schema, simulation algorithm, all six rules with trigger conditions and message templates, error injection procedures, benchmark generation details, and limitations. |
| 16 Oct 2026 | Real-data evaluation: 4 transcribed valid OT-2 protocols (0 false alarms), retrospective on library history — 649b67 shipped example-input overdraw caught, 052d03 diluent-planning fix missed (no reservoir rule; kept as visible miss). Synthetic injection repositioned as regression tests. |
Click wells to make a protocol. Pick a reagent, set a volume, then click a well to put it there. The checker runs after every click.
Reagent left
Steps
Lab words used on this site.
Charts drawn in your browser by the same checker.
Pick a protocol to see what it does to the reagents and the plate. The example is the one on the home page and the others are on the Data page — including three transcribed real OT-2 protocols with their own pipettes, plates and well limits.
This and the next two sections use a set of random valid protocols, the same kind the benchmark uses. Change the seed or the count and press Recompute.
These come from injecting errors into the generated protocols, as on the Results page.
The generated protocols were made for a 200 µL well and a 20 µL pipette minimum. Here the limits change and the checker runs again on the same protocols. The dashed line or red box marks the values they were built for.
Visual summary of the checker's four stages. The parser reads the protocol, the simulator tracks volumes, the rule engine tests six conditions, and the reporter lists every violation.
Each rule fires when its specific condition is met during or after simulation. This table shows the exact logic.
The JSON structure a protocol must follow. Required fields are plate, reagents, and steps. Others have defaults.
How much of each stock is consumed across the protocol. The waterfall shows cumulative usage per reagent.
Heatmap showing how full each well gets as a percentage of its capacity. Red = over capacity.
Each step's volume colored by reagent, with pipette range band. Red dots exceed range.
When multiple errors are injected, which rule codes tend to appear together. Diagonal = marginal frequency.
Pairwise similarity of generated protocols based on their step sequences (reagent + well + volume bucket).
Time to check one protocol of a given length. The test protocol has no errors, so this measures the bookkeeping and not the error reporting.
Every chart and table in one place.
Same generator and checker as the Results page. Change the seed or count and press Recompute; every figure below rebuilds from the new set.
What transfers look like in valid protocols: per-reagent sizes, controls, plate position, reuse, and how close wells get to the limit.
Do the headline numbers survive a change of seed or sample size? Five independent seeds, three sample sizes, and clean-vs-injected report counts.
Limits are where checkers earn trust. Each boundary is tested from both sides: just inside must pass, just outside must fail.
Where errors surface and which codes travel together, beyond the Analysis page.
Fifteen fault variants, each injected into 100 valid protocols. Rows marked must-pass sit exactly on the allowed side of a boundary: any report there is a false alarm by construction.
| # | Variant | What changes | Expect | n | Flagged | Rate |
|---|
Stock usage and depletion risk per reagent.
Where liquid lands on the plate.
How the checker is built and what each figure assumes. Seed stability across 5 seeds:
| Seed | n | Mean steps | Mean µL | False alarms |
|---|
Asymptotic cost (S steps, W filled wells, R rules):
| Stage | Time | Space | Notes |
|---|
Defaults and where they come from:
| Parameter | Default | Source |
|---|