# 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.

```txt
end_date > start_date
```

To show or hide a question, open **Conditions** and choose **Advanced: write a formula**. A formula can sit in a group with
your other rules.

```txt
quantity * unit_price >= 100
```

Add 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, so an error shows as soon as someone leaves the field, and 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`.

```json
{
  "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.

```ts
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](/guides/error-codes/).
