Variables
Workflows declare variables — typed, validated values an author can read inside actions and an operator can override at submit time, mid-flight, or globally. The variable store ships with the framework; you opt in by declaring VariableDefinitions.
from orca.variables.variable_definition import VariableDefinition
Two scopes
| Scope | Reference syntax | Where declared |
|---|---|---|
| Workflow | bare name: dilution_factor | On a @orca.workflow — local to that workflow template. |
| Global | global. prefix: global.reagent_batch | At system level — visible from every workflow. |
The two scopes are strict — a bare workflow lookup does NOT fall through to a global of the same name. To read a global from inside a workflow, write global.<name> explicitly.
Declaring workflow variables
@orca.workflow(name="smc_assay")
def smc_assay(wf: WorkflowContext):
wf.variable("plate_count", VariableDefinition(
type="int", default=1, min=1, max=96,
description="How many sample plates to spawn",
))
wf.start(sample_journey)
Declaring global variables
Done at system-build time via the registry rather than per-workflow. See System building for where this hooks in.
VariableDefinition fields
class VariableDefinition(BaseModel):
type: Literal["int", "float", "str", "bool"] = "str"
default: OptionValue | None = None
min: float | None = None
max: float | None = None
allowed_values: list[OptionValue] | None = None
description: str = ""
unit: str = ""
| Field | Effect |
|---|---|
type | One of int, float, str, bool. Defaults to str. |
default | Value used when no override is set anywhere up the resolution chain. |
min / max | Range bounds for numeric types. Ignored for strings and bools. |
allowed_values | Whitelist. Any value not in the list raises on validation. |
description | Human label for UI / tooling. |
unit | Free-form unit string ("mL", "°C") for UI / tooling. |
Validation behavior
validate_value(name, value) runs every time a value is set. Type rules:
boolis rejected whenintorfloatis expected (Python'sboolis technically a subclass ofint, so the validator guards against it).intis accepted wherefloatis expected (widening), not the other way around.min/maxonly check numeric types; bool / string values skip the range check.allowed_valuesruns unconditionally if set.
A failed validation raises VariableValidationError and the set call fails. Engine code reading the value never sees an invalid value.
Resolution layers
Five layers, evaluated top-down. First match wins.
| Layer | Set by | Scope |
|---|---|---|
| 1. Submission partition | var set --submission <id> / submissions_submit(variables={...}) | Per-submission inside one execution |
| 2. Execution partition | orca var set <exec> <key> <value> / --vars KEY=VAL at submit | One execution |
| 3. Global value layer | orca var set-global <key> <value> | Deployment-wide. Globals only. |
| 4. Computed | Expression string registered via register_computed | Both scopes. Lazy-evaluated at read time. |
| 5. Definition default | default= on VariableDefinition | The scope it was declared in. |
For a global.name lookup the engine walks all five. For a bare name it walks 1, 2, 4, 5 — the global value layer is skipped because scopes are strict.
Mid-flight semantics
A var set mid-flight takes effect on the NEXT read. Threads that already cached a value see the old one until they read again. The variable store is thread-safe (uses a threading.Lock), so concurrent set+resolve from different threads is safe.
Computed variables
A computed variable is an expression string evaluated lazily at read time:
# At system-build / workflow-build time
store.register_computed({
"total_volume": "plate_count * 200",
})
The expression sees:
- workflow-definition defaults (for the executing workflow)
- global-definition defaults
- the global value layer
- the execution partition
Standard Python operators + basic numeric / string ops. The expression evaluator is sandboxed — no imports, no attribute access, no function calls beyond a small allow-list.
Computed values are NOT cached. Every read re-evaluates. If a layer changes mid-execution, the next read sees the new result.
Reserved names
Workflow variable names cannot start with global. or be the bare word global (case-insensitive). The store rejects them at registration. This keeps the two-scope addressing unambiguous.
Operator surface
| Action | CLI |
|---|---|
| List variables visible to an execution | orca var list --execution <id> |
| Get one value | orca var get <exec> <key> |
| Override for an execution | orca var set <exec> <key> <value> |
| Set a global | orca var set-global <key> <value> |
| Remove an override | orca var unset <exec> <key> |
| Pass overrides at submit time | orca run <workflow> --vars KEY=VAL,KEY2=VAL2 |
Pitfalls
- Bare workflow names do NOT see globals. If you want to read a global from inside a workflow action, write
ctx.param("global.batch_id")explicitly. A barectx.param("batch_id")will raiseUndefinedVariableErroreven if a global of the same name exists. - Type widening:
int→floatis allowed, the reverse is not. AVariableDefinition(type="int")rejects a float value, even one with.0. CLI--vars KEY=1.0best-effort-parses to a float and will fail validation against an int definition. boolvalidation is strict.VariableDefinition(type="int")rejectsTrue/Falseeven though Python'sboolis a numeric subclass. This is deliberate to prevent surprising arithmetic.unsetdoesn't unset the definition. It only removes the value from the per-execution partition. The next read falls through to global / computed / definition default. If you want to remove a definition entirely, that's a workflow-template change, not an operator action.- Mid-flight
var setdoesn't retroactively change cached values. A thread that already readshake_speed=500and stored it in a local won't see your new600. If you need to affect an in-flight thread, pause it first or write the action to re-read each time.
Where to go next
- CLI: monitor — read variables from the operator side.
- CLI: control — set and unset values.
- Submissions — how
--varsat submit time fits into the larger submission shape.