# Logic

Conditional visibility, validation rules, and expressions

Canonical URL: https://docs.paradoc.dev/concepts/logic/

Logic adds dynamic behavior to forms. Instead of static structures, you can define rules that control when fields appear, when they're required, and what values are valid.

Logic is defined at design time but evaluated at runtime, when the form is filled with data. Expressions are written in `@paradoc/expr`, a small typed language built for intra-artifact logic. Authoring checks and runtime evaluation use the same function and value contract.

***

## Where Logic Applies

Logic expressions can control:

* **`visible`** — whether a field or annex appears
* **`required`** — whether a field or annex must be filled

```ts
const form = p.form()
  .name("application")
  .fields({
    hasVehicle: { type: "boolean", label: "Do you have a vehicle?" },
    vehicleMake: {
      type: "text",
      label: "Vehicle Make",
      visible: "fields.hasVehicle",
      required: "fields.hasVehicle",
    },
  })
  .build()
```

When `hasVehicle` is true, `vehicleMake` becomes visible and required. When false, it's hidden.

The same language runs in every text, Markdown, HTML, and DOCX template: block conditions, loop sources, and placeholders are expressions, so `fields.hasVehicle` means the same in a template as it does here. See [Templates](#templates).

***

## Referencing Fields

A field's value is addressed as `fields.<id>`, for example `fields.hasVehicle` or `fields.age`. The reference resolves to the field's value directly; there is no `.value` suffix. Composite fields expose their parts with a further segment, e.g. `fields.rent.amount` and `fields.rent.currency` for a `money` field.

```ts
// A boolean field is the boolean itself
visible: "fields.hasVehicle"

// A money field's parts
visible: "fields.rent.amount > 2000"
```

Validation rules (`rules`) are the one exception: there, field values are also available unprefixed (`creditScore` as well as `fields.creditScore`).

Parties are addressed as `parties.<role>`: `parties.buyer.name`, `parties.seller.legalName`. A role that allows several parties is a list, so aggregates and row references apply to it, as in `count(parties.tenant) > 1`. A key that is not an identifier, such as a hyphenated checklist item, is read with brackets: `items["signed-contract"]`.

***

## Row References

Inside a list item, `item` refers to the current row. Use it in the `visible` and `required` conditions of the item and of the fields inside it:

```ts
const form = p.form()
  .name("expenses")
  .fields({
    lines: {
      type: "list",
      item: {
        type: "fieldset",
        fields: {
          kind: { type: "enum", enum: [{ value: "travel" }, { value: "other" }] },
          explanation: {
            type: "text",
            visible: "item.kind == 'other'",
            required: "item.kind == 'other'",
          },
        },
      },
    },
  })
  .build()
```

Each row is evaluated on its own, so only the rows whose `kind` is `other` ask for an explanation. The reference follows the row when rows are added, removed, or reordered.

In a list nested inside a list item, `item` is the inner row and `parent` is the row that contains it:

```ts
// A part's note is required when its line is flagged and the part costs more than 100
note: { type: "text", required: "parent.flagged and item.cost > 100" }
```

A row whose item is a single value (for example a list of `number`) exposes that value as `item`, and composite values keep their parts (`item.amount`, `item.currency`). The row's other fields and the rest of the form stay available through `fields.<id>` and defs.

`item` and `parent` are reserved. The checker rejects `item` outside a list item, `parent` outside an item of a nested list, and a def named `item` or `parent`. Defs, rules, annexes, parties, and the list field's own conditions are outside any row.

***

## Operators

```ts
// Comparison: ==, !=, >, >=, <, <=
visible: "fields.country == 'USA'"

// Logical: and, or, not
visible: "fields.isOwner or fields.isAgent"
required: "fields.age >= 18 and fields.hasLicense"
visible: "not fields.isMinor"

// Membership: in, not in
visible: "fields.state in ['CA', 'NY', 'TX']"
required: "fields.role not in ['guest', 'observer']"

// Arithmetic: + - * / %  (+ is polymorphic)
required: "fields.income + fields.bonus > 50000"

// Ternary
visible: "fields.tier == 'gold' ? fields.optedIn : false"
```

`+` is polymorphic: it adds numbers and concatenates strings, resolved by operand type. Use it to build display strings (`fields.firstName + ' ' + fields.lastName`); there is no `||` concatenation operator.

`==` is equality; `=` is not an operator. `&&` and `||` are not part of the language; write `and` / `or`.

***

## Functions

Strings:

| Function                               | Returns        | Notes                                                                                                                                   |
| -------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `contains(haystack, needle)`           | boolean        | substring / membership test                                                                                                             |
| `startsWith(value, prefix)`            | boolean        |                                                                                                                                         |
| `endsWith(value, suffix)`              | boolean        |                                                                                                                                         |
| `matches(value, pattern)`              | boolean        | bounded regular-expression subset: literals, classes, anchors, and wildcards; no repetition, alternation, assertions, or backreferences |
| `trim(value)`                          | string         |                                                                                                                                         |
| `lower(value)` / `upper(value)`        | string         |                                                                                                                                         |
| `length(value)`                        | number         |                                                                                                                                         |
| `isEmpty(value)` / `isNotEmpty(value)` | boolean        |                                                                                                                                         |
| `coalesce(a, b, …)`                    | first non-null | evaluates left to right and skips unused alternatives                                                                                   |

Numbers (decimal-aware):

| Function                                      | Returns                                                                 |
| --------------------------------------------- | ----------------------------------------------------------------------- |
| `round(value, digits?)`                       | number                                                                  |
| `floor(value)` / `ceil(value)` / `abs(value)` | number                                                                  |
| `min(a, b, …)` / `max(a, b, …)`               | number; absent (`null`) arguments are skipped, and all absent is `null` |

Dates:

| Function                      | Returns         | Notes                                 |
| ----------------------------- | --------------- | ------------------------------------- |
| `today()` / `now()`           | date / datetime | the as-of clock, supplied by the host |
| `yearsBetween(from, to)`      | number          |                                       |
| `dateDiff(from, to, unit?)`   | number          |                                       |
| `addDays(date, days)`         | date            |                                       |
| `addDuration(date, duration)` | date            |                                       |

Party and witness predicates: `partyCount(role)`, `signedCount(role)`, `allSigned(role)`, `anySigned(role)`, `partyType(role)`, `witnessCount()`, `allWitnessesSigned()`, `anyWitnessSigned()`. The signing predicates read the instance's signing records: a party has signed once its signature is captured, and a witness once it has attested. Before signing they report nobody signed.

```ts
visible: "isNotEmpty(fields.notes)"
required: "yearsBetween(fields.birthDate, today()) >= 18"
visible: "partyCount('cosigner') > 0"
```

To replace the old `indexOf(...) >= 0` idiom, use `contains(...)`.

***

## Aggregating Lists

A list field holds rows. Seven functions compute one value from them:

| Function                                    | Values                                          | Returns       | No rows |
| ------------------------------------------- | ----------------------------------------------- | ------------- | ------- |
| `sum(path, filter?)`                        | number, percentage, money                       | the same type | `0`     |
| `count(path, filter?)`                      | any row                                         | number        | `0`     |
| `avg(path, filter?)`                        | number, percentage, money                       | the same type | `null`  |
| `min(path, filter?)` / `max(path, filter?)` | number, percentage, money, date, datetime, time | the same type | `null`  |
| `any(path, filter?)`                        | boolean                                         | boolean       | `false` |
| `all(path, filter?)`                        | boolean                                         | boolean       | `true`  |

The first argument is a path into a list. `fields.lineItems.amount` reads the `amount` of every row, and `fields.lineItems` on its own is the rows, which is what `count` usually takes. The second argument is an optional filter: a boolean expression over the same rows, evaluated once per row, so `fields.lineItems.taxable` in the filter means that row's `taxable`.

```ts
const invoice = p.form()
  .name("invoice")
  .fields({
    currency: { type: "text" },
    lineItems: {
      type: "list",
      item: {
        type: "fieldset",
        fields: {
          description: { type: "text" },
          amount: { type: "money" },
          taxable: { type: "boolean" },
        },
      },
    },
  })
  .defs({
    subtotal: {
      type: "money",
      value: { amount: "sum(fields.lineItems.amount).amount", currency: "fields.currency" },
    },
    taxableTotal: { type: "number", value: "sum(fields.lineItems.amount.amount, fields.lineItems.taxable)" },
    rowCount: { type: "number", value: "count(fields.lineItems)" },
    everyRowPriced: { type: "boolean", value: "count(fields.lineItems, fields.lineItems.amount.amount <= 0) == 0" },
  })
  .build()

const draft = invoice.fill({
  fields: {
    currency: "USD",
    lineItems: [
      { description: "Design", amount: { amount: 1200, currency: "USD" }, taxable: true },
      { description: "Hosting", amount: { amount: 99.5, currency: "USD" }, taxable: false },
    ],
  },
})
draft.getFillState().defsValues
// { subtotal: { amount: 1299.5, currency: "USD" }, taxableTotal: 1200, rowCount: 2, everyRowPriced: true }
```

Aggregates work anywhere an expression does: in `defs`, in `visible` and `required`, and in validation `rules`.

A path through nested lists collects values from every level: `sum(fields.orders.parts.cost)` totals every part of every order. A filter can test either level, and each test reads the row at its own depth: `sum(fields.orders.parts.cost, fields.orders.rush)` totals the parts of rush orders. Inside a list item, a [row reference](#row-references) aggregates the current row's own list: `required: "sum(item.parts.cost) > 1000"` on an order's field reads that order's parts only.

The rules are fixed:

* **Hidden rows never count.** A row is hidden when its list, a field above the list, or the list's `item` is not visible. An `item` condition that reads a missing input hides the row, as it does in the form state. Showing the row again brings it back on the next evaluation.
* **Absent values are skipped.** `sum`, `avg`, `min`, and `max` ignore a `null` value; `count` still counts the row. `all` treats an absent boolean as not true.
* **Money keeps its currency.** A sum of money is money in the same currency, with exact decimals. Values in more than one currency fail evaluation with an error naming the currencies found; nothing is converted. A money sum over no rows is the number `0`, since there is no currency to give it.
* **`min` and `max` keep their other form.** Given anything other than a path into a list, they compare their arguments as before.

The checker enforces the shape. An aggregate over the wrong type (`sum` of a text value), a filter that is not boolean, a path that is not a list, and a filter that reads a different list are authoring errors. So is a path into a list used outside an aggregate: `fields.lineItems.amount > 0` reads every row, so the checker asks for `sum(...)`, `any(...)`, or `count(...)` instead. There are no lambdas, and no `map`, `filter`, or `reduce`.

***

## Types

Field references carry their type into the expression, and the checker enforces it.

* **Exact decimals.** Numbers and money are exact: `0.1 + 0.2 == 0.3` holds, so amount math is reliable.
* **Temporal types.** Dates use `YYYY-MM-DD`. Datetimes representing instants require `Z` or an explicit offset. Month and year addition clamps to the last valid day of the target month. `today()` and `now()` use the artifact's retained clock.
* **Precision boundaries.** Dependent definitions retain decimal precision internally. Values exposed to JavaScript callers are ordinary numbers and inherit JavaScript's precision limits.
* **Null-safe access.** A missing or unfilled reference is `null` rather than an error. `null` is a first-class value you can test for, and member access on `null` stays `null` instead of throwing. An operation that needs a value, such as `null * 2`, makes the whole expression missing rather than failed; see [Missing and failed values](#missing-and-failed-values).

***

## Authoring-Time Checking

`validate()` and `fill()` run the expression checker before evaluation. They catch:

* unknown references (`fields.hasVehicle.value` — there is no `.value`)
* row references (`item`, `parent`) used where no list row encloses the expression
* unknown functions
* type mismatches in ordering comparisons (for example, a string field greater than a number)
* gates that don't evaluate to a boolean
* misused list aggregates, and list values read outside one

```ts
// Flagged at authoring time: "Unknown variable: fields.hasVehicle.value"
visible: "fields.hasVehicle.value"
```

***

## Named Expressions

When the same logic appears in multiple places, define it once in the `defs` section and reference it by name:

```ts
const form = p.form()
  .name("application")
  .fields({
    age: { type: "number" },
    drivingLicense: { type: "text", visible: "isAdult", required: "isAdult" },
    parentConsent: { type: "boolean", visible: "not isAdult", required: "not isAdult" },
  })
  .defs({
    isAdult: { type: "boolean", value: "fields.age >= 18" },
  })
  .build()
```

A def's `value` is an expression over the same `fields.<id>` references. Named expressions reduce duplication and create a single source of truth for business rules.

### Missing and failed values

A computed value is **missing** while an input it reads has no value: an unanswered field, a `null`, or another computed value that is itself missing. A missing value is `null`, and it is not an error, so a form with totals can be filled from its first answer without guarding each def:

```ts
.defs({
  subtotal: { type: "money", value: { amount: "sum(fields.lineItems.amount).amount", currency: "fields.currency" } },
  tax: { type: "money", value: { amount: "subtotal.amount * fields.taxRatePercent / 100", currency: "fields.currency" } },
})
// Before the rate is answered, tax.amount is null. Once it is, tax is computed.
```

Whatever reads a missing value treats it the way it treats an unanswered field. A `visible` or `required` condition that cannot be computed yet is false, so the field waits on the answers it depends on. A rule that cannot be computed yet is not met, and fails with its own message.

A computed value **fails** when the operation that errors did not read a missing value: a type mismatch, a division by zero, or a money sum in more than one currency. A missing value elsewhere in the expression does not hide the failure. The failure is reported with the def's name and reads as missing, so the rest of the form still evaluates: other defs, conditions, and the fill state's next field. It blocks completion until the inputs change. An expression that handles `null` itself, with `coalesce` or `?:`, is evaluated as written.

***

## Logic in Annexes

Annexes support the same logic properties as fields:

```ts
const form = p.form()
  .name("lease-application")
  .fields({
    hasPets: { type: "boolean", label: "Do you have pets?" },
  })
  .annexes({
    petPhoto: p.annex()
      .title("Pet Photo")
      .visible("fields.hasPets")
      .required("fields.hasPets"),
  })
  .build()
```

***

## Templates

Layer templates use this language inside their block markers:

```markdown
{{#if fields.monthlyRent.amount > 2000}}A deposit of {{fields.monthlyRent.amount * 2}} applies.{{/if}}
{{#each parties.tenant}}
- {{index(item) + 1}}. {{item.name}}{{#unless last(item)}},{{/unless}}
{{/each}}
Total: {{sum(fields.lines.amount)}}
```

* A block condition must evaluate to a boolean. A loop source must be a list.
* A placeholder accepts any expression. A path prints the value formatted for its type; a computed value is formatted by its result type.
* Inside `{{#each}}`, `item` is the current row and `parent` the enclosing row. A DOCX `{{FOR line IN ...}}` loop names its row.
* `index(row)`, `first(row)`, and `last(row)` give a row's position, and exist only inside a loop.
* A template reads what field logic reads: `fields`, computed values by name, `parties`, and, in a checklist, `items`. It can call the functions the host has configured, and no others.
* `validate()` checks template expressions in inline layers, and `validateLayers()` also reads file layers through a resolver. Errors name the layer, the line and column (or the DOCX paragraph), and the expression.

See [Text layers](/guides/text-layers#template-syntax-reference) for the markers and the syntax they replace.

***

## Design Time vs Runtime

Logic expressions are **defined** at design time but **evaluated** at runtime. The form definition doesn't change—the same logic produces different results based on the data:

```ts
const filled1 = form.fill({ fields: { age: 15, drivingLicense: '', parentConsent: true } })
// drivingLicense is hidden, parentConsent is visible

const filled2 = form.fill({ fields: { age: 21, drivingLicense: 'A-12345', parentConsent: false } })
// drivingLicense is visible, parentConsent is hidden
```
