Skip to main content

Methods

A method is a named, reusable sequence of actions. An action is one step at one device; a method is the ordered list of them, and its actions may sit on different devices.

Your first method

@orca.method
async def warm_and_shake(ctx: MethodContext):
yield warm # on the warmer
yield shake # on the shaker; the transporter moves the plate first

The body is an async generator that yields actions in order. The method takes its name from the function name, and @orca.method with no parentheses is the whole of the common case.

When a method yields an action, the transporter brings the labware to that action's device if it is not already there, the action runs, then the next action in the method runs, possibly on a different device.

Custom Python between the steps

A method body is ordinary async Python, so you can compute between yields:

@orca.method
async def conditional_step(ctx: MethodContext):
if await ctx.param("include_wash"):
yield wash_action
yield read_action

MethodContext is deliberately small. It has ctx.device(name), ctx.labware(name), await ctx.param(name) and await ctx.emit(...). ctx.device takes a device name here, because a method can span devices; the action-level ctx.device() takes none. There is no wait_for on MethodContext: wait at action scope or thread scope instead.

See the Context API reference for the full surface.

Reusing a method

Define a method once and yield it from several labware threads:

@orca.method
async def seal_step(ctx: MethodContext):
yield seal

@orca.thread(labware=plate_a, ...)
async def plate_a_thread(ctx: ThreadContext):
yield seal_step

@orca.thread(labware=plate_b, ...)
async def plate_b_thread(ctx: ThreadContext):
yield seal_step # same template, its own execution

Each thread that yields the template gets its own executing method. The two plates are sealed independently.

Method names are unique per workflow, not per deployment. Two workflows may each declare a seal_step; two distinct methods with the same name inside one workflow raise a collision error when the workflow is registered.

Advanced

Two things are left: setting a failure policy on the whole method, and the case where several threads meet at one method instead of running their own copy.

Setting a failure policy on the method

from orca.workflow_models.status_enums import FailurePolicy

@orca.method(failure_policy=FailurePolicy.ABORT)
async def critical_step(ctx: MethodContext):
yield delicate_action

failure_policy is the only parameter, passed by keyword. It defaults to PAUSE.

Setting it on the method is the older authoring path. When an action raises, the engine reads the policy off the executing action, so an action-level failure_policy is what actually decides. See Failure policies for both values and the recovery decisions a PAUSE offers.

Shared methods: owner and contributors

Threads converge on one executing method only when a contributor thread yields orca.join(...) for it. That is the difference from the reuse case above.

@orca.method
async def transfer_step(ctx: MethodContext):
yield transfer # inputs=[sample_plate, tips]

@orca.thread(labware=sample_plate, start="stacker", end="waste")
async def sample_thread(ctx: ThreadContext):
yield transfer_step # owner: yields the method itself

@orca.thread(labware=tips, start="tip_stacker", end="waste")
async def tips_thread(ctx: ThreadContext):
yield orca.join(allows=[transfer_step]) # contributor

What each side does:

  • The owner drives the method. It resolves each action, reserves the device, and runs the action body.
  • A contributor registers itself on the method and waits for the owner to bind each action, then meets it at the device. It does not resolve actions and it does not auto-spawn labware for them.
  • Failure is group-wide. If the owner's action fails, every thread converged on that action pauses as a unit, so the operator sees one stuck action rather than a scattered set of paused threads. One recovery decision, fed through any participant, propagates to all of them. See Recovery.

Contributors register and deregister around each join, so a receiver looping on while ctx.has_more_work() joins one contribution at a time. See Labware threads for the thread side of this.

See also