Skip to main content
Version: 2.1.0

Submissions

A submission is a request to the runtime to run a workflow. Almost every submission is the simple one: run this workflow, once, now.

Submit one workflow​

from orca.runtime.run_modes import WorkflowRunMode

record = await runtime.submit_workflow(
"my_assay",
variables={"dilution_factor": 4.0},
mode=WorkflowRunMode.PURE_SIM,
)

final = await runtime.wait(record.id)

submit_workflow takes the name of a workflow template registered on the system. It returns an ExecutionRecord straight away, while the run proceeds in the background, and record.id is the id every other call keys on.

That one call creates a fresh execution with one labware group: one of each labware thread the workflow declares, so a workflow with a sample plate thread and a tip rack thread runs one of each.

variables overrides variable values for this run only, leaving the template's defaults alone. It is optional. See Variables.

runtime.wait(...) blocks until the run finishes and hands back the record's final state. Check it: a run that paused for an operator and was never answered does not come back completed. To watch it as it goes instead of blocking, see Events or orca monitor. From a terminal, the whole of this simple case is orca run, and the Quick Start runs it end to end in one file.

Run modes​

The mode keyword is required on every submission. There is no deployment-level fallback, and omitting it raises RunModeRequiredError. Three values, chosen per submission:

ModeDevices behind the workflowUse it for
WorkflowRunMode.PURE_SIMPure software simulators. No wire calls, no hardware contact.Authoring and iterating on workflows on a laptop. The fastest mode.
WorkflowRunMode.DEVICE_SIMGateway-routed sim drivers on the device bridge. The wire is exercised; the devices are not real.Validating driver behavior, integration testing, dry runs before a hardware deploy.
WorkflowRunMode.LIVEReal hardware drivers talking to real instruments.Production runs.

Before each execution starts, the runtime checks every device declared in the topology against the requested mode. Submitting LIVE against a topology with disconnected hardware fails fast instead of part way through the run.

A single device can be pinned to simulation while the rest of the lab runs live. See Sim hierarchy for that, and for the run mode rules in full.

Advanced​

Everything below is for submitting several pieces of labware at once, adding work to a run already going, or stopping one.

Two runs at once​

LIVE and DEVICE_SIM cannot run at the same time. Both dispatch over the wire to the same device connections, so a DEVICE_SIM run would switch a device to its sim backend while a LIVE run drives it for real. A submission that would create that overlap is refused with ConcurrentLiveSimRefusedError, naming the execution to wait on.

Two concurrent LIVE executions are fine, so are two DEVICE_SIM ones, and PURE_SIM never reaches the wire, so it runs alongside anything.

The full submit() contract​

submit_workflow is a thin wrapper. The full surface is runtime.submit(...), which takes a built WorkflowTemplate rather than a name:

async def submit(
self,
workflow: WorkflowTemplate,
groups: Sequence[LabwareGroup] = (),
variables: Mapping[str, OptionValue] | None = None,
batch_mode: BatchMode = BatchMode.STANDALONE,
operator_id: str | None = None,
deployment_profile: str | None = None,
mode: WorkflowRunMode | None = None,
acknowledge_warnings: bool = False,
) -> Submission: ...
ArgumentWhat it does
groupsEmpty (the default) is the groupless one-shot path: always a fresh execution, one of each declared thread. Non-empty opts into the grouped path below.
variablesPer-submission variable overrides, applied to this execution only.
batch_modeSTANDALONE (default) or JOIN_EXISTING. Only consulted when groups is non-empty.
operator_idRecorded on the submission for audit. Not interpreted.
deployment_profileName of a deployment profile in runtime.profile_store. Resolved and applied through the variable store once, at submit time. Editing the profile afterwards does not reach the running execution.
modeRequired. See above.
acknowledge_warningsSuppresses the gate that refuses a LIVE submission when topology devices declare a sim_override. The CLI maps this to --confirm. See Sim hierarchy.

runtime.submissions.submit_group(...) is the same call by workflow name, and returns a SubmissionSnapshot.

