Skip to main content

Custom Code

The PythonMethod action lets you run custom Python code when labware reaches a device.

Basic Usage

from orca.sdk.actions import PythonMethod
from orca.sdk.workflow import MethodTemplate

async def my_custom_logic(resource, inputs, outputs, options):
# Your code here
pass

method = MethodTemplate(
name="custom_method",
actions=[
PythonMethod(
resource=device,
method=my_custom_logic,
inputs=[plate],
outputs=[plate]
)
]
)

Function Signature

Your function must be async and accept exactly these four parameters:

from typing import Any, Dict, List
from orca.sdk.devices import Device
from orca.sdk.labware import LabwareInstance

async def my_method(
resource: Device,
inputs: List[LabwareInstance],
outputs: List[LabwareInstance],
options: Dict[str, Any] | None = None
) -> None:
...

Parameters

ParameterTypeDescription
resourceDeviceThe device where labware is located
inputsList[LabwareInstance]Labware instances arriving at the device
outputsList[LabwareInstance]Labware instances leaving the device
optionsDict[str, Any] | NoneCustom options passed to the action

Accessing Labware

Inputs and outputs are runtime instances, not templates:

async def process_plate(resource, inputs, outputs, options):
for labware in inputs:
print(f"Processing: {labware.name}")
print(f"Type: {labware.labware_type}")
print(f"ID: {labware.id}")

Accessing PyLabRobot Objects

For plates, you can access the underlying PyLabRobot Plate object:

async def dispense_samples(resource, inputs, outputs, options):
plate_instance = inputs[0]
plr_plate = plate_instance.PLR() # Returns pylabrobot.resources.Plate

# Now use PLR plate methods
well_a1 = plr_plate.get_well("A1")

Passing Options

Pass custom data to your function via the options parameter:

async def transfer_volume(resource, inputs, outputs, options):
volume = options.get("volume", 100)
source_wells = options.get("source_wells", "A1:H12")
print(f"Transferring {volume}µL from {source_wells}")

method = MethodTemplate(
name="transfer",
actions=[
PythonMethod(
resource=liquid_handler,
method=transfer_volume,
inputs=[source_plate, dest_plate],
outputs=[source_plate, dest_plate],
options={"volume": 50, "source_wells": "A1:A12"}
)
]
)

Working with the Resource

The resource parameter gives you access to the device:

async def custom_operation(resource, inputs, outputs, options):
print(f"Running on device: {resource.name}")

# Check device state
if resource.is_initialized:
# Access device driver for low-level operations
driver = resource.driver
await driver.some_method()

Async Operations

Since the function is async, you can use await:

import asyncio

async def timed_operation(resource, inputs, outputs, options):
duration = options.get("duration", 5)

print("Starting operation...")
await asyncio.sleep(duration)
print("Operation complete")

Error Handling

Exceptions in your function will propagate and halt the workflow:

async def validated_operation(resource, inputs, outputs, options):
if not inputs:
raise ValueError("No input labware provided")

plate = inputs[0]
if plate.labware_type != "96_well":
raise TypeError(f"Expected 96-well plate, got {plate.labware_type}")

# Continue with operation...

Complete Example

import asyncio
import logging
from typing import Any, Dict, List

from orca.devices.devices import LiquidHandler
from orca.sdk.labware import PlateTemplate, LabwareInstance
from orca.sdk.workflow import MethodTemplate, ThreadTemplate, WorkflowTemplate
from orca.sdk.system import SdkToSystemBuilder, WorkflowExecutor, ResourceRegistry, SystemMap
from orca.sdk.events import EventBus
from orca.sdk.actions import PythonMethod
from orca.sdk.devices import Device
from orca.resource_models.transporter import Transporter
from cheshire_drivers import SimLiquidHandlerDriver, SimTransporterDriver
from pylabrobot.resources.thermo_fisher.plates import Thermo_Nunc_96_well_plate_1300uL_Rb

logger = logging.getLogger("orca")

# Define the custom method
async def dispense_samples(
resource: Device,
inputs: List[LabwareInstance],
outputs: List[LabwareInstance],
options: Dict[str, Any] | None = None
):
"""Custom dispense logic."""
logger.info(f"Running dispense on {resource.name}")
logger.info(f"Processing {len(inputs)} input plate(s)")

volume = options.get("volume", 100) if options else 100
logger.info(f"Dispensing {volume}µL per well")

# Access PLR plate for well information
for plate_instance in inputs:
plr_plate = plate_instance.PLR()
logger.info(f"Plate {plate_instance.name} has {plr_plate.num_items} wells")

# Simulate operation time
await asyncio.sleep(1)
logger.info("Dispense complete")

# Setup
plate = PlateTemplate("sample_plate", Thermo_Nunc_96_well_plate_1300uL_Rb, None)
liquid_handler = LiquidHandler("liquid_handler", SimLiquidHandlerDriver("lh"))
transporter = Transporter("arm", SimTransporterDriver("arm"), "teachpoints.json")

resources = ResourceRegistry()
resources.add_resources([liquid_handler, transporter])

system_map = SystemMap(resources)
system_map.assign_resources({"liquid_handler": liquid_handler})

# Create method with PythonMethod action
dispense_method = MethodTemplate(
name="dispense",
actions=[
PythonMethod(
resource=liquid_handler,
method=dispense_samples,
inputs=[plate],
outputs=[plate],
options={"volume": 50}
)
]
)

# Thread and workflow
thread = ThreadTemplate(
plate,
system_map.get_location("input_stack"),
system_map.get_location("output_stack"),
[dispense_method]
)

workflow = WorkflowTemplate("custom_workflow")
workflow.add_thread(thread, is_start=True)

# Build and run
builder = SdkToSystemBuilder(
"Custom Code Example", "Demo of PythonMethod",
[plate], resources, system_map,
[dispense_method], [workflow], EventBus()
)
system = builder.get_system()

async def run():
executor = WorkflowExecutor(workflow, system)
await executor.start(sim=True)

asyncio.run(run())

Use Cases

Custom liquid handling logic:

async def cherry_pick(resource, inputs, outputs, options):
source_plate = inputs[0]
dest_plate = inputs[1]
well_map = options.get("well_map", {}) # {"A1": "B3", "C5": "D7"}
# Implement cherrypicking logic...

Integration with external services:

async def update_lims(resource, inputs, outputs, options):
plate = inputs[0]
# Call LIMS API to update plate status
await lims_client.update_plate_status(plate.id, "processed")

Conditional processing:

async def conditional_process(resource, inputs, outputs, options):
plate = inputs[0]
sample_count = options.get("sample_count", 96)

if sample_count < 48:
# Use different protocol for partial plates
await resource.driver.run_partial_plate_protocol()
else:
await resource.driver.run_full_plate_protocol()

Next Steps