Formula Language
Formulas are how you customize a FinZot plan. They replace static numbers with logic, so the plan can react to changing conditions over time. Anywhere a plan takes a value, such as an income or expense amount, a milestone trigger, or a Roth conversion size, you can supply a formula instead of a fixed number.
Each formula is a standalone expression that returns one of two things:
- A number, used for any amount, such as a custom income or expense.
- A boolean (
TRUEorFALSE), used to trigger milestones or control conditional logic.
For the complete list of variables a formula can read, such as net_worth or age_primary, see
the Planner Context reference.
Syntax essentials
Section titled “Syntax essentials”Data types and literals
Section titled “Data types and literals”- Numbers. All numbers are 64-bit floats. You can write them several ways:
123, a whole number1_000_000, with underscores as visual separators that FinZot ignores45.67, a decimal10%, a percentage, which becomes0.1.75%, a leading decimal, which becomes0.0075
- Booleans.
TRUEorFALSE. - Variables. Fields from the planner, like
invested_assetsortotal_debt. - Comments. Use
//. FinZot ignores everything after it on that line.invested_assets * 1.1 // Add a 10% raise
- Case insensitivity. All variables, function names, and keywords such as
IF,AND, andTRUEare case-insensitive.age_primaryis the same asAge_Primary.
Operators
Section titled “Operators”The language supports standard arithmetic, comparison, and logical operators.
| Category | Operators | Description |
|---|---|---|
| Grouping | ( ... ) | Controls the order of operations. |
| Exponent | ^ | Exponentiation. 2 ^ 3 is 8. |
| Arithmetic | *, / | Multiplication and division. |
+, - | Addition and subtraction. | |
| Comparison | =, !=, <> | Equal, not equal. |
>, >=, <, <= | Greater than, less than, or equal. | |
| Logical | NOT | Inverts a boolean. NOT TRUE is FALSE. |
AND | TRUE if both sides are TRUE. | |
OR | TRUE if at least one side is TRUE. |
The AND and OR operators are short-circuiting. They stop evaluating as soon as they have
an answer.
Dividing by zero
Section titled “Dividing by zero”Dividing by zero has no answer, and FinZot treats it that way rather than stopping the projection.
When a formula reaches a division whose divisor is zero, the whole formula is undefined for
that year. Nothing around the division recovers a value from it: not MAX, not a comparison, not
NOT. A divisor within 0.000000001 of zero counts as zero.
A power with no finite answer is undefined in the same way: 0 ^ -1 (a division by zero in
disguise), a negative base with a fractional exponent such as (-8) ^ 0.5, or a result too large
to represent, such as 10 ^ 400. A negative base with a whole exponent is fine: (-8) ^ 3 is
-512.
What an undefined formula means where it is used:
- Sizing an entry. The year books nothing, as if the amount were
0. - A milestone trigger. The milestone does not fire that year. It is asked again the next year,
and fires once the condition has an answer and that answer is
TRUE. - A named formula. A formula that reads it is undefined too, unless the read sits in a branch that is not taken.
Only a division that evaluation reaches counts. IF, CASE, AND, and OR skip the branches
they do not need, so a guard works:
// Spread this year's assets over the years to 90, without assuming there is one IF(age < 90, invested_assets / (90 - age), 0)years_remaining never falls below 1, so it is always safe to divide by.
Order of operations
Section titled “Order of operations”Operators follow standard precedence, like PEMDAS or BODMAS. ^ is evaluated first, then * and
/, then + and -. AND is evaluated before OR. Parentheses override this precedence.
((taxable_balance + roth_balance) >= 1_500_000) AND (NOT debt_free)Built-in functions
Section titled “Built-in functions”Conditional logic (IF, IFS, CASE)
Section titled “Conditional logic (IF, IFS, CASE)”These functions choose a value based on one or more tests.
-
IF(condition, then_value, else_value)returnsthen_valueif theconditionisTRUE, andelse_valueotherwise.// Spend 5k on vacation if over 65, otherwise 2kIF(age_primary >= 65, 5000, 2000) -
IFS(condition1, value1, [condition2, value2], ...)evaluates pairs of conditions and values, and returns the value for the first condition that isTRUE. If no condition is true, it returns0.0. UseTRUEas the final condition for a default “else” value.IFS(net_worth > 5_000_000, 0.02,net_worth > 1_000_000, 0.01,TRUE, 0.0) -
CASE WHEN condition1 THEN value1 ... [ELSE else_value] ENDis a readable, SQL-style alternative toIFS. It returns theTHENvalue for the firstWHENcondition met. If none match, it returns theELSEvalue, or0.0ifELSEis omitted.// Set a spending goal based on the plan yearCASEWHEN calendar_year <= 2040 THEN 75_000WHEN calendar_year <= 2045 THEN 80_000ELSE 85_000END
Logical functions (AND, OR, NOT)
Section titled “Logical functions (AND, OR, NOT)”These are identical to the AND, OR, and NOT operators, but in function form for Excel
familiarity.
AND(expr1, expr2, ...)isTRUEif all arguments areTRUE.OR(expr1, expr2, ...)isTRUEif any argument isTRUE.NOT(expr)inverts a single boolean expression.
AND( age_primary > 50, net_worth > 1_000_000, debt_free)Math functions
Section titled “Math functions”MIN(num1, num2, ...)returns the smallest number in the list.MAX(num1, num2, ...)returns the largest number in the list.// Net savings, but never less than zeroMAX(0, net_worth - total_debt)// Cap a withdrawal at 50,000MIN(FUTURE_DOLLARS(40000), 50000)ABS(number)returns the absolute, non-negative value.ABS(roth_balance - traditional_balance)POWER(base, exponent)raises a number to a power. It is identical to the^operator.POWER(1.03, age_primary - 20)ROUND(number, digits)rounds like Excel, halves away from zero.digitsis optional (default0), a literal from -15 to 15; negative rounds left of the decimal point.ROUND(inflation_rate, 3)
Time-series and rolling functions
Section titled “Time-series and rolling functions”These functions look at data from previous years. Use them to model trends, smooth volatility, or create time-based triggers.
The first argument can be any numeric expression built from planner fields, literals, arithmetic,
and helper functions, such as net_worth - total_debt or BALANCE("account_id"). FinZot
evaluates the whole expression as of the earlier year. A named formula cannot appear inside a
time-series function. Only planner fields and helper functions keep a history. Write the
expression out in place instead of naming it.
Time-series functions nest. A nested window is built from year-end closes only.
LAG(ROLLING_AVG(net_worth, 3), 1) averages the closes of the three years ending last year. That
is not the number ROLLING_AVG(net_worth, 3) returned last year. That number counted last year as
it stood mid-year. ROLLING_MAX(LAG(net_worth, 1), 5) is the highest close of the five years
before this one.
A look back and a live read are different measurements. Every look back reads a year-end close, after that year’s spending, taxes, and settlement. Every field read without a lag reads the year as it stands at the step your entry runs in (planner context). Subtracting one from the other does not give a year-over-year change.
net_worth - LAG(net_worth, 1)is not this year’s savings, and it returns a different number at an expense than at a withdrawal. For the change in a balance, compare two closes withLAG(net_worth, 1) - LAG(net_worth, 2). That needs two years of plan history behind it before it means anything.
Point-in-time lookups
Section titled “Point-in-time lookups”-
LAG(expression, periods)returns the valueexpressionhad at the close of the yearperiodsback.LAG(net_worth, 0)is the exception. It returns the current year as it stands at the step your entry runs in. A lag that reaches before the plan started returns0. A milestone trigger gets no answer from it instead, and does not fire that year. In a trigger,LAG(net_worth, 0)is last year’s close, because a trigger runs before its year (milestones and timing).// Last year's closing balance of one account, less what it owedLAG(BALANCE("account_id") - total_debt, 1)// What was spent last year, the base for a spending ruleLAG(consumption, 1) -
PCT_CHANGE(expression, periods)returns the percentage change between the current value and the value fromperiodsago, computed as(current - past) / past.periodsmust be1or greater. It returns0.0if the past value is zero or needs a year before the plan started. In a milestone trigger, one that needs a year before the plan started has no answer instead.// Net worth dropped more than 20% in the last yearPCT_CHANGE(net_worth, 1) < -0.20// One account lost value last yearPCT_CHANGE(BALANCE("account_id"), 1) < 0
Rolling window aggregations
Section titled “Rolling window aggregations”These compute a single statistic over a window of the last periods years, including the current
year.
ROLLING_SUM(expression, periods)returns the total over the window.ROLLING_AVG(expression, periods)returns the average over the window.ROLLING_MIN(expression, periods)returns the smallest value in the window.ROLLING_MAX(expression, periods)returns the largest value in the window.
// Variable withdrawal rate based on the 3-year average of invested assetsCASE WHEN ROLLING_AVG(invested_assets, 3) < 1_000_000 THEN 0.035 WHEN ROLLING_AVG(invested_assets, 3) < 3_000_000 THEN 0.040 ELSE 0.045ENDThe window’s last year is not measured like the rest. A window ends with the current year as it stands at the step your entry runs in. Every earlier year in it is a year-end close. So
ROLLING_AVG(net_worth, 3)averages one mid-year reading with two closes. The mid-year reading moves if you attach the rule to a different entry. Read a window as a smoothed trend, not as an average of three comparable numbers.
Rolling functions in early years: rolling functions still run in the first few years of a plan, but on a partial window. The starting balances count as one year of history, so
ROLLING_AVG(net_worth, 3)sizing a withdrawal in the first plan year averages the starting net worth and that year. Nothing flowed before the plan started, so a window over income, spending, a return, or inflation counts plan years only:ROLLING_AVG(portfolio_return, 5)in the first plan year is that year’s return, not the return averaged with a zero. An expression that mixes the two, such asnet_worth * portfolio_return, counts as a flow. Missing years never count against a condition:ROLLING_AND(debt_free, 3)isTRUEwhile every year it can see is debt-free. A window only counts the years the whole expression can be evaluated for. A lag inside the expression costs the window one year for every year of lag depth. A three-year window over an expression holdingLAG(x, 1)has one element in the first plan year, not three. A window with no year in it returns0, orTRUEforROLLING_ANDandFALSEforROLLING_OR. In a milestone trigger it has no answer instead.
Do not hand-roll a window out of
LAG. A lag that reaches before the plan started returns0, so(portfolio_return + LAG(portfolio_return, 1) + LAG(portfolio_return, 2)) / 3divides a one-year sum by three in the first plan year and reads a third of the true return. A guardrail such as “trailing average below 4%” written that way stays on for the first two years no matter what the market did.ROLLING_AVG(portfolio_return, 3)divides by the years it actually saw.
Rolling logic
Section titled “Rolling logic”These look at a window of boolean values.
ROLLING_AND(condition, periods)isTRUEif the condition held in every year of the window.ROLLING_OR(condition, periods)isTRUEif the condition held in at least one year of the window.
// Trigger if the 3-year minimum net worth dropped below 1M, or not yet debt-freeOR( ROLLING_MIN(net_worth, 3) < 1_000_000, NOT debt_free)
// Go on a 5,000 vacation, but only after 3 straight debt-free yearsIF(ROLLING_AND(debt_free, 3), 5000, 0)See also
Section titled “See also”- Planner Context — The live variables and helper functions a formula can read, such as net worth, age, balances, income, and returns.
- How a Year Works — The order FinZot processes each projection year. Income, expenses, and required payments first, then each list of actions you control, ending in Roth Conversions and Withdrawals. This order is also what a formula reads: it decides which of the year’s flows have happened by the time an entry is sized. Also the three stages that cover a shortfall and the default account that absorbs surplus.