Labware groups​

A labware group is a bundle of labware that moves through a workflow together: one sample plate plus the tip rack that serves it, for example. submit_workflow always runs a single default group. To submit several, use runtime.submit(...) with groups:

from orca.runtime.submission import BatchMode
from orca.runtime.labware_group import (
LabwareGroup, LabwareGroupMember, LocationAcquisition,
)

template = build.system.get_workflow_template("my_assay")

submission = await runtime.submit(
template,
groups=[
LabwareGroup(id="g1", members=(
LabwareGroupMember(thread_template_name="plate_thread"),
)),
LabwareGroup(id="g2", members=(
LabwareGroupMember(thread_template_name="plate_thread"),
)),
],
mode=WorkflowRunMode.PURE_SIM,
)

Each group spawns its own copy of every required thread. A group id must not contain :, and a group may carry at most one member per thread template.

thread_template_name matches either the @orca.thread-decorated function's name or the labware template's name; both appear naturally in author code. See Labware threads. A group missing a member for a required entry thread is refused at submit time, as is a member naming a template the workflow does not register.

Where a group's labware comes from​

Each member carries an acquisition that says how the engine gets hold of the physical labware. Three variants:

AcquisitionMeaning
PoolAcquisition() (default)Take the next available instance from the template's pool or stacker.
BarcodeAcquisition(barcode)Use the existing labware with this barcode, looked up in the labware store at submit time. An unknown barcode is refused.
LocationAcquisition(source_location, verify_barcode=None)Start from this on-deck position. It overrides the thread template's own start location for this group. verify_barcode, if set, is checked against the scanner reading at pickup.
LabwareGroupMember(
thread_template_name="plate_thread",
acquisition=LocationAcquisition("hotel_slot_3"),
)

The runtime refuses the submission if any entry thread's start location is already occupied, rather than letting the thread retry against the occupied position forever.

Sharing labware across groups​

A labware template can be flagged SHARED_ACROSS_GROUPS so all groups in a submission converge on the same physical instance, which is how several sample plates end up pooled into one final plate:

from orca.resource_models.sharing import GroupSharing
from orca.sdk.labware import PlateTemplate

final_plate = PlateTemplate(
"final_plate",
"Cor_96_wellplate_360ul_Fb",
group_sharing=GroupSharing.SHARED_ACROSS_GROUPS,
)

Two independent flags on a labware template control sharing, both defaulting to the isolated value:

FlagValuesEffect
group_sharingGroupSharing.PER_GROUP (default) / SHARED_ACROSS_GROUPSWithin one submission: each group gets its own instance, or all groups share one.
submission_batchingSubmissionBatching.ISOLATED (default) / BATCHABLEAcross submissions: whether a later JOIN_EXISTING submission may join an in-flight receiver.

Both are silent no-ops in the default single-group, STANDALONE shape. The behavior only activates when the submission opts in: several groups for SHARED_ACROSS_GROUPS, batch_mode=BatchMode.JOIN_EXISTING for BATCHABLE.

Joining a run already in flight​

By default, each submission creates a fresh execution. Pass batch_mode=BatchMode.JOIN_EXISTING (the enum, not the string) to merge into a still-accepting execution of the same workflow:

submission = await runtime.submit(
template,
groups=[...],
batch_mode=BatchMode.JOIN_EXISTING,
mode=WorkflowRunMode.PURE_SIM,
)

This is the mechanism for staged submissions: kick off a long-running multi-plate assay and add more plates to it as samples arrive over hours. Batch mode and groups live only on runtime.submit(...) and runtime.submissions.submit_group(...); submit_workflow does not take them.

The runtime looks for an alive execution of that workflow whose phase is ACCEPTING, and injects your groups into it. It refuses the join in four cases:

  • The execution is DRAINING (someone called close_execution). Resubmit as STANDALONE to start a new execution.
  • The execution is paused. A paused execution stays ACCEPTING, but its pause latch gates new work, so the join is refused rather than queued behind the pause.
  • The execution holds a quarantined slot from an ORPHANED_BACKLOG. Joining would route straight into a slot whose accept-partial resume discards the backlog.
  • The run modes differ. One execution carries one run-mode seed, so a LIVE submission cannot join a PURE_SIM execution.

