Skip to main content

labcode

CI PyPI

The lc command-line interface for the labcode dialect of the Object-flow Programming Language. Installing this one package pulls in the ofplang toolchain and exposes it under a single command:

lc validate ...   # check a workflow is well-formed portable v0
lc schedule ...   # compute a schedule for a workflow
lc run ...        # execute a workflow on the labcode backend
lc export ...     # write documents out for people to read (optional: labcode[export])

labcode is where a site-specific dialect and a custom runner (real lab hardware) are developed on top of the ofplang toolchain. lc validate and lc schedule forward to the ofplang siblings unchanged; lc run is the labcode dialect's own runner: it drives the workflow on the labcode backend, running each device operation's script — supplied in the environment as an x-labcode.script extension on a process mode — out-of-process on a wall clock, so a long-running real operation never blocks the replan loop. See docs/SPECIFICATIONS.md for the x-labcode extension.

# in the execution environment: how a (process, mode) is carried out
processes:
  measure_od:
    modes:
      - id: v0
        duration: 45
        x-labcode:
          script:
            language: python
            code: |
              return {"od": read_plate(plate)}

Install

pip install labcode

Requires Python 3.10+. lc validate and lc schedule are dispatched to the ofplang sibling packages unchanged; lc run is this package's own runner, built on them:

The language is defined in the ofplang/spec repository, and what labcode adds to it in docs/SPECIFICATIONS.md.

Talking to instruments, recording a run and exporting documents are optional, each an extra installed into the interpreter that runs lc (labcode runs every script with that same interpreter):

extra brings needed for
sila2 the sila2 client library flavor: sila2 scripts (§1.7)
lads asyncua, an OPC UA client flavor: lads scripts (§1.10)
otel the OpenTelemetry SDK and exporter lc run --trace
export ofplang-export lc export view — the workflow and its plan as one self-contained HTML file
pip install 'labcode[sila2,lads]'

Without an extra, labcode still runs; only what needs it — a script of that flavor, --trace, or lc export — fails, with a message naming the extra to install.

What lc run brings of its own, beyond dispatching:

  • the labcode backend — each operation's x-labcode.script runs out-of-process on a wall clock (§1.2–§1.4), so a real operation that takes minutes does not block the replan loop, and an operation that never returns is stopped by op_timeout (§1.9; --op-timeout / --no-op-timeout).
  • refilling a stock — where the environment says a replenisher can reach a device, a stock that would run out is topped up rather than ending the run: the refill's own script runs like any other, holding both machines while it works (§1.4). See Refilling a stock below.
  • the dialect front door — the environment's x-labcode extension is validated before anything runs, on top of the portable-v0 check lc validate performs (§1, §2).
  • availability probing — each machine is checked as often as its probe policy says, and one that cannot be reached is taken out of the environment the scheduler plans against, so the run routes around it (§1.6; --no-probe).
  • object identity — the reserved _id view key is declared on Object types and minted per object, so a physical thing can be followed through a run (§4).
  • flavor: sila2 — a script that speaks SiLA2 gets its clients opened around it (§1.7). The client library is the sila2 extra.
  • flavor: lads — the same for LADS OPC UA: a script is handed one LADS functional unit per machine, already connected, with the program runs, state-machine methods and cover movements LADS defines and the waits each needs (§1.10). A connection may name the device and unit on the server, or leave them to be found. A script speaks one protocol; an operation holding machines of both kinds commands only its own flavor's. The client library is the lads extra.
  • recording a run — with --trace, what the run did is recorded as OpenTelemetry traces: one trace per run, a span per operation, and — measured inside the process that issued them — a span per SiLA2 connection, per command, and per gRPC call each of those made. Off by default; it needs the otel extra. A lads operation is recorded as its span alone for now.

Usage

Each subcommand keeps its own options, exit codes, and --help:

lc --help            # top-level help
lc <command> --help  # command-specific options
lc --version

lc can also be run as a module: python -m labcode <command> ....

