In formulas
+ - * / % ^- Add, subtract, multiply, divide, remainder and power. Parentheses group as usual.
abs(x)- The value without its sign — abs(-3) is 3.
ceil(x)- Rounds up to the next whole number — ceil(2.1) is 3.
floor(x)- Rounds down to the previous whole number — floor(2.9) is 2.
round(x, digits)- Rounds to the nearest whole number — or to a number of decimal places: round(2.345, 2) is 2.35.
min(a, b, …)- The smallest of any number of values.
max(a, b, …)- The largest of any number of values.
How formulas behave
- References are field keys and must point at other number fields. Inside a repeating group, a row also sees the template’s top-level number fields.
- A missing, empty or non-numeric input makes the result empty — never zero — and an empty result carries through formulas that build on it.
- Division by zero gives an empty result rather than an error.
- The final value rounds to the field’s decimal places.
- Unknown references and circular formulas are rejected as you type; a formula caps at 1024 characters.
Totals over repeating groups
A formula can also reduce a repeating group’s rows to a single number. Name the group, then the sub-field column — over a line-items group items with a number sub-field cost, an order total is:
order_total = sum(items, cost)
The total adds up the cost of every row and updates as rows are added and filled in.
sum(group, field)- The total of a number column over the group’s rows.
avg(group, field)- The average of the filled cells in a number column.
min(group, field)- The smallest value in a number column — the same min, aggregating when its first argument is a group.
max(group, field)- The largest value in a number column, the same way.
count(group)- How many rows the group has.
countFilled(group, field)- How many rows have answered that column.
defined(group, field)- 1 when every row has answered that column, 0 otherwise.
allChecked(group, field)- 1 when a checkbox or switch column is ticked in every row, 0 otherwise. The one aggregation that asks about ticks — defined cannot, because an untouched tick box already counts as answered.
sum(group.nested, field)- Every aggregation also takes a dotted path into nested groups: sum(items.item, cost) totals the cost column of every item row across all items rows, flattened into one set.
How aggregation behaves
- Blank or non-numeric cells are skipped, not counted as zero — a total stays partial and grows as rows are filled in. Zero and negative values do count.
- countFilled and defined ask whether a cell is answered: empty text and an empty selection are not answers, while an unticked checkbox (false) and the number 0 are. So defined over a checkbox column is always 1 — allChecked is the one that counts ticks, and it only accepts a checkbox or switch column.
- Aggregation starts at the direct child groups of the formula’s own scope and reaches deeper with dots: sum(items.item, cost) gathers every item row of every items row into one flat set. A row formula can do the same over the row’s own nested groups — never over the group it sits in. A row that is missing the nested list simply contributes nothing to the total.
- Over an empty group, sum, count and countFilled give 0; avg, min and max give an empty result; defined and allChecked give 1 — with no rows, nothing is left unanswered or unticked.
Reading the selected material
A formula can also read the material a reference field points to. Name the reference field, then the property — in a line-items group with a material reference mat and a quantity qty, a row’s cost is:
rowcost = qty * lookup(mat, price)
The row cost multiplies the quantity by the selected material’s price per unit, and follows the row when a different material is picked.
lookup(field, property)- The named property of the material a reference field points to — lookup(mat, price) is the per-unit price of the row’s selected material.
How lookup behaves
- The reference must be a material reference in the formula’s own scope or any enclosing scope: a row formula can read its own row’s reference, its parent row’s, or a top-level one. Lookup never reaches down into a group’s rows — a row has one enclosing material, but a group has many rows — and there are no dotted paths.
- One property exists today: price — the material’s price per unit, as entered in your stock.
- Until a material is selected, the lookup is empty — and the empty result carries through formulas that build on it, as formula results always do.
- Values are captured when the order is written: an order keeps the price that held when its rows were saved, and a later price change in your stock never silently rewrites an existing order.
- lookup cannot be an aggregation column — sum(items, lookup(mat, price)) is rejected. Compute per row instead and total at the top: rowcost = qty * lookup(mat, price) in the row, order_total = sum(items, rowcost) above it.
In conditions
== !=- Equal and not equal. Exact match, no type bending — the number 1 is not the text "1".
> >= < <=- Number comparisons. False whenever either side is not a number.
contains(field, value)- On a multi-select, true when that choice is picked; on text, true when the text contains the fragment.
isEmpty(field)- True for empty text or when nothing is selected. An unchecked checkbox and the number 0 are not empty.
isNotEmpty(field)- The opposite of isEmpty.
and or- Chain conditions with and, or with or — one rule uses one of them, never both.
How conditions behave
- The field key goes on the left, the value on the right: a number, text in double quotes, or true and false. Comparing two fields to each other is not supported.
- A condition over a missing value is false, so a show-rule hides its field — rules fail towards hiding, never towards showing by accident.
- The same conditions power field visibility, card visibility, per-row visibility of a field inside a repeating group and workflow advance guards.
Conditions over repeating groups
A condition can also ask about a repeating group as a whole — how many rows it has, whether every row has answered a column, or whether every row has ticked one. Name the group — or a dotted path into its nested groups — then the column. These are the four forms:
defined(items, mat)allChecked(items, done)count(items) > 0countFilled(items, note) >= 2
defined(group, field)- True when that column is answered in every row.
allChecked(group, field)- True when a checkbox or switch column is ticked in every row.
count(group) > 0- How many rows the group has, compared against a plain number. Every comparison operator works: == != > >= < <=.
countFilled(group, field) >= 2- How many rows have answered that column, compared against a plain number.
How group conditions behave
- defined(group, field) is true when that field is filled in every row — and also when the group has no rows at all. To require rows as well, write count(group) > 0 and defined(group, field). It is the same vacuous truth the formula defined has over an empty group, and it is the one caveat that quietly lets an empty order through a gate. It holds through a dotted path too: a row whose nested list is empty leaves defined true. allChecked behaves the same way — pair it with count(group) > 0 for the same reason.
- Answered means the same here as in the aggregation functions: empty text and an empty selection are not answers, while an unchecked checkbox (false) and the number 0 are. This is why a tick box needs allChecked: defined over a checkbox column is true the moment the row exists. allChecked only accepts a checkbox or switch column — on anything else it could never be true, so the builder refuses it rather than letting the rule sit there dead.
- A group condition names a top-level repeating group and reaches its nested groups with dots: defined(items.item, mat) asks about every item row of every items row, flattened into one set — count(items.item) > 0 counts them all.
- If the group is not there at all — hidden by a card rule, or never filled in — every condition about it is false, including count(group) == 0. The same holds at depth: if any row is missing its nested list entirely, the condition is false — a gate never opens on malformed data. Formulas differ here on purpose: an aggregation quietly skips such a row, a condition refuses.
- All four forms work everywhere a condition does: field visibility, card visibility, per-row visibility of a field inside a repeating group and workflow advance guards.
Worked example: never deduct on a half-filled order
A line-items group items holds a material reference and a quantity column qty bound to stock deduction. On the stage before the one that deducts, set the advance condition to:
count(items) > 0 and defined(items, qty)
An order with no rows cannot advance, and neither can one whose rows still have a blank quantity — so stock never leaves for an order nobody finished filling in.
This gate is the moment to get it right: once an order has passed the deduction stage, the amounts it deducted are locked. Editing them is refused, because that stock has already left the shelf.