Topology
A topology is the list of what is in your lab and where each thing sits: the instruments, the locations that hold labware, and the arms that move labware between them.
You write it once, on its own. The same topology serves many workflows, and the same workflow runs against a simulated lab and a real one without a code change.
A complete topology
Four instruments and one arm:
from orca.devices.devices import Storage, Waste
from orca.devices.sealer import Sealer
from orca.devices.shaker import Shaker
from orca.resource_models.transporter import Transporter
from orca.sdk.build import Topology
def build_topology(stores):
arm = Transporter(
"robotic_arm",
teachpoint_store=stores.teachpoints("robotic_arm", seed=teachpoints),
)
return Topology(
locations={
"stacker": Storage("stacker"),
"shaker": Shaker("shaker"),
"sealer": Sealer("sealer"),
"waste": Waste("waste"),
},
transporters=[arm],
)
teachpoints is the list of coordinates the arm needs to reach each position. Anything that moves labware, arm or bridge, is a transporter and goes in that list. See Transporters.
Location names are what a thread starts and ends at
The keys of locations are the names everything else refers to. A labware thread says where its labware comes from and where it leaves, using those keys:
@orca.thread(labware=sample_plate, start=("stacker", DISPENSE), end="waste")
async def sample_thread(ctx: ThreadContext):
yield shake_step
yield seal_step
"stacker" and "waste" are the same strings as in the mapping above. An action naming a device, and a teachpoint taught at a position, use those strings too. Rename a location and you rename it everywhere. See Labware threads.
What a location holds
Each entry in locations is one of two things.
- A device:
Shaker,Sealer,LiquidHandler,Reader,Storage,Wasteand the rest. Every device owns the labware positions it has. See Devices. - A passive placeable:
PlatePad. Somewhere labware can rest that is not an active instrument.
from orca.resource_models.plate_pad import PlatePad
locations = {
"stacker": Storage("stacker"),
"pad_1": PlatePad("pad_1"),
"shaker": Shaker("shaker"),
"sealer": Sealer("sealer"),
"waste": Waste("waste"),
}
PlatePad takes supports_deadlock_resolution (default True). The move resolver stages a blocked plate on a pad to free a route, so turn it off for a pad that must not be used that way, such as either end of a bridge.
The three fields
| Field | Type | Holds |
|---|---|---|
locations | Mapping[str, LocationValue] | Named positions that can hold labware. Values are devices or passive placeables. |
transporters | list[Transporter] | Everything that moves labware between locations: arms (Transporter) and single-carriage bridges (Translator). |
pools | list[ResourcePool] | Multi-device pools (e.g. a bank of shakers treated as one). |
Looking a device up from a topology
Inside a build_workflow(topology) function you can fetch a typed device handle to hand to an action:
shaker = topology.device("shaker", Shaker)
topology.device(name, expected_type) raises KeyError for an unknown name (listing what is available) and TypeError when the location holds a different device type than you expected. The Quick start wires a whole workflow this way.
Sim or hardware, same topology
You never pass a driver= argument. Each device constructor calls resolve_drivers(...), which asks the device factory context in scope whether this device gets a simulated or a real driver. Swap the factory context without touching the topology, and the same workflow runs against a sim lab and a production lab.
Each device may also declare sim_override=WorkflowRunMode.... to pin itself toward sim regardless of what a submission asks for. See Sim hierarchy for the resolver and the LIVE confirmation gate.
Advanced
Read this when your lab has a liquid handler whose deck Orca models, a bank of identical instruments, or a device only the on-prem agent knows about.
Every device-owned position is its own node
build_system turns each entry in locations into graph nodes. A device does not become one node:
- One mutex Location keyed by the device's own name. It is never a routing node and never a move target. It is the key an action reserves, so an action on
shakerexcludes every other thread from that device (ADR-007, ADR-009). - One site node per position the device owns, named
<location>/<site>. A plain device owns one site calledslot, soshakercontributesshaker/slot. A liquid handler with a modeled deck contributes one site per deck slot, such asflex_1/D1-slot.
Deck sites are never deadlock-parking targets; the resolver parks on PlatePads. Routing across one device's deck to reach a second device is not supported: a route may enter a device's sites only for work on that device.
Bare and site-qualified names
Two consequences of the site model that you author against.
Teachpoints. For a device with a single site, teach the bare device name; the build expands it onto that device's site. For a deck, teach the site-qualified name (Teachpoint("flex_1/D1-slot", ...)).
Thread start and end. A bare device name resolves only when the device has exactly one site. A multi-site device needs the qualified form.
Liquid handlers: deck-modeled or deckless
Two handler classes, and which you pick shapes the topology.
LiquidHandler (orca.devices.devices) is a handler whose deck Orca models, either Hamilton carriers or Opentrons slot decks (Flex / OT-2). Each deck position is a site node labware can be delivered to, addressed as <device>/<site>. You do NOT declare handoff points: handoff is derived from the topology. Any deck site the external arm teaches is a transit entry, and the handler's internal gripper relays labware between deck sites (the build wires those gripper edges as a DeckGripperTransporter named <device>/gripper).
Arguments after name are keyword-only:
flex_1 = LiquidHandler(
"flex_1",
deck_layout_store=stores.deck_layouts("flex_1", seed={"default": FLEX_1_DECK}),
deck_layout="default",
park_gripper_at=None, # optional: where to park the gantry
)
deck_layout names a layout in deck_layout_store. It is required for DEVICE_SIM and LIVE, optional for PURE_SIM. See Transporters for park_gripper_at.
LiquidHandlerProtocol (orca.devices.devices) is a handler with no Orca-modeled deck (Bravo/VWorks, remote/gateway handlers). No addressable deck sites and no internal-gripper relay. An unmodeled deck is not the same as no capacity: a protocol that consumes several labware at once needs a position for each, so declare them with site_names=[...]. The default is one position, and an action taking more than one input against a single-position handler is rejected.
For Hamilton Venus protocol files, use the Venus device (orca.sdk.devices): a protocol runner rather than a deck-modeled liquid handler. See Devices.
Resource pools
A resource pool groups identical devices and treats them as one addressable resource. When an action targets a pool, the runtime picks the next available device from the pool.
from orca.resource_models.resource_pool import ResourcePool
shaker_1 = Shaker("shaker_1")
shaker_2 = Shaker("shaker_2")
shaker_pool = ResourcePool("shaker_pool", [shaker_1, shaker_2])
topology = Topology(
locations={"shaker_1": shaker_1, "shaker_2": shaker_2, ...},
transporters=[arm],
pools=[shaker_pool],
)
@orca.action(device=shaker_pool, inputs=[plate]) # picks the next free shaker
async def shake(ctx: ActionContext):
await ctx.device().shake(duration=30, speed=500)
Single-device pools are created automatically. You only need a ResourcePool for multi-device cases.
Fetch one back inside build_workflow(topology) with topology.pool("shaker_pool"), which raises KeyError and lists the pools that do exist.
Devices the topology never declared
orca device list is a union of what the topology declares and what an on-prem agent has connected. A device only an agent knows about can be inspected and driven ad hoc: orca device execute, orca device invoke and the capability reads all resolve against the agent's handshake card when the topology has no such name. A name neither side knows is reported missing. See CLI: inspect the system and CLI: operator interventions.
Reachability stops there. An undeclared device is not in the routing graph, so no labware can be routed to it and no action can name it. To use it in a workflow, declare it in the topology.
See also
- Devices: what device types ship in the box, and how capabilities are decided.
- Transporters: teachpoints, routing, translators and handoffs.
- Labware threads: where
startandendname the locations you declared here. - Quick start: a topology, a workflow and a run in one file.
- Sim hierarchy: which world a run and an operator command mean.