lc validate, lc schedule and lc export are the ofplang siblings' own CLIs unchanged, so their options are documented in those repositories — lc export view plan.yaml -o plan.html, for one, writes the workflow and its plan as a single HTML file (ofplang/export; needs labcode[export]). lc run is this package's own, and is described below.

lc run

lc run <workflow> --env <env>
    [--boundary DOC] [-o OUT] [--boundary-out FILE] [--observation-out FILE]
    [--seconds-per-tick S] [--op-timeout S | --no-op-timeout] [--no-probe]
    [--max-transport-legs N] [--ignore-resources] [--trace] [--mission-id ID]
    [--object-ids seeded|real]

lc run --jobs <run doc> --env <env> [--on-job-failure continue|stop] [...]
  • <workflow> — the portable v0 workflow: what happens.
  • --jobs RUNDOC — run several jobs together in one laboratory (schedule SPEC §6.11) in place of the single <workflow>. The run document names each job — an id, its workflow, its own boundary, and when it may start — plus what the laboratory's own stocks hold to begin with and which spots it is already holding. They are planned together, so they compete for the same machines and share a refill neither needs alone. Each job's workflow goes through both front doors separately and a rejection names the job it came from; a boundary belongs to a job, so --boundary is not used with --jobs. 🔴 Object identities are minted per job: two jobs of one workflow bind the same port names and render the same node paths, so a reproducible generator keyed on those alone would give one job's plate the other's _id. --trace records the job on every operation (ofp.job) for the same reason — the node alone does not say whose work it was. A run of a single workflow names no job and is recorded exactly as it always was.
  • --on-job-failure continue|stop — what one job's failure does to the rest of a --jobs run. continue (the default) stops that job alone and lets the others finish — which is why they were planned together — while stop stops the whole run. A stopped job's plate stays where it is, and the plan is made around it rather than onto it. A single workflow is a single job, so this makes no difference to it.
  • --env (required) — the labcode environment: the execution environment (spec §5) plus the x-labcode extension saying how each operation is carried out.
  • --boundary DOC — the whole-workflow I/O as one document: a boundary: mapping with a {spot, view} descriptor per entry input / final output port. spot says where a boundary Object sits; view supplies an input's value. A workflow with Object-bearing entry inputs needs one, since each must be placed on a spot and only the operator knows where the labware is. Where a device declares a consumable and some mode draws on it, an inventories: {levels: ...} section says what each stock holds at the start of the run — the level later on is never stated, it is worked out from that and what the run has done since. It is not echoed into --boundary-out, because that document is written to be fed back and the next run would take this run's opening stock for its own.
  • -o OUT — write the final execution status (spec §6/§7) here; the default is stdout.
  • --boundary-out FILE — write the result boundary: the same schema as --boundary, with each produced output's view filled in, including the _id its Object was minted with (§4) — which is how one checks that the plate that came back is the plate that went in.
  • --observation-out FILE — stream the observation document: each completed activity's concrete input / output view values, appended as it finishes. What the instruments reported, as against the status document's timings.
  • --seconds-per-tick S — real seconds per environment time tick (default 20). Durations in the environment are counted in ticks, and this is what maps them onto the wall clock. The default is deliberately coarse, so that a real operation's dispatch → running → completed reads as discrete, observable steps; a demo against a fast mock wants a small value.
  • --op-timeout S / --no-op-timeout — how long one operation may run before it is stopped and failed (§1.9). The default is the environment root's x-labcode.op_timeout, else 7200 real seconds. The two forms exclude each other.
  • --no-probe — ignore the environment's x-labcode.probe policies and treat every machine as reachable (§1.6). The documents are still validated.
  • --max-transport-legs N — how many transport activities one Object-bearing arc may be carried in (schedule spec §6.4.1), joined by relay activities. It is 1 by default: the single hop. Raise it where the transporter cannot reach an instrument from where the plate is — a bench it serves from one position only, or a hand-off station a plate has to cross — and the move is planned as the shortest chain between the two spots instead of being reported unreachable. Only the fewest possible moves are offered, so a plate one move away is never sent round by way of somewhere else. This describes the laboratory's reach rather than tuning the loop, which is why it is here and not among the replan knobs below.
  • --ignore-resources — switch the consumable model off. The environment's resource declarations are still checked for shape but none is applied, so a bench whose devices declare stocks nobody is tracking runs without the boundary saying what they held.
  • --trace — record what the run did (see below). Off by default.
  • --mission-id ID — the campaign this run belongs to. Recorded with the run and given no meaning by labcode: several runs may share one, and nothing here reads it back.
  • --object-ids seeded|real — how Object _ids are minted: seeded is reproducible (the same workflow yields the same ids every run, which is what keeps the examples' recorded output stable), real is unique per run. Unset, it follows --trace.

Exit codes: 0 the run completed, 1 it failed (an activity failed, a contract was violated, an operation timed out, or a replan became infeasible), 2 a usage or input error — including a workflow or an x-labcode extension the front doors reject.

A complete invocation, against this repository's examples/ (--seconds-per-tick small because that example's scripts return instantly):