With no live execution for this workflow at all, JOIN_EXISTING falls through to a fresh build, exactly like STANDALONE.

STANDALONE never joins, and that can leave you with two executions​

STANDALONE always boots a fresh execution, even when an ACCEPTING execution for the same workflow is alive. That is the wire contract, and batch_mode is checked on both the ACCEPTING and DRAINING branches so it cannot silently join.

One consequence to plan for. Submit STANDALONE against a workflow whose existing execution is DRAINING and you get two in-flight executions of that workflow: the draining one runs its threads to completion while the new one starts. The name-keyed index now points at the new execution, so later JOIN_EXISTING submissions target it and the draining one is reachable only by execution id. Both consume lab resources until the draining one finishes. To avoid the overlap, wait for the draining execution to reach a terminal phase first.

Execution phases​

PhaseMeaning
ACCEPTINGRunning and open to grouped JOIN_EXISTING submissions.
DRAININGclose_execution was called. New joins refused; live threads continue.
STOPPINGAn abort was issued and the task cancelled. Threads are unwinding. Not terminal.
COMPLETED / FAILED / ABORTEDTerminal.

Closing an execution​

To stop an execution from accepting new submissions but let in-flight threads finish:

phase = runtime.close_execution(execution_id)

close_execution is synchronous and returns the resulting ExecutionPhase. It is idempotent: on an already-draining or terminal execution it returns the current phase and changes nothing.

Stopping an execution​

The operator stop is a two-call confirmed abort:

outcome = await runtime.stop_execution(execution_id)                  # call 1
outcome = await runtime.stop_execution(execution_id, confirm=True) # call 2

Call 1 pauses the execution immediately and arms the abort. It returns StopOutcome(armed=True, aborted=False, phase=...); nothing has been aborted, and resuming the execution disarms it. Call 2, with confirm=True on an armed and still-paused execution, runs the abort and waits for the execution to reach its terminal phase, so the answer reports ABORTED rather than a transient STOPPING.

Two distinct intentful calls are always required. A confirmed call that arrives while nothing is armed only arms; it never aborts. On the daemon this is POST /operations/stop-execution; on the CLI, orca execution stop.

await runtime.abort_execution(execution_id) is the raw single-call primitive underneath. It cancels every thread immediately, including dynamically spawned contributors, with no confirmation and no pause. If you want the run to survive instead, pause it and answer the incident: see Recovery.

How the terminal phase is decided​

When an execution's threads all reach a terminal state, the runtime rolls the execution up to a single terminal phase:

  • COMPLETED only if every thread completed.
  • FAILED if the execution task raised, or if any thread ended in the crash-only FAILED status (a thread whose task died on an unhandled error). FAILED takes precedence over ABORTED in the rollup.
  • ABORTED if any thread was aborted or stopped. A clean task return is not enough on its own: ABORT_THREAD and a cooperative stop both return without raising.

What nobody has settled​

The submit response carries an unsettled list: everything the record cannot answer right now, as of this submission. It is reported, never refused. Committing a deck to a run is the one moment an operator is certainly paying attention, and the same list read any other way needs somebody to think of asking for it. Empty is the ordinary case.

Each entry names the subject, why it is unsettled, and the verb that settles it. That verb can be null, which means no verb settles it yet: an unfinished action is holding operations nobody has folded.

If the read itself fails, the submission still succeeds and the list comes back empty. A report about work never refuses the work. See Runtime state.

See also​

  • Runtime: the service that accepts submissions and runs them.
  • Sim hierarchy: the three run modes, and pinning one device to simulation.
  • Workflows: what you are submitting, and how threads compose into one.
  • Recovery: what happens when a submitted execution hits an incident.
  • CLI: run a workflow: the same submissions from a terminal.