Assigner Syntax
1 What Is an Assigner?
In many research protocols, the value of one field can be computed automatically from other fields—for example, a variable (var), a step (step), or a checkpoint (check). Airalogy offers a high-level feature called an Assigner to describe and execute these dependencies.
1.1 Typical Use Case
Suppose var_3 should always equal var_1 + var_2. There are three common ways to define protocol fields (including types):
- Classic 3-file style (most complete):
protocol.aimd+model.py+assigner.py. Use this when you need advanced typing/validation (e.g., richer constraints in Pydantic). - 2-file style (simpler, covers most cases): use typed AIMD to put types directly in
protocol.aimd, plusassigner.py. - Single-file style (lightweight): only
protocol.aimd, with inline Assigner code blocks.
Typed AIMD syntax: Using Types in Airalogy Markdown.
Below shows the same assigner logic in these styles. For brevity, the rest of this page uses the 2-file typed AIMD style.
Style A: Classic 3-file (protocol.aimd + model.py + assigner.py)
File 1: protocol.aimd
The value of `var_1`: {{var|var_1}}
The value of `var_2`: {{var|var_2}}
The value of `var_3`: {{var|var_3}}
Note: `var_3` = `var_1` + `var_2`File 2: model.py
from pydantic import BaseModel
class VarModel(BaseModel):
var_1: int
var_2: int
var_3: intFile 3: assigner.py
from airalogy.assigner import AssignerResult, assigner
@assigner(
assigned_fields=["var_3"], # fields to assign
dependent_fields=["var_1", "var_2"], # fields that this function depends on
mode="auto", # See "Assigner Modes" below
)
def calculate_var_3(dependent_fields: dict) -> AssignerResult:
v1 = dependent_fields["var_1"]
v2 = dependent_fields["var_2"]
v3 = v1 + v2
return AssignerResult(
assigned_fields={"var_3": v3}
)Note: success and error_message are optional. When omitted, success defaults to True and error_message defaults to None:
return AssignerResult(
assigned_fields={"var_3": v3}
)Style B: 2-file (typed AIMD + assigner.py)
File 1: typed protocol.aimd
The value of `var_1`: {{var|var_1: int}}
The value of `var_2`: {{var|var_2: int}}
The value of `var_3`: {{var|var_3: int}}
Note: `var_3` = `var_1` + `var_2`File 2: assigner.py (same as Style A)
Many-to-many is allowed An Assigner can read any number of fields and assign any number of fields—across
var,step, andcheckalike.
Style C: Single-file (AIMD + inline Assigner code block)
File 1: protocol.aimd
The value of `var_1`: {{var|var_1: int}}
The value of `var_2`: {{var|var_2: int}}
The value of `var_3`: {{var|var_3: int}}
Note: `var_3` = `var_1` + `var_2`
```assigner
from airalogy.assigner import AssignerResult, assigner
@assigner(
assigned_fields=["var_3"],
dependent_fields=["var_1", "var_2"],
mode="auto",
)
def calculate_var_3(dep: dict) -> AssignerResult:
return AssignerResult(
assigned_fields={"var_3": dep["var_1"] + dep["var_2"]},
)
```You can load inline assigners directly in Python:
from airalogy.assigner import load_inline_assigners
with open("protocol.aimd", "r", encoding="utf-8") as f:
load_inline_assigners(f.read())Constraint: inline Assigners cannot be mixed with assigner.py. If assigner.py exists in the same protocol directory, inline assigner blocks are considered invalid.
Recommendations:
- The
assignerblock is Python code and can be treated like a regular.pysnippet. - Prefer one
assignerblock placed at the end of the AIMD file for readability and extraction.
1.2 Real-world Example: Solution Preparation
In real-world research data recording, many values are derived from other fields. A typical example is preparing a solution with a target concentration: given the target volume, target molar concentration, and the solute molar mass, the required solute mass can be computed automatically.
Typed protocol.aimd
Solvent name: {{var|solvent_name: str}}
Target solution volume (L): {{var|target_solution_volume: float, gt=0}}
Solute name: {{var|solute_name: str}}
Solute molar mass (g/mol): {{var|solute_molar_mass: float, gt=0}}
Target molar concentration (mol/L): {{var|target_molar_concentration: float, gt=0}}
Required solute mass (g): {{var|required_solute_mass: float, gt=0}}assigner.py
from airalogy.assigner import AssignerResult, assigner
@assigner(
assigned_fields=["required_solute_mass"],
dependent_fields=[
"target_solution_volume",
"solute_molar_mass",
"target_molar_concentration",
],
mode="auto",
)
def calculate_required_solute_mass(dep: dict) -> AssignerResult:
v = dep["target_solution_volume"]
mm = dep["solute_molar_mass"]
c = dep["target_molar_concentration"]
return AssignerResult(
assigned_fields={"required_solute_mass": v * c * mm},
)If you need more advanced typing/validation (custom Pydantic models, cross-field validation, etc.), use the classic 3-file style with model.py.
1.3 Extensibility Notes
The computation inside an Assigner can be anything you can express in Python: pure Python logic, third-party packages, API calls, or AI services. In practice, prefer deterministic and fast computations, and handle failures by returning success=False with an error_message.
2 Assigner Modes
The mode affects when the Assigner is triggered, and whether it is expected to be user-editable after assignment.
"auto": run whenever dependencies change, and overwrite the assigned field value with the new result."manual": do not auto-run; user must click an "Assign" button in the UI. Use this if you don't want auto-trigger on every dependency change."auto_first": run automatically once (typically when dependencies become available), then stop auto-refresh; behavior-wise you can think of it as “auto once, then likemanual(needs a manual trigger to run again)”."auto_readonly": same as"auto", but UI should lock assigned fields after filling."manual_readonly": same as"manual", but UI should lock assigned fields after filling.
What does “trigger” mean?
“Trigger” means: the platform continuously monitors field readiness, and when an Assigner’s dependencies become ready (and change, or become available for the first time), it executes the Assigner function and writes results into its assigned_fields.
- In
mode="auto": dependency changes (with all dependencies ready) trigger recomputation; assigned fields can then trigger downstream assigners, enabling multi-level auto assignment. - In
mode="auto_first": it triggers once when dependencies first become ready, then stops auto-refresh. - In
mode="manual": it does not auto-trigger; users must click a button in the UI to run it.
3 dependent_fields and assigned_fields
- Both are plain Python dicts whose keys are field names.
- Values follow the JSON Schema of the protocol; i.e. only JSON-serialisable types.
- For special field classes (e.g. a checkpoint) you may need to wrap the value in a helper model such as
CheckValue.
4 Airalogy Field Dependency Graph (Advanced)
In complex research recording scenarios, the assignment relationships between Airalogy Fields can be multi-level and multi-dependency. To support this, the runtime constructs a directed dependency graph between fields, called the Airalogy Field Dependency Graph. It represents all field assignment relationships within a protocol, allowing the runtime to track which fields are ready and trigger assigners accordingly.
Most users don't need to think about this graph explicitly—just declare dependent_fields and assigned_fields correctly, and keep the constraints below.
4.1 Constraints
To keep assignment deterministic and well-defined, the dependency graph must satisfy:
- It must be a Directed Acyclic Graph (DAG) (no cycles).
- Each Airalogy Field can be assigned by at most one assigner function.
4.2 Example: Multi-level and Multi-dependency Assigners
The following protocol contains nine fields (f1–f9) and three assigners (a1–a3):
Typed protocol.aimd (replaces protocol.aimd + model.py)
Number F1: {{var|f1: int}}
Number F2 (= F1 + 1): {{var|f2: int}}
Number F3 (= F1 × 2): {{var|f3: int}}
Number F4: {{var|f4: int}}
Number F5: {{var|f5: int}}
Number F6 (= F4 × F5): {{var|f6: int}}
Number F7: {{var|f7: int}}
Number F8 (= F2 + F3 + F6 + F7): {{var|f8: int}}
Number F9: {{var|f9: int}}Their dependencies look like the following diagram:
assigner.py
from airalogy.assigner import AssignerResult, assigner
@assigner(
assigned_fields=["f2", "f3"],
dependent_fields=["f1"],
mode="auto",
)
def a1(dependent_fields: dict) -> AssignerResult:
f1 = dependent_fields["f1"]
return AssignerResult(
assigned_fields={"f2": f1 + 1, "f3": f1 * 2},
)
@assigner(
assigned_fields=["f6"],
dependent_fields=["f4", "f5"],
mode="auto",
)
def a2(dependent_fields: dict) -> AssignerResult:
f4 = dependent_fields["f4"]
f5 = dependent_fields["f5"]
return AssignerResult(
assigned_fields={"f6": f4 * f5},
)
@assigner(
assigned_fields=["f8"],
dependent_fields=["f2", "f3", "f6", "f7"],
mode="auto",
)
def a3(dependent_fields: dict) -> AssignerResult:
f2 = dependent_fields["f2"]
f3 = dependent_fields["f3"]
f6 = dependent_fields["f6"]
f7 = dependent_fields["f7"]
return AssignerResult(
assigned_fields={"f8": f2 + f3 + f6 + f7},
)4.3 Example Run: Entering Fields in Order f1 → f9
Suppose the user enters values in the recording UI in the order f1 to f9. The runtime triggers assigners when dependencies become available (see “What does trigger mean?”):
- Fill
f1→ triggersa1→ assignsf2andf3. - Fill
f4→a2is still waiting forf5. - Fill
f5→ triggersa2→ assignsf6. - Fill
f7→ triggersa3(nowf2,f3,f6,f7are ready) → assignsf8. - Fill
f9→ all fields are ready; the record can be submitted.
5 Working with Complex Types
If a field stores a complex type (e.g. datetime) it is transmitted as a JSON-compatible value (usually a string). Convert it to a native Python object before calculation, then convert back:
from datetime import datetime, timedelta
from airalogy.assigner import AssignerResult, assigner
@assigner(
assigned_fields=["record_time_plus_1_day"],
dependent_fields=["record_time"],
mode="auto",
)
def plus_one_day(dep: dict) -> AssignerResult:
# JSON → Python
t0 = datetime.fromisoformat(dep["record_time"])
t1 = t0 + timedelta(days=1)
# Python → JSON
return AssignerResult(
assigned_fields={
"record_time_plus_1_day": t1.isoformat()
}
)6 Assigners for Checkpoints
Checkpoints (check) can be calculated the same way, but you must return a CheckValue:
from airalogy.assigner import AssignerResult, assigner
from airalogy.models import CheckValue
@assigner(
assigned_fields=["var_1_2_sum", "check_sum_gt_10"],
dependent_fields=["var_1", "var_2"],
mode="auto",
)
def check_sum(dep: dict) -> AssignerResult:
v1 = dep["var_1"]
v2 = dep["var_2"]
total = v1 + v2
passed = total > 10
return AssignerResult(
assigned_fields={
"var_1_2_sum": total,
"check_sum_gt_10": CheckValue(
checked=passed,
annotation=f"var_1 + var_2 = {total} ({'>' if passed else '<='} 10)"
)
}
)The same pattern works for
stepfields—return the helper modelStepValue.
Reference
| Decorator Argument | Description |
|---|---|
assigned_fields | List of field names the function assigns |
dependent_fields | List of field names the function depends on |
mode | "auto" (run on change + overwrite), "manual" (button click), "auto_first" (run once), "auto_readonly" (auto + lock), or "manual_readonly" (manual + lock) |
AssignerResult
| Field | Type | Default | Meaning |
|---|---|---|---|
success | bool | True | Whether the assignment succeeded |
assigned_fields | dict[str, Any] | required | New values |
error_message | str | None | None | Reason when success is False |
Other Assigners
Legacy Class Syntax (To Be Deprecated)
Older versions required an explicit class Assigner(AssignerBase) with static methods. This style is still supported for backward compatibility but will be removed in a future release, so please migrate new code to the function-based API shown earlier.
from airalogy.assigner import AssignerBase, AssignerResult, assigner
class Assigner(AssignerBase):
@assigner(
assigned_fields=["var_3"],
dependent_fields=["var_1", "var_2"],
mode="auto",
)
def calculate_var_3(dependent_fields: dict) -> AssignerResult:
return AssignerResult(
assigned_fields={"var_3": dependent_fields["var_1"] + dependent_fields["var_2"]}
)⚠️ New development should not add more class-based Assigners unless absolutely necessary for legacy interoperability.