Skip to main content
Version: 2.0.0

Labware

You declare the plates, tip racks and troughs your workflow needs as labware templates. A template names a type in the labware catalog. The runtime makes the real pieces of labware as the workflow runs, and tracks each one.

Declare a template​

from orca.sdk.labware import PlateTemplate, TipRackTemplate, TroughTemplate

sample_plate = PlateTemplate("sample_plate", labware_type="Cor_96_wellplate_360ul_Fb")
tips = TipRackTemplate("tips", labware_type="opentrons_96_filtertiprack_200ul", with_tips=True)
reagent_trough = TroughTemplate("reagent_trough", labware_type="AGenBio_1_troughplate_190000uL_Fl")
ClassUse for
PlateTemplateMicroplates: 96, 384, 1536-well and so on
TipRackTemplatePipette tip racks
TroughTemplateReservoirs and troughs

All three import from orca.sdk.labware.

The first argument is the name you use to look the labware up in your own code. labware_type names a row in the labware catalog, and that row owns the geometry: well count, well dimensions, max volumes. A template carries no geometry of its own, and it takes a catalog name, not a PyLabRobot object.

with_tips is required on TipRackTemplate. It declares whether the rack starts full or empty.

Pass a template to a thread and an action​

An action lists the templates it works on in inputs. A labware thread names the one template it carries:

@orca.action(device=flex_1, inputs=[sample_plate, tips])
async def transfer(ctx: ActionContext):
lh = ctx.device(ILiquidHandler)
plate = ctx.plate("sample_plate")
await lh.pick_up_tips(await ctx.next_tips("tips", 8))
await lh.aspirate([plate.well(f"{r}1") for r in "ABCDEFGH"], [50.0] * 8)


@orca.thread(labware=sample_plate, start="stacker", end="waste")
async def sample_thread(ctx: ThreadContext):
yield transfer

Inside an action, ctx.labware(name) gives you the piece of labware the thread is carrying. ctx.plate(name), ctx.tip_rack(name) and ctx.trough(name) skip the wrapper and hand you the PyLabRobot object directly. The name is always the template's name, the string you passed as the first argument.

Prefer await ctx.next_tips(name, count) over naming tip positions by hand. It returns the next count spots that still hold a tip, column-major, so an 8-channel head gets a whole column. A rack left on the deck between runs has no full column left by the second run, and a hard-coded position picks air. It raises when the rack cannot supply that many, or when nothing has said what the rack holds.

That is the whole basic story: declare templates, hand them to actions and threads, and read them back off ctx.

Advanced​

Below is what you need once the basic shape stops being enough: how a template resolves against the catalog, what the runtime remembers about one physical plate, and the operator commands that put labware on a deck or take it off.

Catalog, template, instance​

Labware is modeled in three layers:

  1. Catalog definition: a row in the labware catalog, keyed by a string (labware_type, for example "Cor_96_wellplate_360ul_Fb"). The catalog row owns the geometry. The catalog is seeded from PyLabRobot definitions (see PyLabRobot).
  2. Template: what you author. A template names a labware_type; it does not carry geometry of its own. At build time the runtime resolves the name against the catalog and pulls the geometry from the matching row.
  3. Instance: one physical piece of labware flowing through the system at runtime, with its own identity and its own position.

Workflows reference templates; the runtime tracks instances.

The labware_type must resolve to a catalog row whose category matches the template class: plate for PlateTemplate, tip_rack for TipRackTemplate, trough for TroughTemplate, tube for TubeRackTemplate. A mismatch raises TemplateCategoryMismatch at build time, with a hint pointing at the right template class.

Two more template classes​

ClassUse forImport
AnyLabwareTemplateWildcard: matches any labware, for example a waste actionorca.sdk.labware
TubeRackTemplateTube racksorca.resource_models.labware

Use AnyLabwareTemplate() when an action accepts any labware type. Common for discard and 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

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.labware(name, PlateInstance).has_lid:

lidded_plate = PlateTemplate("assay_plate", labware_type="Cor_96_wellplate_360ul_Fb", with_lid=True)

Seeding starting 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", labware_type="Cor_96_wellplate_360ul_Fb",
initial_state=LabwareInitialState(max_fill=True))

# Per-well volumes
seeded = PlateTemplate("std_plate", labware_type="Cor_96_wellplate_360ul_Fb",
initial_state=LabwareInitialState(wells={"A1": 50.0, "B1": 50.0}))

# A reagent trough that starts full and never depletes in sim
reagent = TroughTemplate("reagent", labware_type="AGenBio_1_troughplate_190000uL_Fl",
initial_state=LabwareInitialState(max_fill=True, replenished=True))
FieldEffect
max_fillWells fill to max, racks fill every position. Wins when set.
uniform_volumeEvery well at this volume.
wellsPer-well dict.
tip_positionsOnly these positions hold a tip. Tip racks only.
replenishedA sim-only reagent source that never depletes on aspirate.

One identity, from spawn to disposal​

An instance is minted with an id and a name of the form <template>-<8 hex characters>, for example tips_384-a1b2c3d4. That name is the one link between three places:

  • the Orca instance,
  • the PyLabRobot object inside it,
  • the resource on the driver's own deck.

