Skip to main content

Stores

The SystemRuntime accepts a handful of small store interfaces, one each for the kinds of state the runtime needs to read and write. The framework ships SQLite-backed service defaults (plus an in-memory labware store, a JSONL ops-history store, and a Null/file profile store). You can swap any of them for a custom implementation that talks to a database, a remote service, or whatever else suits your deployment.

Most store interfaces live in orca.runtime.interfaces (IIncidentStore is the exception; it lives in orca.runtime.incident_store).

The full set

Most stores follow a Service-over-Store split: a *Service owns the orchestration (locking, events, lifecycle) and delegates raw persistence to an injected store that satisfies the store Protocol. The framework defaults are SQLite-backed.

InterfaceHoldsDefault impl
IEventSinkReceives RuntimeEvents as they fire. External integration point.None (sinks are registered explicitly).
ILabwareStoreLabware identity, location, history, relationships.InMemoryLabwareStore
IAccessConfigStoreNamed approach/retract patterns referenced by teachpoints.AccessConfigService over SqliteAccessConfigStore
ITeachpointStoreNamed transporter positions.TeachpointService over SqliteTeachpointStore
IDeckLayoutStoreNamed PLR deck configurations per liquid handler.DeckLayoutService over SqliteDeckLayoutStore (NullDeckLayoutStore when a LiquidHandler is given no store)
IDeploymentProfileStoreNamed variable-default bundles loaded at submit time.NullDeploymentProfileStore (or FileDeploymentProfileStore for a directory of JSON files)
IIncidentStoreQueryable record of non-action runtime errors. A Protocol, injected via IncidentService.IncidentService over SqliteIncidentStore
IExecutionRecordStorePersisted per-execution records. Injected via ExecutionRecordService.ExecutionRecordService over SqliteExecutionRecordStore
IOpsHistoryStoreAppend-only operational history (ops-history search).JsonlOpsHistoryStore.ephemeral()
labware catalog storeLabware DEFINITIONS catalog (the single CRUD path). Injected via LabwareCatalogService.seeded_labware_catalog_service() (over SQLite)

Notes:

  • IEventSink is a Protocol with one method: on_event(event: RuntimeEvent) -> None. Register sinks on the runtime via runtime.register_sink(...). See Events.
  • IIncidentStore IS a Protocol (satisfied by SqliteIncidentStore); you extend it by implementing the Protocol and injecting your store via IncidentService, not by subclassing a concrete class. IStateStore does not exist in orca-core. A hosted deployment ships database-backed stores behind these same interfaces as a separate product layer.

Wiring stores into a runtime

The runtime constructor takes the labware store directly plus a set of services (which each wrap a store), not a store per concern:

from orca.runtime.system_runtime import SystemRuntime
from orca.runtime.labware_store import InMemoryLabwareStore
from orca.runtime.incident_service import IncidentService

runtime = SystemRuntime(
build.system,
event_bus=build.event_bus,
labware_store=InMemoryLabwareStore(),
incident_service=IncidentService(MyCustomIncidentStore()),
# also: execution_record_service, access_config_service, profile_store,
# ops_history_store, labware_catalog_store, gateway_registry,
# connection_source.
)

Teachpoints and deck layouts are managed via their services on the built System, not via a SystemRuntime kwarg. Any service or store you don't pass uses the framework default: in-memory SQLite for access-configs, teachpoints, deck layouts, incidents, and execution records; a JSONL store for ops-history; a plain in-memory store for labware; and a Null store for profiles. SystemRuntime(build.system, event_bus=build.event_bus) is a valid call.

Interface surfaces

The interfaces are small and mostly CRUD. Each protocol's docstring in interfaces.py is the canonical reference; the highlights below cover what's most important to know when writing a custom backend.

ILabwareStore

class ILabwareStore(Protocol):
async def register(self, instance: LabwareInstance, execution_id: str | None = None) -> None: ...
async def get_by_id(self, labware_id: str) -> LabwareInstance | None: ...
async def get_by_barcode(self, barcode: str) -> LabwareInstance | None: ...
async def update_location(self, labware_id: str, position_id: str) -> None: ...
async def get_by_position(self, position_id: str) -> LabwareInstance | None: ...
async def list_active_locations(self) -> list[tuple[str, str]]: ...
async def get_location_history(self, labware_id: str) -> list[tuple[str, str | None, float]]: ...
async def record_relationship(self, relationship: LabwareRelationship) -> None: ...
async def get_relationships(self, labware_id: str) -> list[LabwareRelationship]: ...
async def delete(self, labware_id: str) -> None: ...

delete must be idempotent: unknown ids are no-ops so the panic-chain operator surfaces can sweep best-effort without choking on partially-removed state. get_by_position returns the labware persisted at a position (most-recently-registered wins on ties) so reuse-bind rebinds a resident reagent to its stable identity.

ITeachpointStore