lc run examples/plate_line.workflow.yaml --env examples/plate_line.env.yaml \
  --boundary examples/plate_line.boundary.yaml --seconds-per-tick 0.2

The remaining options tune the replan loop rather than describe the run — --poll-interval, --margin, --seed, --speed, --max-ticks, --no-validate — and are covered by lc run --help.

Refilling a stock

Where a device declares a consumable and the environment says a replenisher can reach it, a stock that would run out is topped up instead of ending the run. The procedure goes on the replenishments[] route — the pair is what has a procedure, while the machine has only an address, the same division transporters and transports have:

replenishers:
  - id: dispenser
    x-labcode:
      connection: { kind: sila2, host: 10.0.0.9, port: 50055, insecure: true }

replenishments:
  - replenisher: dispenser
    device: reader
    duration: 4                 # ticks: the scheduler's estimate of the visit
    x-labcode:
      script:
        language: python
        code: |
          import time
          time.sleep(80)        # real seconds: what the visit actually takes

The script is handed replenisher, device and the amounts the scheduler derived, and is expected to put that in. It is not handed the duration: a real refill takes as long as it takes, so a stand-in says so in its own code — which is why the two numbers above are written separately. Like a transport script it returns nothing; it acts.

A route with no script runs as a timed visit: both machines are held for the declared duration and nothing is commanded. That is a real thing to write (an operator tops the stock up while the schedule waits for them) and an easy one to write by accident, so it is warned about.

flavor: sila2 and flavor: lads are refused on a refill route for now: such a script is handed clients, and which machine's clients a refill should receive — the replenisher's, or both ends' as a transport may ask for — is not settled. Use raw (the default), which may connect for itself.

A refill holds the device it fills and the replenisher filling it, so it never overlaps the work it feeds. It is recorded (--trace) as a replenishment span naming both machines.

Recording a run

--trace records what the run did as OpenTelemetry traces. It needs the extra, in the interpreter that drives the run — which is also the one that runs the scripts, since labcode launches each with sys.executable:

pip install 'labcode[otel]'
lc run <workflow> --env <env> --trace --mission-id M-2026-001

One run is one trace, and the id it can be found by is printed to stderr as the run starts (lc run: recording this run as trace …). What it holds:

run                                  mission.id, and the failure if it stopped on one
├─ process Seal                      which node, process and mode; the plan's interval;
│  │                                 which Objects it handled
│  ├─ sila2.connect                  the address, measured in the process that connected
│  │  └─ /…/SiLAService/GetFeatureDefinition   one per feature, × however many
│  └─ sila2 SealerControl.Seal       the command, from its start to its real completion
│     └─ /…/SealerControl/Seal       the round trip that started it
└─ transport                         the route and the transporter

