- name
- protocol-verification
- description
- Simulate and analyze Opentrons protocols to verify correctness. Use when asked to verify, simulate, analyze, validate, or check a protocol, or when needing to confirm a newly created protocol works.
# Protocol Verification — Simulate & Analyze
After creating or modifying a protocol, verify it using the binaries in `api/.venv/bin/`. All commands run from the **monorepo root**.
## Prerequisites
The `api/` venv must exist. If it doesn't (first time, or after teardown):
```bash
make -C api setup
```
This creates `api/.venv/` with all entry points installed. You only need to do this once.
### OT-2 protocols (outside this repository's `api/.venv`)
OT-2 robot software and the Python API live in a **separate fork** ([opentrons-ot2](https://github.com/Opentrons/opentrons-ot2)). This monorepo's `api/` package (and `api/.venv`) is **Flex-only**: `opentrons analyze` and `opentrons_simulate` **reject OT-2 protocols**.
The same split applies on PyPI: **`opentrons==9.0.0` is the last release where one package could analyze or simulate both OT-2 and Flex.** That wheel supports **Python API through 2.28** (the last apiLevel shared across both robots before the fork). **`opentrons` 9.1+ analyzes Flex only**; use the OT-2 fork / OT-2 app stack for OT-2 protocols at 2.28 or below.
For mixed OT-2/Flex reference checks at apiLevel 2.28, create a **separate** Python 3.12 venv (do not commit it):
```bash
uv venv /path/to/opentrons-9.0-venv --python 3.12
uv pip install --python /path/to/opentrons-9.0-venv/bin/python 'opentrons==9.0.0'
/path/to/opentrons-9.0-venv/bin/python -m opentrons.cli analyze tmp-protocols/my_protocol.py --check --json-output=-
/path/to/opentrons-9.0-venv/bin/opentrons_simulate tmp-protocols/my_protocol.py
```
Flex-only checks use `api/.venv/bin/...` from this monorepo (or current PyPI `opentrons` for Flex).
## Entry Points
| Tool | Binary | Notes |
| ------------------------------------------ | ------------------------------------------------- | --------------------------------------------------- |
| Simulate | `api/.venv/bin/opentrons_simulate` | Flex dev tree; OT-2 rejected |
| Analyze | `api/.venv/bin/python -m opentrons.cli analyze` | Flex dev tree; OT-2 rejected |
| Simulate / analyze (OT-2 or mixed @ ≤2.28) | Separate venv with `opentrons==9.0.0` (see above) | Last unified PyPI release; not monorepo `api/.venv` |
> **Do not use `uv run`** for one-off simulate/analyze calls. It checks and potentially rebuilds the venv on every invocation, adding significant latency. Call the venv binaries directly.
## Working Directories
All local dev artifacts are gitignored and live at the monorepo root:
| Directory | Purpose |
| --------------------- | ---------------------------------- |
| `tmp-protocols/` | Protocol `.py` files |
| `tmp-custom-labware/` | Custom labware `.json` definitions |
| `tmp-csv/` | CSV files for RTP inputs |
## Simulation
Produces a **human-readable runlog** of every command the robot would execute. Use for quick validation.
> **⛔ RTP protocols cannot be simulated.** `opentrons_simulate` has no `--rtp-values` or `--rtp-files` flag. If the protocol defines `add_parameters()` (any RTP — including CSV, int, bool, str, or float), **you must tell the user this upfront and use `opentrons analyze` instead.** Do not attempt to simulate an RTP protocol and let it fail; explain the limitation first, then switch to analyze automatically.
```bash
# Standard
api/.venv/bin/opentrons_simulate tmp-protocols/my_protocol.py
# With custom labware (can be specified multiple times)
api/.venv/bin/opentrons_simulate tmp-protocols/my_protocol.py \
-L tmp-custom-labware/
```
The current working directory is **always** searched for custom labware implicitly.
### Simulation Options
| Flag | Description |
| ----------------------------- | ----------------------------------------------------- |
| `-l`, `--log-level` | `debug`, `info`, `warning` (default), `error`, `none` |
| `-L`, `--custom-labware-path` | Directory to search for custom labware (repeatable) |
| `-e`, `--estimate-duration` | Estimate protocol run time (experimental) |
| `-o`, `--output` | `runlog` (default) or `nothing` |
### Interpreting Results
- **Success**: runlog prints, exit code 0
- **Failure**: Python traceback with error message, exit code 1
Common errors:
- `DeckConflictError` — labware placement conflict
- `LabwareDefinitionDoesNotExist` — invalid labware name
- `OutOfTipsError` — not enough tips for the protocol
- `LiquidHeightUnknownError` — `.meniscus()` called on a well without `load_liquid()` — see `reference-source-map.md`
- `IncompatibleAddressableAreaError` — wrong slot for robot type
## Analysis
Produces **structured JSON** with predicted commands, labware layout, pipettes, modules, and errors. Use for deep inspection or CI validation. Also the only way to verify protocols with CSV RTPs.
```bash
# Standard
api/.venv/bin/python -m opentrons.cli analyze tmp-protocols/my_protocol.py \
--check --human-json-output=-
# With custom labware — pass the JSON file(s) as extra positional arguments
api/.venv/bin/python -m opentrons.cli analyze \
tmp-protocols/my_protocol.py \
tmp-custom-labware/my_custom_plate.json \
--check --json-output=-
# With primitive RTP values (int, float, bool, str)
api/.venv/bin/python -m opentrons.cli analyze tmp-protocols/my_protocol.py \
--check --json-output=- \
--rtp-values='{"sample_count": 8, "dry_run": false}'
# With CSV RTP file
api/.venv/bin/python -m opentrons.cli analyze tmp-protocols/my_protocol.py \
--check --json-output=- \
--rtp-files='{"transfer_map": "tmp-csv/transfer_map.csv"}'
# Combined: custom labware + CSV RTP
api/.venv/bin/python -m opentrons.cli analyze \
tmp-protocols/my_protocol.py \
tmp-custom-labware/my_custom_plate.json \
--check --json-output=- \
--rtp-files='{"transfer_map": "tmp-csv/transfer_map.csv"}'
```
### Analysis Options
| Flag | Description |
| -------------------------- | ------------------------------------------------------------------- |
| `--json-output=FILE` | Machine-readable JSON (`-` for stdout) |
| `--human-json-output=FILE` | Pretty-printed JSON (`-` for stdout) |
| `--check` | Exit non-zero if protocol has errors |
| `--rtp-values=JSON` | Primitive RTP values as JSON string (`int`, `float`, `bool`, `str`) |
| `--rtp-files=JSON` | CSV RTP file paths as JSON string — keys are `variable_name`s |
| `--log-output=PATH` | Log destination (`-` stdout, `stderr` default, or file path) |
| `--log-level` | `DEBUG`, `INFO`, `WARNING` (default), `ERROR` |
> **Custom labware in analyze**: there is no `-L` flag. Pass each labware JSON as an extra positional file argument. The `analyze` command recognizes them by their JSON schema and registers them before running.
### JSON Output Structure
```json
{
"createdAt": "...",
"result": "ok",
"robotType": "OT-3",
"config": {"protocolType": "python", "apiVersion": [2, 28]},
"metadata": {"protocolName": "..."},
"commands": [...],
"labware": [...],
"pipettes": [...],
"modules": [...],
"liquids": [...],
"errors": [],
"runTimeParameters": [...]
}
```
Key fields: `result` (`"ok"` / `"not-ok"` / `"parameter-value-required"`), `errors` (empty = valid), `commands` (full ordered command list).
### With Runtime Parameters
```bash
api/.venv/bin/python -m opentrons.cli analyze protocol.py \
--check --json-output=output.json \
--rtp-values='{"sample_count": 48, "dry_run": false}'
# CSV parameter files
api/.venv/bin/python -m opentrons.cli analyze protocol.py \
--check --json-output=output.json \
--rtp-files='{"plate_map": "/path/to/map.csv"}'
```
## Standard Verification Workflow
**Before running anything**, check whether the protocol defines `add_parameters()`. If it does, skip simulate entirely and go straight to analyze — then tell the user why.
```bash
# 1. Quick check — simulate (only if protocol has NO add_parameters())
api/.venv/bin/opentrons_simulate tmp-protocols/my_protocol.py
# 1b. Quick check — simulate with custom labware (still no RTPs)
api/.venv/bin/opentrons_simulate tmp-protocols/my_protocol.py \
-L tmp-custom-labware/
# 2. Deep check — analyze (required for any protocol with add_parameters())
api/.venv/bin/python -m opentrons.cli analyze \
tmp-protocols/my_protocol.py \
[tmp-custom-labware/my_plate.json] \
--check --human-json-output=- \
[--rtp-values='{"key": value}'] \
[--rtp-files='{"csv_param": "tmp-csv/file.csv"}']
```
**Decision guide:**
| Scenario | Use | Agent behavior |
View on GitHub