Formulas
Check answers against each other, right on the form, with one line. end_date > start_date tells a visitor at once that
their dates are backwards, with no server of your own and no code to host.
Write one
Open a question in the Builder, then Validations, and choose Formula. Write what must be true, and the message to show when it isn’t. The builder reads your formula back in words, and tells you what to fix as you type.
end_date > start_dateTo show or hide a question, open Conditions and choose Advanced: write a formula. A formula can sit in a group with your other rules.
quantity * unit_price >= 100Add a few tries under Try it to see the answer before you publish. They’re saved for whoever edits the form next, and visitors never see them.
What you can write
| You write | It means |
|---|---|
a + b, a - b, a * b, a / b, a % b | Arithmetic |
a == b, a != b, a < b, a <= b, a > b, a >= b | Compare two things |
country in ["CA", "US"] | One of these |
a and b, a or b, not a | Combine |
contact.phone | A question inside a group |
items[].price, items[0].price | A column of a repeated group, or one row |
`first-name` | A key with a hyphen, a leading digit, or the name of a word or function |
Text goes in double or single quotes, and a phone’s curly quotes work too. There’s no assignment, no loop, and no way to reach anything outside this form’s own answers.
Functions
| Group | Functions |
|---|---|
| Checks | luhn(text), isEmpty(value), isEmail(text), isUrl(text), isPostalCode(text, "CA"), isPhone(text) |
| Text | len, lower, upper, trim, contains, startsWith, endsWith, concat |
| Numbers | round(number, places), floor, ceil, abs, min, max |
| Lists | count, sum, contains, unique |
| Dates | date(text), today("America/Toronto"), daysBetween(from, to), dateOf(moment, "America/Toronto"), year, month, day |
| Logic | if(condition, a, b), coalesce(a, b, ...) |
luhn says a number is well formed, nothing more. Don’t collect card numbers in a form.
When a question has no answer
An unanswered question is unknown, so an optional question never fails because it’s empty.
| Case | Result |
|---|---|
| Arithmetic with an unanswered question | Unknown |
<, <=, >, >= with an unanswered question | Unknown |
==, !=, in | Always answers. a == null is true when a is unanswered, and a != "CA" is true too |
| A validation formula that ends up unknown | Passes. Required is what demands an answer |
| A condition that ends up unknown | False. The rule doesn’t apply yet |
| Dividing by zero | Unknown |
So country == "CA" as a validation fails on an empty answer. To allow one, write isEmpty(country) or country == "CA".
Numbers and dates
Numbers are exact decimals, so 0.1 + 0.2 == 0.3 is true everywhere, up to 30 significant digits and 12 decimal places.
Dates are calendar dates, so no time zone can move one. today needs a named time zone, because a business’s today and a
visitor’s aren’t always the same day. A date and a number can’t be compared, and neither can a date and text until date(field) reads the text as a date.
Who can read a formula
Visitors’ browsers run a formula for instant feedback, so anyone can read it. The builder warns you when one holds a value that could matter, such as a code. Don’t put a secret in one.
For a check that shouldn’t show its logic, turn on Check only when submitted, and keep it private. Nothing about it reaches the browser, and the error comes from our server when the form is sent. Even so, someone can try guesses and read the answers, so for a real list of secrets, check it on your own server.
Showing or hiding a question has to work as people type, so those formulas can’t be private. A formula that reads a list is checked only when the form is sent, and can’t show or hide a question.
Our servers repeat every check, whatever a browser says, so skipping a formula in the browser gets nobody past it. A formula can’t read a password field, a recipient, a sender, or a credential.
When a question changes
If you rename a question or move it into a section, the builder updates your formulas when it saves. If you delete one that a formula reads, the builder names what depends on it, and the formula shows as Broken until you fix it. A form can’t be published with a broken formula, and the people on an earlier version keep that version.
A recipient’s condition isn’t part of the published version. A formula there reads each answer as it came in, so one that names a renamed question reads it as unanswered for submissions sent before the change.
Limits
A formula can be 1,000 characters, 500 parts and 32 levels deep, and a form can hold up to 64 KB of them. A list counts its first 1,000 items, and past that the formula is unknown. For anything bigger, split it into two rules.
Going further
Formulas as JSON
A formula goes wherever a condition goes, and as a rule’s validator. Send only text, since the server writes the rest. A
rule of nothing but formulas needs no when.
{
"validationRules": [
{
"id": "dates",
"then": [
{
"path": "end_date",
"validator": {
"type": "formula",
"formula": { "text": "end_date > start_date" },
"code": "END_BEFORE_START",
"message": "The end date has to be after the start date."
}
}
]
}
],
"visibilityRules": [
{
"id": "budget",
"path": "budget",
"default": "hidden",
"state": "visible",
"when": { "formula": { "text": "quantity * unit_price >= 100" } }
}
]
}A public schema sends a formula as { "v": 1, "tree": { ... } }, with no text. v is the language version the formula was
saved in, and it keeps its meaning for good. A version’s functions and results don’t change.
In your own code
validateFormeriePublicFields runs a schema’s formulas once the evaluator has loaded, and forms drawn by the SDK load it by
themselves.
import { loadFormerieFormulas, validateFormeriePublicFields } from "@formerie/client";
await loadFormerieFormulas();
const result = validateFormeriePublicFields(schema, answers);The package ships the language’s specification as @formerie/client/conformance/formula-v1.json. Every entry runs on the
browser and on our servers.
Through MCP
get_form_schema lists the grammar and functions. validate_form_definition checks a formula without saving, and formulaSamples tries one against answers, storing nothing. update_form saves one in definitionPatch. Through MCP or
the API, a formula that doesn’t check out is refused with its FORMULA_ code, rather than saved as a draft to fix later.
See Error codes.