The name is minted first and handed to the PLR factory, so instance.name and the PLR object's name are always equal. Because the driver's deck is keyed by instance name rather than template name, two racks of one template can sit on one deck and stay distinguishable. Per-labware ledger projections (tip depletion, well volumes) key on that name too.

Two consequences for authoring:

  • The 96-head API takes typed labware, never a name string: await lh.aspirate96(ctx.plate("assay_plate"), 50.0). The bridge reads .name off the object, so nobody has to know a minted name.
  • A vendor protocol that matches on labware names must match the <template>- prefix, not a bare template name.

Where a labware is: the position ledger​

One ledger holds where every labware is, keyed by labware. Each record carries a position, a state, and a reach. Runtime state covers who owns which fact and how a disagreement is settled.

StateMeaning
EXPECTEDThis position is being waited on for this labware. Nothing is there yet.
CLAIMEDThe position is taken for this labware and nothing else may have it, but nothing durable has been written.
PRESENTIt is there.
RETIREDIt has left the system.
ReachMeaning
AT_HANDA pad, a deck site, the jaws, a plate staged at the approach point. An arm can take it.
INSIDEIt occupies the position but is clamped in the device.

"In transit" is deliberately not a state. A carried plate is PRESENT at the mover's own gripper position, so a pick, an operator's assertion, and a rehydrate after a reboot all land in one place and cannot drift apart. A plate in a gripper is one record plus a position an operator can name, not a special case.

Two ways a labware leaves a position, and they are not the same:

  • Vacate: it is no longer here, and where it went is someone else's to say. That is what a pick does.
  • Retire: it has left the system. The record stays and the position is released. Spending the terminal state on a pick would leave a labware already retired when its thread really does end.

What a labware holds​

The runtime tracks identity, location and history. Per-well volumes and tip occupancy fold out of the operation history recorded as actions run.

Two surfaces are easy to confuse:

  • Starting state is declared on the template with initial_state=LabwareInitialState(...). This is what a fresh instance begins with.
  • DeclaredTracking is a per-action declares= annotation for closed-protocol actions, not a template field. It states what an action consumed (wells_used, tips_used, volume_transferred) so the runtime can keep bookkeeping for a device that reports nothing itself. It does not declare starting volumes.

Labware left behind between 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. Plate sources (stackers, hotels) that produce labware on demand are exempt: they declare start=("loc", DISPENSE).
  2. Persistent reagents are a feature. Long-lived troughs that live on a deck 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. Import them from orca.spawn. Threads has the full table; the short version:

SentinelWhen to use it
MANUAL_PLACE / MANUAL_REMOVEDefault for a bare-string start or end. An operator places or removes the labware. Sim modes fulfill it automatically; LIVE parks the thread at AWAITING_MANUAL_PLACE / AWAITING_MANUAL_REMOVE.
DISPENSEPlate stackers and hotels. The runtime asks the source to produce the next plate.
REUSE_EXISTING / LEAVE_IN_PLACEDeck-resident labware that outlives any one execution, such as reagent troughs.

Introducing labware nobody declared​

An operator can put a piece of labware on the bench that no workflow author anticipated. register takes it either way round:

orca labware register sample_plate --location pad_1                    # a declared template
orca labware register --labware-type nest_1_reservoir_195ml --location flex_1/C2-slot

Exactly one of the two. Naming a labware_type derives an ad-hoc template on the spot, named adhoc__<labware_type>. Nothing is stored: the template name is a pure function of the labware type and every instance row already carries its labware_type, so a rebuilt system re-derives a byte-identical template rather than finding a dangling name.

Two refusals are worth knowing about. A catalog row with no PyLabRobot factory behind it raises LabwareHasNoModel before anything is registered, because a catalog row carries geometry and building an instance needs a factory. A category nothing can hold, such as a carrier, raises UnplaceableLabwareCategory.

Registering is also the way to say a labware is somewhere. It routes through the placement chokepoint, which writes the slot, the loaded list, the position ledger and the driver's deck together. The raw deck-world writes (add_deck_labware, remove_deck_labware, reset_deck_labware) are refused as operator commands: sent straight at a device they move one side of the system and leave the ledger calling the site empty.

When a thread is already parked waiting for labware of that template at that location, register adopts the expectation rather than minting a second instance. One thread, one labware identity. A slot awaited for a different template is refused before anything is written.

Clearing labware off a deck​

If a previous run left labware in the way of your next submission:

CommandWhat it does
orca labware discharge <labware_id>Take one labware off the deck world it is on and out of tracking.
orca labware clear-submission <submission_id>The usual end-of-run tidy-up. Clears that submission's labware and leaves deck residents, because a resident's identity is what carries its volumes and remaining tips to the next run.
orca labware clear-allEvery labware, deck residents included. Prompts on a TTY and requires --yes anywhere else.

These are the labware facade's verbs, reachable from the daemon REST API as well as the CLI. CLI: control has the flags and the confirmations.

See also​

  • Actions: how an action declares its inputs and reads labware off ctx.
  • Threads: the thread that carries one piece of labware, and its start and end.
  • Runtime state: who owns the position and contents facts, and how a disagreement is settled.
  • PyLabRobot: how PLR definitions back the catalog rows a labware_type resolves against.
  • Move and pipetting parameters: how one piece of labware can say how it is carried.