Workflows
A workflow is the top-level orchestration unit. It declares which threads exist, which one starts the workflow, what variables it accepts, and which event handlers fire during execution. Workflows are what you submit to the runtime.
The basic shape
@orca.workflow(name="my_assay")
def my_assay(wf: WorkflowContext):
wf.start(sample_journey)
wf.thread(tips_journey)
wf.thread(trough_journey)
The workflow function is a builder — it receives a WorkflowContext (wf) and calls methods on it to declare structure. The function runs once at decoration time to register everything.
| Builder call | Declares |
|---|---|
wf.start(thread) | The starting thread — kicks off when the workflow is submitted. |
wf.thread(thread, capacity=None, overflow_strategy=None) | A secondary thread, spawned when its trigger fires (a join, a contributing thread). capacity sets a slot-level CapacityPolicy; overflow_strategy attaches a custom IOverflowStrategy. |
wf.on(event_name, handler) | An event handler that fires for matching events during this workflow's executions. |
wf.variable(name, definition) | A variable that submissions can override. |
Starting threads vs secondary threads
wf.start(...) declares the entry point — the thread that always spawns first when an execution begins. wf.thread(...) declares additional threads that spawn later, usually triggered by:
- A primary thread reaching a
orca.join()point (the joined thread spawns to meet it). - A contributing thread completing its journey (a "contributes_to" target spawns).
- A submission group explicitly asking for them.
Joining threads
A thread can pause and wait for other threads to converge, using orca.join():
@orca.thread(labware=tips, start="stacker", end="waste")
async def tips_journey(ctx: ThreadContext):
yield orca.join(allows=[transfer_step, dilute_step])
This thread parks at its start location until something pulls it into a method. Use this for shared resources (tips, troughs) that join other threads' methods rather than running their own.
Branching on events
orca.branch(...) picks one of several method sequences based on an emitted event value:
@orca.thread(labware=plate, start="stacker", end="waste")
async def assay(ctx: ThreadContext):
yield run_step
yield evaluate_qc_step # emits "qc_result" with value "pass" or "fail"
yield orca.branch("qc_result", {
"pass": [seal_step],
"fail": [retry_step, evaluate_qc_step],
})
The branch waits for the named event to fire, then runs the matching method list.
Variables
Workflows can declare variables that submissions override at submit time:
from orca.variables.variable_definition import VariableDefinition
@orca.workflow(name="dilution_assay")
def dilution_assay(wf: WorkflowContext):
wf.variable(
"dilution_factor",
VariableDefinition(
type="float",
default=10.0,
min=2.0,
max=100.0,
description="Serial dilution factor",
),
)
wf.start(plate_journey)
Inside an action: factor = await ctx.param("dilution_factor", float). ctx.param is async, so await it. Pass the expected type as the second argument for a typed return value; omit it for the untyped form.
See Variables for the full type taxonomy, validation rules, scoping (global vs workflow), and the five-layer resolution order.
Event handlers
@orca.workflow(name="my_assay")
def my_assay(wf: WorkflowContext):
wf.start(plate_journey)
wf.on("ACTION.COMPLETED", MyAuditHandler())
Handlers receive every matching event during this workflow's executions. Use them for logging, audit, or in-process integrations.
TODO: handler protocol details, available events, scoping.
Where to go next
- Threads — defining the journey of a single piece of labware.
- Submissions — submitting a workflow with groups and variables.