Labware
A labware template describes a type of physical labware (a plate, a tip rack, a trough), not a specific instance. Workflows reference labware templates; the runtime tracks specific physical instances flowing through the system.
Built-in labware templates
| Class | Use for | Import |
|---|---|---|
PlateTemplate | Microplates (96, 384, 1536-well, etc.) | orca.sdk.labware |
TipRackTemplate | Pipette tip racks | orca.sdk.labware |
TroughTemplate | Reservoirs and troughs | orca.sdk.labware |
AnyLabwareTemplate | Wildcard — matches any labware (e.g., for waste actions) | orca.sdk.labware |
The three layers: catalog, template, instance
Labware is modeled in three layers, and getting them straight is the key to using templates correctly:
- Catalog definition: a row in the labware catalog, keyed by a string slug (e.g.
"Cor_96_wellplate_360ul_Fb"). The catalog row owns the geometry: well count, well dimensions, max volumes. The catalog is seeded from PyLabRobot definitions at build time (see PyLabRobot). - Template: what you author. A template names a slug; it does not carry geometry of its own. At build time the runtime resolves the slug against the catalog and pulls the geometry from the matching row.
- Instance: a specific physical piece of labware flowing through the system at runtime. The runtime creates instances from a template and tracks each one's identity, location, and history.
Workflows reference templates; the runtime tracks instances.
Defining a labware template
A template takes a name and a catalog slug string (labware_type), not a PyLabRobot object. The slug resolves against the labware catalog at build time:
from orca.sdk.labware import PlateTemplate, TipRackTemplate, TroughTemplate
sample_plate = PlateTemplate("sample_plate", "Cor_96_wellplate_360ul_Fb")
tips = TipRackTemplate("tips", "hamilton_96_tiprack_10uL_filter", with_tips=True)
trough = TroughTemplate("reagent_trough", "AGenBio_1_troughplate_190000uL_Fl")
with_tips is required on TipRackTemplate — it declares whether the rack starts full or empty.
The slug must resolve to a catalog row whose category matches the template class (a plate row for PlateTemplate, a tip_rack row for TipRackTemplate, a trough row for TroughTemplate). A mismatch raises TemplateCategoryMismatch at build time, with a hint pointing you at the right template class.
Plates with a lid
Pass with_lid=True to start the plate lidded. Lids are a flag on the plate, not a separate template; at runtime branch on ctx.plate(name).has_lid:
lidded_plate = PlateTemplate("assay_plate", "Cor_96_wellplate_360ul_Fb", with_lid=True)
Seeding initial volumes and tips
Use initial_state=LabwareInitialState(...) to declare what a fresh instance starts with — well volumes for plates and troughs, tip occupancy for racks:
from orca.sdk.labware import PlateTemplate, TroughTemplate, LabwareInitialState
# Fill every well to max
full_plate = PlateTemplate("ctrl_plate", "Cor_96_wellplate_360ul_Fb",
initial_state=LabwareInitialState(max_fill=True))
# Per-well volumes
seeded = PlateTemplate("std_plate", "Cor_96_wellplate_360ul_Fb",
initial_state=LabwareInitialState(wells={"A1": 50.0, "B1": 50.0}))
# A reagent trough that starts full
reagent = TroughTemplate("reagent", "AGenBio_1_troughplate_190000uL_Fl",
initial_state=LabwareInitialState(uniform_volume=190000.0))
LabwareInitialState fields: max_fill (wells fill to max / racks fill every position; wins when set), uniform_volume (every well at this volume), wells (per-well dict), tip_positions (only these positions hold a tip, tip racks only), replenished (sim-only reagent source that never depletes on aspirate).
Wildcards: AnyLabwareTemplate
Use AnyLabwareTemplate() when an action accepts any labware type. Common for discard / waste actions:
from orca.sdk.labware import AnyLabwareTemplate
@orca.action(device=waste, inputs=[AnyLabwareTemplate()])
async def discard(ctx: ActionContext):
... # discards whatever labware the thread brings
Labware in actions
Actions reference labware templates in their inputs. At runtime, ctx.labware(name) gives you a handle to the labware instance the thread is carrying. For typed access to the underlying PyLabRobot resource, use the typed accessors ctx.plate(name), ctx.tip_rack(name), and ctx.trough(name):
@orca.action(device=liquid_handler, inputs=[sample_plate, tips])
async def transfer(ctx: ActionContext):
plate = ctx.plate("sample_plate") # underlying PLR Plate
rack = ctx.tip_rack("tips") # underlying PLR TipRack
await ctx.device(ILiquidHandler).pick_up_tips([rack.tip_spot("A1")])
await ctx.device(ILiquidHandler).aspirate([plate.well("A1")], [50.0])
Tracking and labware state
The runtime tracks labware identity, location, and history through the ILabwareStore. Per-well volumes and tip occupancy fold out of the operation history the runtime records as actions run.
Two distinct surfaces are easy to confuse:
- Starting state is declared on the template with
initial_state=LabwareInitialState(...)(see above). This is what a fresh instance begins with. DeclaredTrackingis a per-actiondeclares=consumption demand, not a template field. It states what an action expects to consume (e.g. which tip positions it will use) so the runtime can refuse to continue when the labware can no longer satisfy it. It does not declare starting volumes.
Labware persistence across executions
Labware persists at every location across executions — the engine never auto-clears. When an execution finishes, plates stay where they ended unless the workflow explicitly removed them. This matters for two reasons:
- Start locations cannot already be occupied. If you submit a workflow whose entry thread starts at
pad_1, and a plate from a previous run is still atpad_1, the submission is refused withSTART_LOCATION_OCCUPIED. The exception is plate sources (stackers, hotels) that produce labware on demand — those declarestart=("loc", DISPENSE)and are exempt. - Persistent reagents are a feature, not a bug. Long-lived troughs (deck-resident reagents that live across many executions) use
start=("loc", REUSE_EXISTING)andend=("loc", LEAVE_IN_PLACE)so the runtime binds to the existing instance instead of expecting a fresh one.
Spawn sentinels
The start and end arguments on @orca.thread accept either a bare location name or a (location, sentinel) tuple. The sentinel tells the runtime how the labware appears or departs. See Threads — Start and end sentinels for the full table; the short version:
| Sentinel | When to use it |
|---|---|
MANUAL_PLACE / MANUAL_REMOVE | Default for bare-string start/end. Operator places or removes the plate. |
DISPENSE | Required for plate stackers and hotels. Runtime asks the source to produce the next plate. |
REUSE_EXISTING / LEAVE_IN_PLACE | Persistent reagent troughs that stay on the deck across executions. |
Recovering occupied start locations
If a previous run left a plate in the way of your next submission, three tools handle the cleanup:
| Tool | Action |
|---|---|
labware_discharge | Remove one specific labware instance from the runtime's tracking. |
labware_clear_submission | Clear every labware tracked by one submission. |
labware_clear_all | Nuclear option — clear all labware tracking. Useful for sandbox resets. |
These live on the runtime's labware facade (not directly on SystemRuntime) and are exposed through the daemon REST API and the orca CLI.
Where to go next
- Actions — using labware in action bodies.
- PyLabRobot — how PLR definitions seed the catalog your slugs resolve against.