An operation's span is opened when it is dispatched and closed when a poll finds it finished, so its end is late by up to one poll period; what an instrument actually spent is in the command spans, which are measured where the commands are issued. ofp.object.id lists the _ids an operation handled, including one it created — which is what makes "everything that happened to this plate" a single query.

The innermost layer is the gRPC calls themselves, each under the connection or the command that issued it — so what a connection spends is broken down into the feature definitions it had to fetch, and a command's span separates its round trip from the time the instrument then took. It needs grpcio, which arrives with the sila2 extra; without it the record is the same minus that layer. Two things it does not do: an observable command's execution-info subscription is not recorded (it is read on a thread of sila2's own, where it would land in a trace of its own, and its duration is the command's anyway), and each recorded call sends the trace context to the instrument in its gRPC metadata, which a SiLA2 server ignores as it does any key that is not SiLA Client Metadata.

Where the record goes is configured by the standard OTEL_* environment variables (OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME, …), so labcode adds no settings of its own; service.name falls back to labcode if nothing sets it. LC_TRACE_FILE=path additionally writes the spans to a file as JSON lines, one file per process (the name gets the process id), which is how a record can be read without standing up a collector.

Two things to know before pointing a run at a collector. --trace makes Object _ids real rather than reproducible unless --object-ids says otherwise, since a reproducible id is the same on every run and would collapse several runs' plates into one. And a collector that is configured but does not answer delays each operation, by as long as OpenTelemetry's own export timeout allows: labcode sets no timeout of its own, so OTEL_EXPORTER_OTLP_TIMEOUT is the knob, and it belongs to whoever pointed the run there.

Examples

examples/ holds worked runs of two kinds.

  • Runnable with nothing but Python — plate_line, an Object-bearing line driven entirely by environment scripts, and shared_bench, two jobs sharing one laboratory (--jobs).
  • Against real instrument servers — sila2_seal (one instrument), sila2_plate_cycle (a circuit through four instruments and an arm) and sila2_plate_cycle_no_atc (the same circuit on a bench without a thermal cycler), over SiLA2; and lads_seal and lads_plate_cycle, the first two workflows unchanged with every machine reached over LADS OPC UA instead. sila2_seal also walks through what a run does when a machine stops answering — before an operation, and in the middle of one.

The servers they are verified against are the reference lab, ofplang/mocklab: mock instruments serving SiLA2 or LADS OPC UA by Docker Compose profile, over one simulated world. It is a reference, not a requirement — the environments speak plain SiLA2 and LADS, so a bench is a change of hosts and ports. run_all_sila2_examples.py and run_all_lads_examples.py run each set as a check.

License

MIT

Metadata

Release files for labcode 0.10.8

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for labcode 0.10.8
File Size Uploaded Provenance Actions
labcode-0.10.8.tar.gz 261.0 kB With provenance Details for labcode-0.10.8.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for labcode 0.10.8
File Interpreter ABI Platform Actions
labcode-0.10.8-py3-none-any.whl Python 3 none any Details for labcode-0.10.8-py3-none-any.whl

Total release size: 370.8 kB

Release files / labcode-0.10.8.tar.gz

Download URL labcode-0.10.8.tar.gz
Size 261.0 kB
Tags Source
SHA-256 checksum
How to use checksums
7669b3110bbeeb9fc8cf35a8162f64ec8f5cbe8cabafa1edab9ba0ec54f870e4
BLAKE2b-256 checksum
How to use checksums
bfbe37864416e2be42e20b4787991bfd79d161a2c98da8c99d91a7de90668de2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release files / labcode-0.10.8-py3-none-any.whl

Download URL labcode-0.10.8-py3-none-any.whl
Size 109.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f284197f624800ab7ce7f9e095c654a168082c514104a169dc22b97d29a82e34
BLAKE2b-256 checksum
How to use checksums
23b86954731e67b3ac374808822f2291725598d6353290a8e6a23916b58cec92
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Security for labcode 0.10.8

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for labcode-0.10.8.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.10.8 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page