Skip to main content

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

ScopeReference syntaxWhere declared
Workflowbare name: dilution_factorOn a @orca.workflow — local to that workflow template.
Globalglobal. prefix: global.reagent_batchAt 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 = ""
FieldEffect
typeOne of int, float, str, bool. Defaults to str.
defaultValue used when no override is set anywhere up the resolution chain.
min / maxRange bounds for numeric types. Ignored for strings and bools.
allowed_valuesWhitelist. Any value not in the list raises on validation.
descriptionHuman label for UI / tooling.
unitFree-form unit string ("mL", "°C") for UI / tooling.

Validation behavior

validate_value(name, value) runs every time a value is set. Type rules:

  • bool is rejected when int or float is expected (Python's bool is technically a subclass of int, so the validator guards against it).
  • int is accepted where float is expected (widening), not the other way around.
  • min / max only check numeric types; bool / string values skip the range check.
  • allowed_values runs 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.

LayerSet byScope
1. Submission partitionvar set --submission <id> / submissions_submit(variables={...})Per-submission inside one execution
2. Execution partitionorca var set <exec> <key> <value> / --vars KEY=VAL at submitOne execution
3. Global value layerorca var set-global <key> <value>Deployment-wide. Globals only.
4. ComputedExpression string registered via register_computedBoth scopes. Lazy-evaluated at read time.
5. Definition defaultdefault= on VariableDefinitionThe 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

ActionCLI
List variables visible to an executionorca var list --execution <id>
Get one valueorca var get <exec> <key>
Override for an executionorca var set <exec> <key> <value>
Set a globalorca var set-global <key> <value>
Remove an overrideorca var unset <exec> <key>
Pass overrides at submit timeorca 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 bare ctx.param("batch_id") will raise UndefinedVariableError even if a global of the same name exists.
  • Type widening: intfloat is allowed, the reverse is not. A VariableDefinition(type="int") rejects a float value, even one with .0. CLI --vars KEY=1.0 best-effort-parses to a float and will fail validation against an int definition.
  • bool validation is strict. VariableDefinition(type="int") rejects True / False even though Python's bool is a numeric subclass. This is deliberate to prevent surprising arithmetic.
  • unset doesn'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 set doesn't retroactively change cached values. A thread that already read shake_speed=500 and stored it in a local won't see your new 600. 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 --vars at submit time fits into the larger submission shape.