class ITeachpointStore(Protocol):
async def get(self, position_id: str) -> Teachpoint | None: ...
async def resolve(self, position_id: str) -> Teachpoint | None: ...
async def list(self) -> list[Teachpoint]: ...
async def add(self, teachpoint: Teachpoint) -> None: ...
async def update(self, teachpoint: Teachpoint) -> None: ...
async def delete(self, position_id: str) -> bool: ...
async def create_schema(self) -> None: ...
async def aclose(self) -> None: ...

Surfaces worth knowing:

  • resolve(position_id) is the wire-source-of-truth: it returns a fully-flattened Teachpoint with AccessConfig fields inlined. Use this for handing teachpoint payloads off to a remote driver. The store is consulted at every dispatch, so mid-run mutations are visible to the next move.
  • create_schema / aclose are the persistence lifecycle hooks the owning TeachpointService calls (create the table for an in-memory default; release resources on shutdown).

add raises on duplicate position_id. update raises on unknown position_id. delete returns True iff something was removed.

IDeckLayoutStore

class IDeckLayoutStore(Protocol):
async def get(self, name: str) -> DeckLayoutConfig | None: ...
async def list(self) -> list[tuple[str, DeckLayoutConfig]]: ...
async def add(self, name: str, config: DeckLayoutConfig) -> None: ...
async def update(self, name: str, config: DeckLayoutConfig) -> None: ...
async def delete(self, name: str) -> bool: ...
async def create_schema(self) -> None: ...
async def aclose(self) -> None: ...

Deck layouts are rebuild-required. A running LiquidHandler has its deck configured once at SystemRuntime.start and cannot be reconfigured mid-run; physical labware can't reposition while motion is in flight. Edits to the registry take effect on the next RuntimeLifecycle.rebuild().

build_system awaits the async get to derive child Locations from the layout's carriers, reading the live store with no sync snapshot in front of it. create_schema / aclose are the persistence lifecycle hooks the owning DeckLayoutService calls.

IAccessConfigStore / IDeploymentProfileStore

Both follow the same CRUD shape: get returns None for unknown names, add raises on duplicates, update raises on unknown, delete returns the boolean changed-state. IAccessConfigStore also carries create_schema / aclose lifecycle hooks (its owning AccessConfigService calls them); IDeploymentProfileStore does not.

IDeploymentProfileStore editing a profile does not affect a running execution: the new body is picked up at the next submission that names this profile.

IIncidentStore (Protocol)

IIncidentStore is a Protocol, not a concrete class. Extend it by implementing the Protocol and injecting your store via IncidentService, not by subclassing. The store is the dumb per-DB persistence layer (raw async reads/writes); IncidentService owns the off-loop record queue, locking, and event emission.

from datetime import datetime
from orca.runtime.incident_store import IIncidentStore, IncidentCategory, SystemIncident
from orca.runtime.incident_service import IncidentService

class MyDbIncidentStore: # satisfies IIncidentStore structurally
async def create_schema(self) -> None: ...
async def insert(self, incident: SystemIncident) -> None: ...
async def get(self, incident_id: str) -> SystemIncident | None: ...
async def fetch(self, *, unacknowledged_only: bool = False,
category: IncidentCategory | None = None,
execution_id: str | None = None,
since: float | None = None) -> list[SystemIncident]: ...
async def mark_acked(self, incident_id: str, acknowledged_at: datetime) -> None: ...
async def mark_all_acked(self, category: IncidentCategory | None,
acknowledged_at: datetime) -> int: ...
async def aclose(self) -> None: ...

runtime = SystemRuntime(
build.system,
incident_service=IncidentService(MyDbIncidentStore()),
)

The full incident category taxonomy is documented in Recovery.

When to swap a default

Use caseSwap what
Operator UI needs to query labware history months backILabwareStore → DB-backed
Teachpoints managed in an external system, not the runtimeITeachpointStore → custom store behind TeachpointService
Incident logs need to survive restarts and feed an alerting pipelineIIncidentStore → custom store behind IncidentService
Approach/retract patterns defined per-deployment in a databaseIAccessConfigStore → DB-backed
Profiles managed in a central admin tool, not on the local filesystemIDeploymentProfileStore → admin-tool-backed
Events need to land in Postgres + a websocket fan-outRegister an IEventSink per channel (see Events)

Most local deployments run on the SQLite-backed defaults plus an event sink or two. Database-backed everything is what a hosted deployment does, but that's a different product layer; the framework doesn't require it.

Pitfalls

  • Stores are async all the way down. build_system awaits the async store reads it needs (teachpoint list, deck-layout get); there is no sync list_sync / peek snapshot to reconcile. A custom store just implements the async Protocol methods.
  • delete semantics differ by store. ILabwareStore.delete is idempotent (returns nothing, unknown id is no-op). ITeachpointStore.delete / IAccessConfigStore.delete / IDeckLayoutStore.delete return True only if something was removed.
  • Event sinks are NOT stores. They're write-only observers. To query historical events, persist them via a sink and read from your own backing store. See Events.
  • There's no IStateStore in orca-core. IIncidentStore exists but as a Protocol you implement (and inject via IncidentService), not a class you subclass.

Where to go next

  • Events — the sink protocol and event taxonomy.
  • Recovery: what kinds of incidents the incident store records and how operators decide what to do with them.