Computed expressions
A computed output derives a value with a deterministic JSON expression (arithmetic, string work, date math, and conditionals over other case-data values) that re-evaluates live in the case detail and again at document generation, with $today and $now keeping date math current without re-extraction.
Calculations are computed outputs: one of four derived-output kinds a field carries, alongside AI prompts, static values, and bindings. Where an AI-prompt output asks a model a question, a computed output runs a deterministic formula: same inputs, same answer, every time, no AI call. Use them for totals, day counts, formatted currency, concatenated addresses, and conditional flags. New to outputs and keys? See core concepts.
Where calculations live
Editing templates needs an owner or member role. Open a field's Outputs section in the template editor and click + Add output. This creates a derived output (its Kind defaults to AI instruction); set the Kind picker to Computed (other options: AI instruction, Static value, Binding) and enter the expression. The output gets its own name and key, independently placeable on the PDF with its own write zones.
The expression is JSON in the Computed expression box: an object with an op and args. Each argument is one of three shapes:
| Shape | Meaning | Example |
|---|---|---|
{"literal": ...} | A constant | {"literal": ", "} |
{"ref": "..."} | A case-data value by key | {"ref": "bid_amount"} |
| Nested expression | Another {op, args} | {"op":"add","args":[...]} |
Expressions nest up to 10 levels deep, so you can round a division, format it as currency, and wrap it in a conditional.
What an expression can do
| Group | Operations |
|---|---|
| Math | add, subtract, multiply, divide, round(value, decimals), abs |
| Aggregates (over an array) | sum, avg, count, min, max |
| Formatting | formatCurrency(value, code) (USD → $12,625.58), formatPercent(value, decimals) (0.0875 → "8.75%"), formatDate(date, format) (tokens yyyy, MM, dd) |
| Dates | addDays, subtractDays, daysBetween(from, to) (whole days, to minus from) |
| Strings | concat, substring, replace, uppercase, lowercase, titleCase, trim, isEmpty |
| Conditionals | if(condition, then, else), greaterThan, lessThan, equals |
| Boolean | and, or, not |
References accept several forms:
- A plain key:
bid_amountreads that value; numeric strings become numbers automatically. - A dotted path:
address.cityreads inside a structured value. $today/$now: today's date and the current timestamp; they re-evaluate on every view so date math never goes stale. Meant as operands inside a calculation over a real case date (daysBetween($today, bid_due_date)), not a field on their own. An output referencing only$today/$now/constants yields the same value on every case, so the case detail treats it as a helper and hides it from the Case Data panel.- An array projection:
line_items[*].amountcollects one column from a row-repeating (batch) field into an array forsum/avg/count. - A key pattern:
keys:^line_item_\d+$gathers every value whose key matches the regex, powering "Total" fields that auto-include later-added line items.
Expressions never break a case or a run: a bad reference, divide-by-zero, or malformed expression simply produces a blank value.
When expressions evaluate
You never click "recalculate":
- In the case detail (the Computed bucket of the Case Data panel), values evaluate against current data. Expressions referencing
$today,$now, an array projection, or akeys:pattern re-evaluate on every render, so a "Days Until Bid Due" value counts down by itself. - At document generation, computed outputs recompute fresh before stamping, so the PDF carries the value as of generation. A manual override on the output's key wins over recomputation.
- During extraction, a field's outputs evaluate in order: each can use the field's inputs and any earlier output.
A computed output depending on an unanswered form-question input is dormant: the case detail shows it as pending with the missing inputs listed, rather than a misleading 0 or half-built string.
Worked examples
Days until a bid is due, given a date input bid_due_date:
{"op":"daysBetween","args":[{"ref":"$today"},{"ref":"bid_due_date"}]}
Positive means days remaining, negative means overdue. To show a word for overdue:
{"op":"if","args":[
{"op":"lessThan","args":[
{"op":"daysBetween","args":[{"ref":"$today"},{"ref":"bid_due_date"}]},
{"literal":0}
]},
{"literal":"OVERDUE"},
{"op":"daysBetween","args":[{"ref":"$today"},{"ref":"bid_due_date"}]}
]}
Full-address concat, given inputs address_line_1, city, state:
{"op":"concat","args":[
{"ref":"address_line_1"},
{"literal":", "},
{"ref":"city"},
{"literal":", "},
{"ref":"state"}
]}
concat treats missing values as empty strings, so a case without a city renders the rest rather than failing.
Reference inputs, not other outputs
The one rule that prevents the worst failure mode: point refs at input keys (extracted or question values) or use literals, never at sibling static or binding outputs. Static-kind and binding-kind outputs are never stored on the case; they resolve at generation time only. The case detail evaluates expressions against stored data, so a ref to a static/binding output's key finds nothing and renders blank: a full-address concat referencing a static state output shows "21 Hudson St, Manhattan, " with the state silently missing. Fix it by referencing an extracted or question input key, or inline the constant: {"literal":"NY"}.
This is sneaky because extraction evaluates outputs in sequence (where a sibling's value is visible), so the same expression can look correct there yet render blank in the case detail. Input keys and literals behave identically everywhere.