Skip to content

Calculating

Calculate (utility/calculate) computes one number from an expression: qty * price, round(subtotal * (1 + vat_rate), 2), sum(order.lines[*].amount), if(subtotal >= 300, subtotal * 0.05, 0). It is the node for every amount a workflow shows or decides on — and the reason an AI model should never be asked to do sums: a language model writes numbers that look right, Calculate computes them.

Every name in the expression that is not a function is a variable, and every variable becomes an input port of the node with the same name. Write round(subtotal * (1 + vat_rate), 2) and the node grows two inputs, subtotal and vat_rate; connect a value to each. An unconnected variable is caught when the workflow is validated (CALC_UNCONNECTED_VARIABLE), and an expression that does not parse is rejected with the position of the mistake.

A variable takes a number, a boolean (for a condition), or a JSON value read with a path. Text is never read as a number — the text "12" is refused, as everywhere in Madoo (see Values and types) — so connect a Number Input, a number-typed field or the result of another Calculate.

Element Written Example
Numbers 12, 0.22 price * 0.22
Operators + - * /, unary -, parentheses (subtotal - discount) * (1 + vat_rate)
A field of a JSON value .field, [n], ["name with spaces"] order.lines[0].price, quote["unit price"]
Every item of a list [*] order.lines[*].qty
Functions round(x), round(x, digits), floor, ceil, abs, min, max, sum, count, avg sum(order.lines[*].amount)
A condition if(condition, then, else), with == != < <= > >= if(subtotal >= 300, subtotal * 0.05, 0)
Booleans true, false, a boolean variable if(express, 15, 9.9)

A few rules keep expressions unambiguous:

  • Comparisons live inside if(). The result of Calculate is always a number; to route a workflow on a comparison, use a Predicate (see the decision pages of this section).
  • One comparison at a time. a < b < c is refused; nest if() instead.
  • Only the chosen branch of if() is computed, so if(guests == 0, 0, total / guests) never divides by zero.
  • No %. Write a percentage as a fraction: price * 0.22, or price * 22 / 100.
  • = is not a comparison. Write ==.
  • A path with [*] gives a list, which only the list functions accept: sum, count, avg, min, max. Arithmetic between two lists (qty[*] * price[*]) is not supported: compute each line first (see the lab).

Calculate works in decimal, not in binary floating point: 0.1 + 0.2 is 0.3, and a total of cents stays a total of cents. There is no silent value: dividing by zero, a missing JSON field and the average of an empty list fail the node with a reason instead of producing 0, NaN or an empty result.

Decimals rounds the result; leave it empty to keep it exact. round(x, 2) rounds inside the expression, which matters when an intermediate amount — a discount, a VAT line — must be rounded before it is added. Rounding chooses how a value exactly halfway goes: half away from zero (2.345 → 2.35, the usual commercial rule) or half even (2.345 → 2.34, banker’s rounding).

A number is formatted for the reader — separators, currency symbol, the reader’s language — not by Calculate but where it becomes text: by Format as Text before a prompt, a message or a Text Template (see Turning a value into text), or by the document template that prints it (see Conditions, links and formats).

Inside an iteration, Calculate runs once per item: connect the item’s fields and it computes each line total. To add the lines up, collect them with Aggregate JSON and sum the collected field:

  1. enumerate/json over order.lines → qty, price per line;
  2. Calculate qty * price → the line total;
  3. aggregate/json collects sku, qty, price, line_total;
  4. Calculate sum(priced_lines[*].line_total) → the subtotal.

The collection travels as a stored JSON file; Calculate reads it like any JSON value.

The calculation lab turns an order into a quote: a total per line, a subtotal, a discount above a threshold, shipping that depends on a switch, VAT, the total and a deposit — plus a count and an average read straight from the order.

The lab, top to bottom: the order, the VAT rate and the express switch; the lines enumerated and priced one at a time, collected by Aggregate JSON and summed into the subtotal; discount and shipping from the subtotal; VAT, total and deposit; each step shows the value of the reference run

The order has three lines — 2 lamps at 89.00, 1 chair at 240.00, 3 cushions at 24.90 — the VAT rate is 0.22 and express delivery is off:

Step Expression Result
Line total, once per line qty * price, Decimals 2 178.00 · 240.00 · 74.70
Subtotal sum(priced_lines[*].line_total) 492.70
Discount: 5% from 300 if(subtotal >= 300, round(subtotal * 0.05, 2), 0) 24.64
Shipping if(express, 15, if(subtotal >= 200, 0, 9.9)) 0
VAT round((subtotal - discount + shipping) * vat_rate, 2) 102.97
Total subtotal - discount + shipping + vat, Decimals 2 571.03
Deposit: 30% round(total * 0.3, 2) 171.31
Number of lines count(order.lines[*].sku) 3
Average unit price avg(order.lines[*].price), Decimals 2 117.97

