Skip to main content

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

ClassUse forImport
PlateTemplateMicroplates (96, 384, 1536-well, etc.)orca.sdk.labware
TipRackTemplatePipette tip racksorca.sdk.labware
TroughTemplateReservoirs and troughsorca.sdk.labware
AnyLabwareTemplateWildcard — 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:

  1. 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).
  2. 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.
  3. 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.
  • DeclaredTracking is a per-action declares= 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:

  1. 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 at pad_1, the submission is refused with START_LOCATION_OCCUPIED. The exception is plate sources (stackers, hotels) that produce labware on demand — those declare start=("loc", DISPENSE) and are exempt.
  2. Persistent reagents are a feature, not a bug. Long-lived troughs (deck-resident reagents that live across many executions) use start=("loc", REUSE_EXISTING) and end=("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:

SentinelWhen to use it
MANUAL_PLACE / MANUAL_REMOVEDefault for bare-string start/end. Operator places or removes the plate.
DISPENSERequired for plate stackers and hotels. Runtime asks the source to produce the next plate.
REUSE_EXISTING / LEAVE_IN_PLACEPersistent 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:

ToolAction
labware_dischargeRemove one specific labware instance from the runtime's tracking.
labware_clear_submissionClear every labware tracked by one submission.
labware_clear_allNuclear 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.