Quick Start
You are going to build a four-instrument lab and run a plate through it, in one Python file. Everything runs in simulation on your laptop, so no hardware is involved.
A plate comes out of the stacker, gets shaken, gets sealed, and goes to waste. One robotic arm carries it between the instruments.
stacker -> shaker -> sealer -> waste
dispense shake seal discard
a robotic arm moves the plate between them
Before you start
You should have:
- Orca Framework installed (see Installation)
- A Python file open in your editor. Call it
my_first_workflow.py
The whole file
Copy this into my_first_workflow.py. Run it first, then read the walkthrough below; the numbered comments match the sections there.
import asyncio
import orca.orca as orca
from cheshire_drivers import CartesianCoordinates, Teachpoint
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.runtime.execution import ExecutionPhase
from orca.runtime.run_modes import WorkflowRunMode
from orca.runtime.store_factory import InMemoryRuntimeStoreFactory
from orca.runtime.system_runtime import SystemRuntime
from orca.sdk.build import Topology
from orca.sdk.labware import PlateTemplate
from orca.spawn import DISPENSE
from orca.workflow_models.action_context import ActionContext
from orca.workflow_models.method_context import MethodContext
from orca.workflow_models.thread_context import ThreadContext
from orca.workflow_models.workflow_context import WorkflowContext
# 1. Labware template
sample_plate = PlateTemplate("sample_plate", "Cor_Falcon_96_wellplate_340ul_Fb_Black")
def build_topology(stores):
"""Define the physical lab: devices, transporter, locations."""
shaker = Shaker("shaker")
sealer = Sealer("sealer")
stacker = Storage("stacker")
waste = Waste("waste")
c = CartesianCoordinates
teachpoints = [
Teachpoint("stacker", c(0, 200, 300, 0, 90, 180), orientation="right"),
Teachpoint("shaker", c(400, 200, 300, 0, 90, 180), orientation="right"),
Teachpoint("sealer", c(800, 200, 300, 0, 90, 180), orientation="right"),
Teachpoint("waste", c(1200, 200, 200, 0, 90, 180), orientation="right"),
]
arm = Transporter(
"robotic_arm",
teachpoint_store=stores.teachpoints("robotic_arm", seed=teachpoints),
)
return Topology(
locations={
"stacker": stacker,
"shaker": shaker,
"sealer": sealer,
"waste": waste,
},
transporters=[arm],
)
def build_workflow(topology):
"""Define what happens to each sample plate."""
shaker = topology.device("shaker", Shaker)
sealer = topology.device("sealer", Sealer)
# 2. Actions: steps run at one device, each issuing one or more operations
@orca.action(device=shaker, inputs=[sample_plate])
async def shake(ctx: ActionContext):
await ctx.device().shake(duration=30, speed=500)
@orca.action(device=sealer, inputs=[sample_plate])
async def seal(ctx: ActionContext):
await ctx.device().seal(temperature=100, duration=60)
# 3. Methods: sequences of actions
@orca.method
async def shake_step(ctx: MethodContext):
yield shake
@orca.method
async def seal_step(ctx: MethodContext):
yield seal
# 4. Labware thread: one piece of labware's path through the lab
@orca.thread(labware=sample_plate, start=("stacker", DISPENSE), end="waste")
async def sample_thread(ctx: ThreadContext):
yield shake_step
yield seal_step
# 5. Workflow: orchestration of one or more threads
@orca.workflow(name="my_first_workflow")
def my_workflow(wf: WorkflowContext):
wf.start(sample_thread)
return my_workflow
async def main():
# stores hold calibration data like teachpoints and deck layouts; the in-memory factory is fine for sim
stores = InMemoryRuntimeStoreFactory()
topology = build_topology(stores)
workflow = build_workflow(topology)
build = await orca.build_system(
name="My First Workflow",
workflow=workflow,
topology=topology,
stores=stores,
)
runtime = SystemRuntime(build.system, event_bus=build.event_bus)
await runtime.start()
try:
submission = await runtime.submit(workflow, mode=WorkflowRunMode.PURE_SIM)
status = await runtime.wait_for_execution(submission)
if status.status is not ExecutionPhase.COMPLETED:
raise RuntimeError(f"run ended {status.status}: {status.error}")
print(f"run finished: {status.status.value}")
finally:
await runtime.shutdown()
if __name__ == "__main__":
asyncio.run(main())
Run it
python my_first_workflow.py
Nothing prints for the first twenty to thirty seconds. Python is importing the framework. The run itself then logs forty-five lines in about four seconds and the script exits.
build_system puts a handler on the orca logger at INFO, writing to stdout
as <timestamp> [INFO] <message>. This is the whole run with the timestamps and
the reservation UUIDs cut short:
Thread sample_plate-6427f607 - Reservation 3682... granted for stacker/slot
Releasing reservation 3682... for stacker/slot
Thread sample_plate-6427f607 - Starting method: shake_step
Thread sample_plate-6427f607 - Reservation 433c... granted for shaker
Thread sample_plate-6427f607 - Action resolved: shake at shaker
Thread sample_plate-6427f607 - Reservation 9b3b... granted for shaker/slot
LabwareStagingBridge: shaker/slot - preparing for place of sample_plate-6427f607
robotic_arm pick sample_plate-6427f607 from Location: stacker/slot: picking...
robotic_arm pick sample_plate-6427f607 from Location: stacker/slot: picked
LabwareStagingBridge: stacker/slot - labware sample_plate-6427f607 picked from stage
robotic_arm place sample_plate-6427f607 to Location: shaker/slot: placing...
robotic_arm place sample_plate-6427f607 to Location: shaker/slot: placed
LabwareStagingBridge: shaker/slot - labware sample_plate-6427f607 received on stage
Releasing reservation 9b3b... for shaker/slot
Thread sample_plate-6427f607 - Executing: shake at shaker
Thread sample_plate-6427f607 - Action completed: shake at shaker
Thread sample_plate-6427f607 - Method completed: shake_step
[seal_step, at the sealer, in the same shape]
Thread sample_plate-6427f607 - Reservation 518e... granted for waste/slot
LabwareStagingBridge: sealer/slot - preparing for pick of sample_plate-6427f607
LabwareStagingBridge: waste/slot - preparing for place of sample_plate-6427f607
robotic_arm pick sample_plate-6427f607 from Location: sealer/slot: picking...
robotic_arm pick sample_plate-6427f607 from Location: sealer/slot: picked
LabwareStagingBridge: sealer/slot - labware sample_plate-6427f607 picked from stage
robotic_arm place sample_plate-6427f607 to Location: waste/slot: placing...
robotic_arm place sample_plate-6427f607 to Location: waste/slot: placed
LabwareStagingBridge: waste/slot - labware sample_plate-6427f607 received on stage
Releasing reservation 518e... for waste/slot
Releasing reservation 433c... for shaker
Releasing reservation 0c81... for sealer
run finished: completed
Reading it
sample_plate-6427f607 is the plate. The hex suffix is minted per plate, so
yours differs, and that name is the only thing in the log that tells one plate
from another.
- The stacker dispenses. The reservation on
stacker/slot, granted and then released, is the dispense. The stacker's owndispensecall logs nothing, so the first line naming the plate is all you get for this step. shake_stepruns. The method reserves the shaker for its whole duration andshaker/slotfor the move alone.Action resolvedmeans the runtime picked the device,Executingmeans the action body started, andAction completedmeans it returned. TheLabwareStagingBridgelines are the slot agreeing to receive the plate and confirming it arrived.seal_stepruns. The same shape at the sealer, with the arm carrying the plate over from the shaker, plus one extra staging line: the shaker has to make the plate reachable again before the arm can pick it, whereas the plate the stacker dispensed was already sitting there reachable.- The thread ends at the waste. The arm places the plate, and the discard
itself logs nothing, so the last lines are the three reservations being let
go:
waste/slot, then the shaker and the sealer the methods were holding.
What success looks like
No log line says the run finished. The last thing Orca logs is a reservation being released, which looks like any other step ending, so the log alone does not tell you the plate got all the way through. Two things do:
run finished: completed, the line the script prints itself. That comes from the statuswait_for_executionreturned.- Exit code 0. The script raises on any other status, so a non-zero exit and a traceback is the failure case.
Read the status, not the log. wait_for_execution blocks until the execution
reaches a terminal phase, and it raises SystemStallError if the stall detector
pauses the run before that, so a run that gets stuck fails the script instead of
falling quietly out of the bottom of it.
How the file works
Each section below quotes lines from the file above.
1. The labware template
sample_plate = PlateTemplate("sample_plate", "Cor_Falcon_96_wellplate_340ul_Fb_Black")
A labware template describes a type of plate, not a specific physical plate. The first argument is a name you reference in actions. The second is a labware-catalog key (the PyLabRobot plate definition name) that the runtime resolves to the geometry, well count, dimensions and the rest, at build time.
Each plate the workflow actually runs is a separate instance with its own name, minted as <template>-<8 hex chars>, for example sample_plate-a1b2c3d4. That instance name is what shows up in the records, so two plates from one template are never confused. See Labware.
2. The devices and the topology
shaker = Shaker("shaker")
sealer = Sealer("sealer")
stacker = Storage("stacker")
waste = Waste("waste")
return Topology(
locations={
"stacker": stacker,
"shaker": shaker,
"sealer": sealer,
"waste": waste,
},
transporters=[arm],
)
These are the four instruments in this lab. Each is constructed with a name.
The topology is the lab's physical layout. It maps location names to devices. The string "stacker" in the locations dict is the same name a labware thread uses in its start= and end= arguments. The transporters list tells the runtime which arm can move labware between those locations.
The topology is defined separately from any workflow, so the same lab can serve many different workflows. A third argument, pools, groups several interchangeable devices into one addressable pool; this lab has none. See Topology.
3. The arm and its teachpoints
c = CartesianCoordinates
teachpoints = [
Teachpoint("stacker", c(0, 200, 300, 0, 90, 180), orientation="right"),
Teachpoint("shaker", c(400, 200, 300, 0, 90, 180), orientation="right"),
Teachpoint("sealer", c(800, 200, 300, 0, 90, 180), orientation="right"),
Teachpoint("waste", c(1200, 200, 200, 0, 90, 180), orientation="right"),
]
arm = Transporter(
"robotic_arm",
teachpoint_store=stores.teachpoints("robotic_arm", seed=teachpoints),
)
A teachpoint is the coordinate the arm needs in order to pick or place labware at one position. The numbers above are placeholders, which is fine for simulation.
The transporter is the robotic arm. It takes a name and a teachpoint store seeded with that list. Keeping teachpoints in a store means you can retrain a position later without editing source, and teachpoints resolve on every move, so an edit made mid-run applies to the next move. Capturing real ones is under Advanced.
4. Actions
@orca.action(device=shaker, inputs=[sample_plate])
async def shake(ctx: ActionContext):
await ctx.device().shake(duration=30, speed=500)
An action runs at one device, with one or more pieces of labware as input. Its body issues one or more operations, here a single shake call. ctx.device() gives you a typed handle to the device so you can call its methods directly. See Actions.
5. Methods
@orca.method
async def shake_step(ctx: MethodContext):
yield shake
A method groups actions into a named, reusable sequence. A method can also be run on its own. See Methods.
6. The labware thread
@orca.thread(labware=sample_plate, start=("stacker", DISPENSE), end="waste")
async def sample_thread(ctx: ThreadContext):
yield shake_step
yield seal_step
A labware thread is the path one piece of labware takes through the system. It declares which template it is tracking, where the labware starts, where it ends, and the methods to run along the way. The start and end strings are names from the topology's locations dict.
start and end also say how the labware arrives and leaves. DISPENSE means the stacker dispenses the next plate itself, which is why start is a tuple here. A bare string end, like end="waste", means an operator takes the plate away, and simulation does that for you. The other ways in and out are under Advanced.
Threads run in parallel. A second one for tip racks moving through the same lab would run alongside this one. See Threads.
7. The workflow
@orca.workflow(name="my_first_workflow")
def my_workflow(wf: WorkflowContext):
wf.start(sample_thread)
A workflow ties threads together. wf.start(...) declares which thread starts when the workflow runs. For a multi-thread workflow you add wf.thread(other_thread) for the secondary threads.
The workflow function is a regular def, not async def. It declares structure; it does not run anything. See Workflows.
8. Build, start, submit
build = await orca.build_system(
name="My First Workflow",
workflow=workflow,
topology=topology,
stores=stores,
)
runtime = SystemRuntime(build.system, event_bus=build.event_bus)
await runtime.start()
submission = await runtime.submit(workflow, mode=WorkflowRunMode.PURE_SIM)
status = await runtime.wait_for_execution(submission)
await runtime.shutdown()
Five moves:
build_systemis async and produces aSystemBuild: your topology, stores and event bus wired together, with the routing graph resolved.SystemRuntime(...)wraps the built system in the long-running service that schedules and runs submissions. See Runtime.runtime.start()brings the runtime to a state where it can accept submissions.runtime.submit(workflow, mode=...)hands the workflow off. It returns immediately, with a handle, while the runtime runs the workflow in the background.modeis the run mode, and it is the submission that carries it, not the runtime.runtime.wait_for_execution(submission)blocks your script until that submission's execution finishes, and returns the final status. Without it, the script would exit before the workflow completed. It raises if the stall detector pauses the execution, because a stalled run never finishes on its own.runtime.shutdown()then aborts anything still in flight and tears the runtime down.
Check the status you get back. wait_for_execution returns the final snapshot whatever happened, so a run that failed or was aborted comes back FAILED or ABORTED with status.error set. Only COMPLETED means the plate finished its thread.
Advanced
None of this is needed for the run above. Each section points at the page that owns it.
Capture teachpoints on real hardware
In a real lab you capture a teachpoint by jogging your arm to the position and recording its (x, y, z, yaw, pitch, roll). Swap the placeholder numbers for real values before running against hardware. See Transporters.
Deck sites and their names
Every device owns one or more labware sites, named <location>/<site>. A single-nest device like this shaker owns exactly one, shaker/slot, and the build maps a teachpoint named after the location onto it, so Teachpoint("shaker", ...) is enough. A liquid handler is the exception: its sites come from its deck layout, one per deck position, and you teach the site the arm actually reaches by its full name, for example Teachpoint("lh/carrier-7-2", ...). See Transporters.
The other ways labware enters and leaves
A bare string start means MANUAL_PLACE: an operator puts the plate there. A bare string end means MANUAL_REMOVE: an operator takes it away. Under PURE_SIM and DEVICE_SIM both are fulfilled automatically; under LIVE the thread parks at AWAITING_MANUAL_PLACE or AWAITING_MANUAL_REMOVE until the operator says the labware is there or gone. REUSE_EXISTING and LEAVE_IN_PLACE cover deck-resident labware that outlives one run. See Threads.
Run modes beyond PURE_SIM
The devices above are constructed with just a name (Shaker("shaker")) and no driver. That is deliberate. This quickstart runs in PURE_SIM, where each device runs against its simulation driver in-process, so no real driver is involved. For DEVICE_SIM (a simulated driver reached over the gateway, exercising the wire) or LIVE (real hardware), the device factory binds the right driver, chosen by the run mode. You name the devices once; the driver is resolved later. See Submissions and Devices.
See also
- Architecture: how Orca thinks about your lab.
- Threads: what a labware thread can do beyond a straight line.
- Workflows: multi-thread workflows, joins and branches.
- Runtime: accept submissions over time with a long-lived runtime.
- Devices: wire up real hardware.