The discount is rounded before it enters the VAT, as an invoice does: 5% of 492.70 is 24.635, printed and charged as 24.64. Turn express delivery on and shipping becomes 15, VAT 106.27, the total 589.33 and the deposit 176.80. The run takes about ten seconds and costs nothing.

Terminal window
node madoo-install-example.mjs lab-calculate --api <your Madoo API URL> --key <key.json> --run

Date Calculation (utility/date) does for calendar dates what Calculate does for numbers: exactly, instantly, for 0 credits. A contract that starts on 1 November 2026 for four years ends on 31 October 2030; notice six months before that is due by 30 April 2030 — and a model asked for those dates may get them wrong by a day, a month or a leap year.

Operation What it gives Example
add, subtract a date moved by amount days, weeks, months or years (unit) 2026-11-01 + 4 years → 2030-11-01
difference the whole units from date to other_date, negative when other_date comes first 2026-10-01 → 2026-12-20: 80 days
end_of_month, start_of_month the last or the first day of the month 2026-10-17 → 2026-10-31
day_of_week 1 (Monday) to 7 (Sunday) 2026-12-20 → 7
  • ISO in, ISO out. date and other_date take an ISO date (2026-11-01) or date and time (2026-11-01T14:30:00Z) — from data, from Extract, or written on the node — or today, the date in UTC when the step runs. A date written for people (03/10/2026) is refused with DATE_INVALID, never guessed, and so is a date that does not exist (2026-02-30). A date result arrives on date as ISO text, a number on number; the output an operation does not use is absent.
  • Months as contracts read them. Adding months or years keeps the day of the month when it exists and otherwise takes the last day: 31 January + 1 month is 28 February (29 in a leap year), and 31 October − 6 months is 30 April. A difference counts whole units only: 1 January to 31 March is 2 months.
  • amount can come from the data: a duration read from a contract, a number of days from an order. It must be a whole number.
  • Write the result with Format as Text: 2030-04-30 becomes 30 April 2030, or 30 aprile 2030 for an Italian reader.
  • No times, time zones, holidays or working days: Date Calculation counts calendar days.

The dates lab computes a lease’s end and its notice deadline, a warranty’s end, the days to a check-in and its weekday, and the end of the month for an invoice:

The dates lab: a Dates of the case input; Extract reads the lease start, the lease years, the notice months, the purchase date, the check-in date and the date taken as today; Date Calculation steps compute the same day four years later, the lease end the day before (2030-10-31), the last day to give notice six months earlier (2030-04-30), the warranty end two years after purchase (2028-10-01), the days to check-in (80), the check-in weekday (7) and the end of the month (2026-10-31); Format as Text writes the dates out and a Text Template joins them.

The lease ends on 31 October 2030. To leave, give notice by 30 April 2030 (in Italian: 30 aprile 2030).
The warranty ends on 1 October 2028.
Check-in is in 80 days, on day 7 of the week (1 is Monday); the invoice is due by 31 October 2026.

A four-year lease ends the day before the anniversary: the lab adds four years, then subtracts one day. The lab uses a fixed date in place of today, so it gives the same result whenever it runs; it costs nothing.

Terminal window
node madoo-install-example.mjs lab-dates --api <your Madoo API URL> --key <key.json> --run
Code Cause Fix
CALC_UNCONNECTED_VARIABLE (validation) a variable of the expression has no connection connect a value to the port with that name, or rename the variable
CALC_INVALID_EXPRESSION the expression does not parse; the message gives the position fix the character named in the message (%, =, a chained comparison, an unclosed bracket)
CALC_TYPE_MISMATCH a variable is text, a file, or the wrong shape for its use (a list in arithmetic, a boolean as a number) connect a number or a JSON value; use [*] only inside list functions
CALC_PATH_NOT_FOUND a JSON field or position does not exist check the path against a real document
CALC_DIVISION_BY_ZERO a divisor is 0 guard it: if(guests == 0, 0, total / guests)
CALC_EMPTY_LIST avg, min or max of an empty list guard it with if(count(list[*].x) == 0, 0, …)
CALC_OVERFLOW a value is too large for exact decimal arithmetic check the inputs: a real amount never overflows
CALC_MISSING_INPUT no value reached a variable at run time connect it, or make sure its branch runs
DATE_INVALID Date Calculation received no date, a date not in ISO form (03/10/2026) or one that does not exist (2026-02-30) give an ISO date such as 2026-11-01, or today
DATE_MISSING_OTHER a difference with no other_date connect or write the second date
INVALID_DATE_OPERATION an unknown operation or unit, or an amount that is not a whole number choose from the lists; round the amount
DATE_OUT_OF_RANGE the result falls outside the years 1 to 9999 check the amount