> ## Documentation Index
> Fetch the complete documentation index at: https://docs.insight.nobly.dk/llms.txt
> Use this file to discover all available pages before exploring further.

# Calculated keywords

> Calculate a keyword from other keywords of the same document every time a document is saved — rule settings, the expression language, where the rules apply, and what users see.

## What a calculated keyword is

A calculated keyword gets its value from other keywords of the same document whenever a document is saved. You set it up as a **rule** on a document type: the keyword to calculate (the **target**) and an **expression** that calculates it.

For example, documents of the document type *UF - Opsigelse* (a termination) must be deleted on 1 January six years after the termination. The keyword *Slettedato* (deletion date) is calculated from *Opsigelsesdato* (termination date):

```text theme={null}
date(year([Opsigelsesdato]) + 6, 1, 1)
```

When someone enters 26 October 2026 as the termination date, *Slettedato* becomes 1 January 2032.

Nobly Insight calculates the value on the server, as part of the save. The rule therefore holds however a document is saved: by a user in the keyword panel, an upload or a form, or by a workflow, a script or an integration. Calculated values go through the same validation as values a user enters. See [where the rules apply](#where-the-rules-apply).

## Before you begin

Open **Admin settings → Keywords → Calculated keywords**.

| To | You need |
| - | - |
| Open the page and read the rules | **View document structure** and **View keyword configuration** |
| Add, change, reorder or delete rules | **Manage document structure** and **Manage keyword configuration** |
| See the **History** of a document type's rules | **View keyword configuration** and **View user groups** |

Both areas are needed because a rule can write a keyword that is read-only on the document type, and read one that is hidden. Without both manage permissions, the page is read-only. See [application permissions](https://docs.insight.nobly.dk/permissions/application-permissions.md).

The target and every keyword the rule reads must be [assigned](https://docs.insight.nobly.dk/configuration/keyword-assignment-models.md) to the document type.

## Set up a rule

![The Calculated keywords settings page with document type UF - Opsigelse selected. One rule calculates Slettedato with the expression date(year(\[Opsigelsesdato\]) + 6, 1, 1), recalculates when an input changes, lets a manual value win when \[Risikotager\] == "JA", leaves the target unchanged when the result is empty, and is enabled. Try this rule is open with Opsigelsesdato and Today both 26/10/2026 and shows Slettedato becomes 2032-01-01.](https://mintcdn.com/nobly/QlJWvaye_rOEVV7d/images/configuration/calculated-keywords.png?fit=max\&auto=format\&n=QlJWvaye_rOEVV7d\&q=85\&s=80095f2602bb2fd5ac8fdd6fdd27464a)

The Slettedato rule on UF - Opsigelse, with Try this rule showing the result for a termination date of 26/10/2026.

**1. Choose the document type**

Pick it in **Document type**. The list marks document types that have rules, and shows an amber badge when some of their rules are inactive. With no document type chosen, the page lists the document types that have calculated keywords.

**2. Add a rule**

Choose **Add rule**, then choose the **Target keyword**. Keywords that cannot be calculated are disabled; hover one to see why.

**3. Write the expression**

Type it, or use **Insert keyword** to add a keyword reference at the cursor. Errors show under the field as you type, with the character where they were found.

**4. Try it**

Open **Try this rule** and enter sample values for the keywords the rule reads, and a date to use as today. The result shows what the target becomes. The manual value condition is not part of the try-out.

**5. Save**

Choose **Save**. The rules of the document type are saved together. Each rule needs a target and an expression. If someone else saved the rules after you opened them, your save is refused: reload the page and apply your change again.

Switching to another document type with unsaved changes asks first. Saved rules apply from the next save of each document; see [what is not recalculated](#what-is-not-recalculated).

## Rule settings

| Setting | What it does |
| - | - |
| **Target keyword** | The keyword the rule calculates. Each keyword can be the target of one rule per document type. |
| **Expression** | The value written to the target. See [the expression language](#the-expression-language). |
| **Condition** (optional) | A true/false expression. The rule applies only while it is true; otherwise the target is left as it is. |
| **Recalculate** | When the rule runs. **When an input changes** (the default) or **On every save** — see below. |
| **Manual value wins when** (optional) | A true/false expression. While it is true, a value entered for the target is kept instead of the calculated one. |
| **Keep earlier manual values** | Keeps a value entered for the target in an earlier save, too. Needs a **Manual value wins when** condition. |
| **When the result is empty** | **Leave the target unchanged**, or **Clear the target**. A required target cannot be cleared; see [a target made required later](#a-target-made-required-later). |
| **Enabled** | A disabled rule is kept but not run. |
| Order | The order of the rule list. Use the arrows on a rule to move it. |

### Recalculate

* **When an input changes.** The rule runs when a document is created, and when a save changes a keyword the expression or condition reads. A save that changes none of them leaves the target alone, so a value entered for the target in such a save is stored as entered.
* **On every save.** The rule runs every time a document of the type is saved. Existing documents are recalculated by any later save, including saves by workflows, scripts and integrations. A save is refused when the rule cannot be calculated from a changed value.

Keywords used only in **Manual value wins when** are not inputs: changing them alone does not run a **When an input changes** rule.

### Manual values

Without **Manual value wins when**, a value entered for the target is replaced whenever the rule runs. With it, and while its condition is true:

* A value entered for the target in this save is kept. On a new document, any value supplied for the target counts. On an existing document, a target value that differs from the stored one counts.
* With **Keep earlier manual values** also on, a stored target value that differs from what the rule calculates from the stored keywords is kept in later saves too, even when they change an input. Without it, an earlier manual value is recalculated the next time the rule runs.

In the example, the manual value condition is `[Risikotager] == "JA"`: while *Risikotager* (risk taker) is *JA*, a deletion date entered by hand is kept.

### Order and chains

A rule can read the target of another rule. Rules then run in dependency order: a rule that reads another rule's target runs after it and sees its new value. Rules that do not depend on each other run in list order. Rules may not depend on each other in a cycle, and disabled rules count for that check, so enabling a rule can never create one.

## Which keywords a rule can use

| Keyword | Read by a rule | Calculated by a rule |
| - | - | - |
| Standalone keyword of type text, whole number, currency, date, or date & time | Yes | Yes |
| Hidden or read-only on the document type | Yes | Yes |
| In a single-instance keyword group | Yes | No |
| In a multi-instance keyword group | No | No |
| Decimal | No | No |
| Encrypted | No | No |
| Used by a [security keyword rule](https://docs.insight.nobly.dk/configuration/security-keywords.md) | Yes | No |
| A member of an [autofill keyword set](https://docs.insight.nobly.dk/configuration/autofill-keyword-sets.md) | Yes | No |

A security keyword cannot be calculated because a calculated value is written by the system and could change who may see the document. An autofill set member cannot be calculated because the calculation would overwrite the autofilled value without refilling the set's other keywords.

A rule writes its target even when the target is hidden or read-only, and reads hidden keywords. When a rule reads a hidden keyword into a visible target, the page warns that users who cannot see the hidden keyword see a value derived from it.

## The expression language

An expression is a single formula: keyword references, literal values, operators and function calls.

### Keyword references

* Write a keyword by its name in square brackets: `[Opsigelsesdato]`. Names match without regard to case and to spaces at either end.
* A `]` inside a name is written twice: the keyword *Amount \[DKK]* is `[Amount [DKK]]]`.
* `[#137]` refers to the keyword with keyword type ID 137. Use it when a name is shared by two keywords on the document type, or cannot be written as a reference because it is empty or starts with `#`.

Rules are stored with keyword IDs, so renaming a keyword type does not break them. The page shows the names again when you open the rules.

### Values and types

Every value has a type, and nothing is converted for you: use `text()` or `date()` to convert. The one exception is that a date can be written to a date & time target, as midnight.

| Keyword data type | Type in an expression |
| - | - |
| Text | Text |
| Whole number, currency | Number |
| Date | Date |
| Date & time | Date & time |

* **Text** literals use double or single quotes, `"JA"` or `'JA'`, with the escapes `\\`, `\"`, `\'`, `\n` and `\t`. A text keyword is read as stored, so a keyword type that stores text in upper case gives upper-case text.
* **Numbers** are written with digits and an optional `.` and decimals, such as `12.5`, with no thousands separator. Calculation is exact to 8 decimals; multiplication and division round half away from zero. A currency keyword is read at the 2 decimals it stores, so `12.345` entered in the same save is calculated as `12.35`.
* **Dates** are calendar dates with no time zone. A date & time value is used as stored, with no time zone conversion. Date & time keywords are stored to the whole second, so a fraction of a second sent through the API is dropped before the calculation reads the value.
* **true** and **false** are the results of comparisons and conditions. They cannot be written to a keyword.
* **empty**, or `null`, is the empty value. See [empty values](#empty-values).

### Operators

| Operator | Works on | Result |
| - | - | - |
| `+` `-` `*` `/` | Numbers | A number. Division by zero refuses the save. |
| `-` | Two dates | The number of days from the right date to the left one |
| `&` | Text | The two texts joined. An empty side counts as no text. |
| `==` `!=` | Two values of the same type, or `empty` | True or false. Text is compared without regard to case. |
| `<` `<=` `>` `>=` | Two numbers, two dates or two date & time values | True or false. False when either side is empty. |
| `and` `&&`, `or` `\|\|` | True/false values | True or false. The right side is only evaluated when it decides the result. |
| `not` `!` | A true/false value | The opposite |
| `-` in front of a value | A number | The negative number |

Operators bind in this order, loosest first: `or`, `and`, `not`, comparisons, `&`, `+` and `-`, `*` and `/`, then a leading `-`. So `not [Risikotager] == "JA"` means `not ([Risikotager] == "JA")`, and `[Amount] * 2 & " DKK"` is not valid, because `&` needs text. Comparisons cannot be chained: write `[A] < [B] and [B] < [C]`. Use parentheses to group.

### Functions

Function names are not case-sensitive.

| Function | Result |
| - | - |
| `if(condition, a, b)` | `a` when the condition is true, otherwise `b`. Only the chosen one is evaluated; `a` and `b` must have the same type. |
| `coalesce(a, b, …)` | The first value that is not empty, from 2 to 10 values of the same type. |
| `isEmpty(x)` | True when `x` is empty, or text of only spaces. |
| `date(year, month, day)` | The date. The parts must be whole numbers that form a valid date: `date(2026, 2, 30)` is an error, not 2 March. |
| `date(text)` | The date in a text written as `YYYY-MM-DD`, such as `date("2026-10-26")`. |
| `date(x)` | The date of a date & time value, without the time. |
| `year(x)`, `month(x)`, `day(x)` | The year, month or day of a date or date & time value, as a number. |
| `addDays(x, n)` | The date or date & time `n` days later; a negative `n` goes back. The time of day is kept. |
| `addMonths(x, n)` | `n` months later. A day the new month does not have becomes its last day: 31 January plus one month is 28 February. |
| `addYears(x, n)` | `n` years later, as `addMonths(x, 12 * n)`: 29 February 2024 plus one year is 28 February 2025. |
| `today()` | The date of the save, in your environment's time zone. The preview users see while editing uses their computer's date. |
| `text(x)` | `x` as text: a number without trailing zeros, a date as `YYYY-MM-DD`, a date & time as `YYYY-MM-DDTHH:MM:SS`. |

`n` must be a whole number. There is no `now()`, and no functions for text such as upper case, trimming or parts of a text, for rounding or for formatting dates.

### Empty values

* A keyword without a value is empty.
* A calculation with an empty part is empty: `[Amount] + 1` is empty while *Amount* is, and so is every function except `if`, `coalesce` and `isEmpty` when one of its values is empty.
* `&` treats an empty side as no text.
* `empty == empty` is true, and `empty` equals no other value. `<`, `<=`, `>` and `>=` are false when a side is empty.
* `if`, `coalesce`, `and` and `or` evaluate only what they need, so an error in a part they skip does not count.
* `true` and `false` are never empty. When `if` or `coalesce` gives true or false, none of its values may be `empty`: `if([Risikotager] == "JA", true, empty)` is an error.

When the expression's result is empty, **When the result is empty** decides what happens to the target. A text result of only spaces counts as empty. To use something else in place of an empty input, use `coalesce()` or `if(isEmpty(…), …, …)`.

### Writing the result

| Target data type | Accepts | Written as |
| - | - | - |
| Text | Text | The text without surrounding spaces. The keyword type's length, mask, value list and check-digit validator then apply as for a value a user enters. |
| Whole number | Number | The number. A result with decimals, or one too large for the keyword type, refuses the save. |
| Currency | Number | Rounded half away from zero to 2 decimals. A result of 1,000,000,000,000,000,000 or more refuses the save. |
| Date | Date | The date. A year before 1753 refuses the save. |
| Date & time | Date & time, or a date (as midnight) | The date and time, to the whole second. A year before 1753 refuses the save. |

The expression's type must fit the target: the page reports an expression that gives a number for a date keyword, for example.

### Limits

| What | Limit |
| - | - |
| Rules per document type | 50 |
| An expression as you write it | 4,000 characters |
| An expression as stored, with keyword IDs in place of names | 1,000 characters |
| Nesting of parentheses, function calls, `not` and a leading `-` | 32 levels |
| Parts of an expression: values, references, operators and function calls | 200 |
| Text in quotes | 255 characters |
| A text result | 4,000 characters |
| A number you write | 20 digits before the decimal point, 8 after |
| Numbers during a calculation | Less than 100,000,000,000,000,000,000 in size |
| Dates during a calculation | 1 January 0001 to 31 December 9999 |
| `addDays`, `addMonths`, `addYears` | At most 3,652,058 days, 119,987 months or 9,998 years |

### Examples

| Purpose | Expression |
| - | - |
| Deletion date six years after the termination year | `date(year([Opsigelsesdato]) + 6, 1, 1)` |
| Due 30 days after receipt | `addDays([Received date], 30)` |
| Last day of the month a document was received | `addDays(addMonths(date(year([Received date]), month([Received date]), 1), 1), -1)` |
| Reference from two keywords | `[Department] & "-" & text([Case number])` |
| The approved amount, or the requested amount until there is one | `coalesce([Approved amount], [Requested amount])` |
| Days from receipt to termination | `[Opsigelsesdato] - [Received date]` |

To stamp the date a status was set, give a date keyword the expression `today()` and the condition `[Status] == "Terminated"`, and keep **When an input changes**. The rule then runs on the save that changes *Status* to *Terminated*, and not again until *Status* changes.

### Not the workflow formula language

Calculated keywords use their own language, not the [workflow formula language](https://docs.insight.nobly.dk/workflows/expressions.md). Workflow formulas work on the whole context of a workflow run, where every keyword is a list and dates are UTC moments, and the workflow engine evaluates them. A calculated keyword is evaluated inside each save of a document, over that document's keywords as single values, with exact decimal and calendar-date arithmetic. The same rule must give the same result on every write path and in the preview users see while they edit.

So the two differ:

* References are `[Name]`, not `$document.kw.Name.Value`. A keyword is one value; one with several values cannot be read by a rule.
* Text is joined with `&`, not `string.cat`.
* Function names are not namespaced: write `addDays`, not `date.addDays`.
* The function set is smaller. There are no text, list, number formatting or date formatting functions, and no `now()`.

Kept the same where it costs nothing: `and`, `or`, `&&`, `||` and `!`, both quote styles, `true`, `false` and `null`, and the function names `if`, `coalesce`, `isEmpty`, `today`, `addDays`, `addMonths`, `year`, `month` and `day`.

## Where the rules apply

The rules of the document type run, on the server, whenever keywords are written:

* uploading and creating documents, including form documents, copies of documents and documents the [mailbox importer](https://docs.insight.nobly.dk/mailbox-importer/archive-rules.md) stores,
* editing keywords in the keyword panel, in bulk edit and in forms,
* re-indexing: a document moved to another document type is calculated with that type's rules, as a new document; a change of document date alone counts as a save of the document,
* workflows, scripts, AI assistants that change keywords — including over [AI assistant access](https://docs.insight.nobly.dk/mcp/introduction.md) — and integrations that write keywords through the API.

A calculated value then goes through the normal validation, so a value that fails the keyword type's length, mask, value list or [check-digit validator](https://docs.insight.nobly.dk/configuration/keyword-types.md#check-digit-validation), or leaves a required keyword empty, refuses the save. Nothing is written in part.

Two more rules protect the calculation:

* **The older v1 API.** A keyword change through the v1 API is refused when it names a keyword that a rule on the document type calculates, or reads in its expression or condition. The message asks to make the change through the v3 API, which runs the rules. Creating documents through the v1 API runs the rules.
* **Saves at the same time.** When a document type has rules, saves of the same document are checked against each other. If another save changed a keyword a rule uses while yours was being calculated, yours runs once more on the new values. If that happens again, it is refused with *The document's keywords were changed by another save. Try again.*

### What is not recalculated

* **Values already stored.** Saving a rule does not change existing documents, and viewing a document never changes it. An existing document is calculated the next time it is saved: by any save for an **On every save** rule, or by a save that changes an input for **When an input changes**.
* **[Mass value editing](https://docs.insight.nobly.dk/configuration/mass-value-editing.md)** replaces stored values directly and runs no rules.
* **Document composition** stores the documents it generates without running rules.
* **Removed rules.** Deleting or disabling a rule leaves the values it calculated.

## What users see

In the keyword panel while editing, in update forms and in create forms, the target shows the calculated value as the user changes the keywords it is calculated from, with a hint under the field:

| Hint | Meaning |
| - | - |
| *Calculated from other keywords* | The rule calculates the value. The field is locked. |
| *Calculated. You can enter a different value.* | The manual value condition is true. A value the user enters is kept. |
| *May be calculated when you save.* | The rule reads a keyword this form or panel does not show — for example one hidden on the document type — so the value cannot be shown in advance. The field stays editable, and the server decides when the document is saved. A required keyword marked this way can be left empty: the server calculates it, or refuses the save when it stays empty. |

What users see is a preview. The browser sends the values the user entered, never the preview, and the server calculates what is stored. While a rule does not run — a **When an input changes** rule whose inputs are unchanged, or a rule whose condition is false — its target shows no hint and can be edited.

When the preview cannot calculate a value, saving is blocked with a message such as *Slettedato could not be calculated because Opsigelsesdato has an invalid value.* In a form the message appears under the target field — in a create form after the first save attempt — and in the keyword panel in the **Save** button's list of validation errors.

Uploads and bulk edit show no preview: the value is calculated when the document is stored or saved. Neither does re-indexing into another document type. When uploading or re-indexing, a target shows *May be calculated when you save.* and stays open to a value. If it is required, it can be left empty: the server calculates it, or refuses the save when it stays empty.

When a save of an existing document changes a keyword through a rule, that keyword change in the document's history ends with *(calculated)*.

### When a save is refused

Any save that a rule cannot calculate is refused as a whole, with a validation error that names the target and never repeats a value, for example:

* *Keyword validation failed: Calculated keyword 'Slettedato' could not be calculated: 'Opsigelsesdato' is not a valid date.*
* *Keyword validation failed: Calculated keyword 'Slettedato' could not be calculated: 'Opsigelsesdato' has more than one value.*
* *Keyword validation failed: Calculated keyword 'Slettedato' could not be calculated: division by zero.*

When the rule reads a hidden keyword, the message names only the target: *Keyword validation failed: Calculated keyword 'Slettedato' could not be calculated.* Workflows, scripts and integrations get the same message in the API's validation error.

An **On every save** rule whose inputs a save did not change is skipped instead of refusing the save, so a stored value the rule cannot read does not block saves that do not touch it.

## Inactive rules and keyword changes

A rule becomes **inactive** when it no longer fits the document type's keywords. For example:

* a keyword it uses is no longer assigned to the document type,
* the target joins an autofill keyword set or a keyword group, or a security keyword rule starts using it.

An inactive rule is skipped when documents are saved and is not previewed. The page marks it **Inactive** with the reason, and the document type list shows an amber badge with the number of inactive rules. Correct the rule or the configuration, then save.

A keyword type that a rule uses, as its target or in any of its expressions, cannot be deleted: the delete is blocked with *Calculated keyword rules reference this keyword type*. Remove it from the rules first. Deleting a document type deletes its rules.

### A target made required later

A rule set to **Clear the target** whose target is made required on the document type afterwards does not become inactive. The rule keeps running, and a save whose result is empty is refused with *Keyword validation failed: Keyword 'Slettedato' is required.* The page warns that an empty result refuses the save, and the document type's rules cannot be saved again until you choose **Leave the target unchanged** for that rule.

## History, promotion and the admin assistant

**History** on the page lists every change to the document type's rules, with who made it.

[Configuration promotion](https://docs.insight.nobly.dk/configuration-promotion/introduction.md) carries calculated keyword rules as one entry per document type, under **Keywords and data**:

* Rules travel with keyword names, never with keyword IDs, and each name is matched to the keyword with that name on the document type in the target environment.
* A document type's rules are **Blocked** when a rule is inactive in the source, when a rule uses a keyword whose name cannot be written in a rule or is shared by two keywords, when a name does not match exactly one keyword in the target, or when the rules do not fit the target's keywords.
* The document type and the keywords must exist in the target, or be created by the same promotion.
* Rules that the source no longer has for a document type are removed in the target. Documents keep the values the rules calculated.
* Exporting and previewing need **View document structure** and **View keyword configuration**. Applying needs **Manage document structure** and **Manage keyword configuration**.

The [admin assistant](https://docs.insight.nobly.dk/ai-administration/admin-assistant.md), and an AI assistant over [AI assistant access](https://docs.insight.nobly.dk/mcp/introduction.md#changing-configuration), can read the rules and replace a document type's rules, with the same permissions as the page and the same checks.

## Where to read next

> **[Document access rights](https://docs.insight.nobly.dk/configuration/document-access-rights.md)**
>
> Grant user groups the privileges to view, create, modify, delete, and otherwise work with documents of each document type.

> **[Keyword behaviour on a document type](https://docs.insight.nobly.dk/configuration/keyword-behaviour.md)**
>
> Required, read-only, hidden and default values, and conditional keyword rules that change behaviour as users enter values.

> **[Keyword types](https://docs.insight.nobly.dk/configuration/keyword-types.md)**
>
> Data types, masks, value lists and check-digit validators — the validation every calculated value passes.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.