The expression language
One small formula language runs everywhere Kaizendex calculates — a Formula field on one entry, and the statistics behind a chart. Same syntax, same functions, two scopes.
An expression is a one-line formula you type instead of a number. prop("Weight kg") / 2. round(mean(prop("Steps"))). if(prop("Steps") > 10000, "Great day", "Keep going"). Kaizendex works it out for you and keeps the answer up to date the moment the data underneath it changes.
There is exactly one expression language, and it shows up in two places: a Formula Property calculating a value for one entry, and the statistics behind a chart or query calculating over a whole column of your history. Same syntax, same function names — the only thing that changes is what prop(...) hands you.
Referring to your data
Every value you pull in comes through prop("Field name"), spelled exactly as the field is named in the Collection builder, in double quotes:
prop("Weight kg")
There are no bare names. Weight kg on its own is a syntax error, and so is Weight_kg — always prop("..."), in both scopes. That's deliberate: your field names have spaces, punctuation and emoji in them, and a quoted string can hold all of it.
Row scope vs dataset scope
This is the one idea worth slowing down for.
| Row scope | Dataset scope | |
|---|---|---|
| Where | A Formula field on a Collection | The statistics behind a chart or query |
prop("Steps") gives you | One entry's own value — 8,412 | A whole column — [8412, 11003, —, 7750, …] |
| Runs | Once per entry | Once per chart |
| Typical answer | A value shown on that entry's card | A single headline number, or a new column |
Row scope reads sideways: a Formula on today's Sleep entry can see today's Bedtime and today's Wake time, and nothing else. That's why a Formula can compute your BMI but can't compute your average weight — it has never seen yesterday.
round(prop("Weight kg") / (prop("Height m") * prop("Height m")), 1)
Dataset scope reads downwards: the same prop("Weight kg") hands back every weight in the range you're charting, in order, with a gap wherever you didn't log. That's what makes averages, trends, correlations and streaks possible.
round(mean(prop("Weight kg")), 1)
Most functions work in both — round, if, min, abs, concat don't care whether they're handed one value or a summary. The statistical ones (mean, corr, rollingAvg, streak, …) need a whole column, so they're dataset only; the function reference marks each one's scope.
Value types
Seven kinds of value flow through an expression. Mixing them sensibly is allowed; mixing them nonsensically gives you a clear error rather than a wrong answer.
| Type | What it is | Written as |
|---|---|---|
| Number | Any number | 10000, 2.5, -3 |
| Text | A piece of text | "Great day" (double quotes) |
| Boolean | Yes or no | true, false |
| Date & time | A moment | now(), or a Date/Date-time field |
| Duration | A length of time, counted in minutes | the gap between two moments, or a Stopwatch field |
| Series | A column of numbers with gaps in it | what prop(...) gives you in dataset scope |
| Empty | Nothing logged | empty() |
A few conversions happen for you:
- Subtract two moments and you get a duration —
prop("Wake time") - prop("Bedtime")reads as7h 42m. - Add a duration to a moment and you get a moment back —
prop("Start") + prop("Break"). - Divide a duration by a duration and you get a plain ratio —
prop("Total sleep") / prop("Time in bed")is your sleep efficiency. - Compare a duration to a number and it compares as minutes —
prop("Total sleep") < 180. - Add anything to text and you get text —
"Day " + prop("Day index").
A series is deliberately opaque: you can't index into it, print it or compare it directly. Reduce it to a number first (mean, sum, seriesAvg, …) and then do whatever you like with the result.
Operators
+ - * / % arithmetic
== != < <= > >= comparison
and or not logic
( ) grouping
and and or short-circuit — if the left side already settles the answer, the right side is never worked out. if(...) does the same: only the branch you take is evaluated. Nothing else is lazy, which matters more than it sounds — see Guarding against empty below.
Dividing by zero is an error, not infinity.
Gaps are skipped, never counted as zero
The single most important rule in dataset scope: a day you didn't log is a gap, and a gap is left out of the calculation — it is never quietly treated as a 0.
If you logged your weight on 5 of 7 days, mean(prop("Weight kg")) is the average of those 5 weights. Two phantom zeros would have dragged it to a number you never weighed.
The consequences are worth knowing:
count(prop("Steps"))counts the values you actually have — that's your sample size, and it's the number to check before trusting anything.stddevandvarianceare sample statistics (the n−1 kind), because your log is a sample of your life, not every day you'll ever live. One lone value therefore has no spread at all: both come back empty, not0.corr,slope,interceptandr2compare two columns and drop in pairs — a day counts only when both sides have a value. One missing sleep score removes that day from the correlation entirely, rather than sliding your steps column out of line. Fewer than two surviving pairs gives you empty.rollingAvg(prop("Weight kg"), 7)leaves the first 6 rows empty rather than reporting a "7-day average" of two days. Once a full 7-day window exists, any gaps inside it are simply skipped, so one missed day doesn't blank three rolling values.delta(...)— the day-over-day change — is empty on the first row and on either side of a gap.cumsum(...)leaves a gap as a gap but carries the running total across it.streak,longestStreakandlongestGapread a column where non-zero means true; zero and empty both mean false, so a skipped day breaks a streak.
The nine families of function
Every function belongs to one family, and the full reference is grouped the same way. Knowing the families is usually enough to guess where to look.
Work on one value at a time — these are at home in row scope:
- Logic —
ifandempty. Choosing between two values, and asking whether something is blank. - Math —
round,floor,ceil,abs,min,max,clamp. Ordinary arithmetic tidying: rounding a result, keeping a score inside 0–100. - Text —
concatandlength. Sticking strings together and measuring them. - Date & time —
nowanddateBetween. How long between two moments, in minutes, hours or days. - Shaping —
bucketandcoalesce. Rounding a number down to the nearest step of 5 or 10, and falling back to the first value that isn't empty.
Work on a whole column — these only make sense in dataset scope:
- Descriptive —
mean,median,mode,stddev,variance,sum,count,percentile. The plain summary of a column: its typical value and how much it moves around. - Relational —
corr,slope,intercept,r2,zscore. How two columns move together. This is the family behind "do late nights cost me recovery?". - Sequence —
rollingAvg,rollingSum,lag,delta,cumsum,streak,longestStreak,longestGap. Anything that cares about order: trends, running totals, comparing today with a week ago, and how many days in a row you kept something up. - Series —
seriesAvg,seriesMin,seriesMax,seriesCount,seriesFractionBelow,seriesFractionAbove. For fields that already hold many readings inside a single entry, like a night of heart-rate samples.
Guarding against empty
Anything can come back empty — a field you left blank, a statistic with nothing to work on. Two tools:
if(...), which is lazy, so the branch you don't take can't fail:
if(count(prop("Steps")) >= 30, round(mean(prop("Steps"))), empty())
and coalesce(...), which takes the first argument that isn't empty:
coalesce(prop("Nickname"), prop("Name"), "Anonymous")
Test for emptiness directly with empty(prop("Notes")), and produce the empty value itself with empty() and no arguments.
Worked recipes
One entry at a time (row scope)
Sleep length, from a Bedtime and a Wake time:
dateBetween(prop("Wake time"), prop("Bedtime"), "minutes")
A label that reacts to a number:
if(prop("Reps") >= 10, "Strong set 💪", "Keep going")
Pace — minutes per kilometre, guarded against a zero distance:
if(prop("Distance km") > 0, prop("Duration") / prop("Distance km"), empty())
Output: Duration. A duration divided by a plain number stays a duration, so this renders as 5h 12m-style time rather than a bare decimal.
How far off target you are, sign ignored:
abs(prop("Weight kg") - prop("Target weight kg"))
Weight snapped into 5 kg bands, so a chart can count how many entries land in each:
bucket(prop("Weight kg"), 5)
Across your history (dataset scope)
Your typical day, using the median so one huge day doesn't skew it:
round(median(prop("Steps")))
A smoothed weight line, immune to daily water-weight noise:
rollingAvg(prop("Weight kg"), 7)
Does sleeping better go with moving more?
corr(prop("Sleep score"), prop("Steps"))
The same question, answered honestly — say nothing until there's enough data:
if(count(prop("Sleep score")) >= 30, round(corr(prop("Sleep score"), prop("Steps")), 2), empty())
Weight change per day on the best-fit line, and how much to trust it:
slope(prop("Day index"), prop("Weight kg"))
r2(prop("Day index"), prop("Weight kg"))
Your current run of hitting the protein goal, and your best ever:
streak(prop("Hit protein goal"))
longestStreak(prop("Hit protein goal"))
Two things on one chart, on the same 0–1 footing:
normalize(prop("Steps"))
normalize(prop("Sleep score"))
Which days were genuinely unusual, in standard deviations from your own normal:
zscore(prop("Resting heart rate"))
Your good days, defined by your own history rather than a round number:
percentile(prop("Sleep score"), 90)
When something goes wrong
Expressions never fail silently. A typo'd field name reads back as Unknown property "Wieght kg". Handing a function the wrong kind of value reads back as mean() requires a series, got number. Too many arguments reads back as round() requires 1 or 2 arguments. The message names the function and what it wanted, so it's usually a one-word fix.
In a Formula field, a short error marker shows on the card and table, and the full message on the entry page.
Next
- Function reference — every function, its arguments, its scope, and a copy-pasteable example
- Formulas — adding a Formula Property to a Collection
- Building charts and dashboards — where the dataset-scope statistics end up