Calculations and actions
Calculated fields, reusable functions, automatic actions and triggers, their limits, and why a file with triggers needs approval on each computer.
A Nendo file can hold four kinds of behaviour beside its records and screens:
| Kind | What it does |
|---|---|
| Calculation | A field that is computed from other values each time it is read. Nobody types into it. |
| Function | A reusable formula with typed parameters. Calculations call it by name. |
| Action | Ordered steps that set a field, create a record or delete a record. |
| Trigger | Runs an action when a record of one type is created, updated or deleted. |
Behaviour is part of the app definition. You add or change it through a change set that becomes a proposal, and a person accepts the proposal. Today an agent writes these definitions through the MCP server. Studio shows them but has no editor for them. See Agents for the authoring flow and Concepts for the terms.
Calculated fields
A calculated field belongs to one record type. It has a result type, a formula and a list of bindings. It has no stored column, and no edit, import or agent write can set it. A write that names one is refused as NENDO_FIELD_CALCULATED.
Result types
A formula works with five kinds of value: Integer (64-bit whole number), Decimal (exact .NET decimal), Boolean, Text and Date (a calendar date). Each can be declared to allow an empty result. Stored date-time, UUID and reference values are valid data, but a formula cannot use them as values. A reference only resolves a binding.
Numbers stay exact from the file to the screen and to an agent. A decimal total never passes through a floating-point number.
Bindings
A formula cannot name a field directly. Each name in a formula is a binding, and the binding states where the value comes from by stable ID. A rename of a record type or a field therefore changes no formula.
| Binding | Reads |
|---|---|
SameRecordField |
A stored field on the same record |
SameRecordCalculation |
Another calculated field on the same record |
ReferenceTraversal |
A stored field on the record that one reference field points to |
RelatedAggregate |
A total over the records that reference this one |
SubtreeAggregate |
A total over the records under this one, at every level, in a record type kept as a tree |
HierarchyPath |
The record’s place in a record type kept as a tree, as 1.2.3, with an optional prefix such as CAP- |
A related aggregate is one of three:
- Count counts the related records. An empty set gives 0.
- FilteredCount counts the related records whose Boolean field is true.
- Sum adds an Integer or Decimal field over the related records. An empty set gives 0. The field must be required, because a missing value is an error and not a zero.
A subtree aggregate is one of the same three, over the records under this one; includeSelf adds the record itself. A related aggregate with acrossSubtree counts or totals the records that point at this record or at anything under it.
A hierarchy path follows a move at once, because it is worked out on every read. That makes it a display code, not a name to write down: a code people repeat belongs in a numbered field.
An aggregate never returns a partial total. If there are more related rows than the limit, or the total does not fit exactly in the result type, the calculation fails.
Operators
| Operator | Takes | Gives |
|---|---|---|
+ - * % |
two numbers | a whole number, or a decimal if either side is decimal |
/ |
two numbers | always a decimal, also for whole ÷ whole |
== != |
two values of the same kind | true or false |
< <= > >= |
two numbers, dates or texts | true or false |
and or |
true or false | true or false; the right side is skipped when the left side decides |
not, unary - |
true or false, a number | the same kind |
? : |
a condition and two values | one of the two values; the other side is not evaluated |
Text and true/false values never become numbers: '2' + 1 and true + 1 are refused when the definition is installed. Power, bitwise operators, lists, member access and any undeclared name are also refused.
Functions
The function set is closed:
| Function | Result |
|---|---|
RoundEven(number, digits) |
Decimal rounded to 0–28 digits, halves to even |
RoundAway(number, digits) |
Decimal rounded to 0–28 digits, halves away from zero |
Date(text) |
A date from yyyy-MM-dd text; an invalid date is an error |
DaysBetween(start, end) |
Signed whole number of days |
Concat(a, b, …) |
2 to 8 texts joined |
TextLength(text) |
Number of characters; empty text is 0 |
Refuse(text) |
No value; the calculation fails with your text as the reason |
No function reads a clock, a file, the network or any other part of the computer. The same inputs always give the same result.
Use Refuse in one branch of a choice to reject a value, for example rating > 5 ? Refuse('Ratings go from 1 to 5') : rating. A formula that can only refuse is invalid.
Reusable functions
A function has a display name, typed parameters, a result type and a formula. A calculation calls it through a call alias, for example Percent(done, total). A function can call other functions. Calls in a loop are refused, and a chain of definitions can be at most 8 deep.
Empty results and errors
A result is a value, an empty value or an error.
- An empty input stops the formula when it is used in arithmetic, a comparison, a condition or a function argument. It never counts as 0 or false.
- If the calculation allows an empty result, the result is empty. If not, the result is an error.
- A division by zero, an overflow, an invalid date, a text that is too long, a refusal or a limit gives an error with a reason.
- If a calculation reads another calculation that failed, it fails too and names that input.
One failed calculation does not affect the other fields of the record. The stored input stays saved. When you correct the input, the calculation recovers.
Automatic actions and triggers
A trigger belongs to one record type. It subscribes to one or more of the events Created, Updated and Deleted. An update subscription can name the fields that matter, and then fires only when one of them changes. A trigger can also carry a Boolean condition.
An action is a list of steps. Each step is one of:
SetField: set fields on a record;CreateRecord: create a record;DeleteRecord: delete a record.
A step targets the record that raised the event (EventRecord), or the record that one reference field on it points to (ReferencedRecord). If that reference is empty, the step writes nothing and the save still commits. Make the reference field required if every event must reach a target.
A step writes ordinary record data through the same checks as a person’s edit: record versions, required fields and reference targets. It cannot change a definition, the schema, access settings or anything outside the file.
Order and atomicity
A save and every change its actions make are one transaction and one revision. All of it is stored, or the file does not change.
- Nendo stages the whole edit.
- It compares the staged state with the state before the edit. A field set to the value it already had is no change and raises no event.
- For each event, in a stable order, it runs the matching triggers in order of their stable ID. Each step sees the effects of the steps before it.
- Writes made by actions raise their own events, which join the same queue.
- Nendo commits once.
If an action cannot complete, if a limit is reached, or if the save is cancelled, nothing is saved. The refusal names the action, the step and the field. If only a trigger’s condition cannot be evaluated, that action does not run and the edit still commits.
The save result lists every other record that the actions changed (alsoChanged over MCP). History records which trigger, action and step produced each change. Reversing the save reverses the edit and every change its actions made, and does not run the actions again.
Nothing runs when a file opens, when a screen draws, or when a person approves the actions. Actions run only when a record changes.
Worked example
This is taken from the calculate-and-act-automatically example that the MCP server publishes at nendo://application/examples. A project counts its tasks, and a new task marks its project active. These are three of the operations in that change set. The record types, fields and the taskProject reference come earlier in it.
[
{
"operationType": "behaviour.setDefinition",
"payload": {
"definitionId": "project.totalHours",
"definitionKind": "Calculation",
"body": {
"entityId": "project",
"fieldId": "totalHours",
"displayName": "Hours",
"resultType": "Integer",
"resultNullable": false,
"expression": "hours",
"bindings": [
{
"bindingId": "hours", "kind": "RelatedAggregate", "aggregate": "Sum",
"entityId": "project", "relatedEntityId": "task", "relatedReferenceFieldId": "taskProject",
"valueFieldId": "taskHours", "resultType": "Integer", "nullable": false
}
],
"callAliases": []
}
}
},
{
"operationType": "behaviour.setDefinition",
"payload": {
"definitionId": "project.markActive",
"definitionKind": "Action",
"body": {
"displayName": "Mark the project active",
"steps": [
{
"stepId": "10-status",
"kind": "SetField",
"target": { "kind": "ReferencedRecord", "referenceFieldId": "taskProject" },
"assignments": [
{ "fieldId": "projectStatus", "expression": "'Active'", "bindings": [], "callAliases": [] }
]
}
]
}
}
},
{
"operationType": "behaviour.setDefinition",
"payload": {
"definitionId": "task.onCreated",
"definitionKind": "Trigger",
"body": {
"entityId": "task",
"displayName": "Mark the project active when a task arrives",
"events": "Created",
"actionId": "project.markActive",
"relevantFieldIds": [],
"conditionBindings": [],
"callAliases": []
}
}
}
]
The bindings of a step resolve against the record that the step writes to. A step that targets the referenced project can read the project, but not the task that raised the event. Nendo checks each action and its trigger together when either one is installed.
Limits
The limits belong to the host. A file cannot raise them.
| Resource | Limit |
|---|---|
| Formula length | 2,048 characters |
| Tokens in a formula | 128 |
| Nodes in a parsed formula, and nesting depth | 128, and 24 |
| Depth of the chain of definitions | 8 |
| Functions in a file, or call aliases in one formula | 32 |
| Definitions in a file, of all kinds | 256 |
| Bindings or parameters in one formula | 16 |
| Stable ID or alias length | 128 characters |
| Text value length | 4,096 characters |
| Work units per evaluation or save | 16,384 |
| Function calls per evaluation or save | 64 |
| Related rows read per evaluation or save | 256 |
| Records written by actions per save | 64 |
A save and all the actions it starts share one budget. If a chain of triggers reaches a limit, the whole save is refused. These limits bound the work a formula can do. They are not a memory sandbox or a time limit.
What a person sees
A calculated field is shown, but it is not offered for editing. It has four states, and each looks different:
| On screen | Meaning |
|---|---|
| The value | The calculation produced a value. |
| Not set | An input was empty, and the calculation allows an empty result. |
| Calculating… | Nendo is still working it out. |
| Cannot calculate | The calculation failed. The reason is shown beside it. |
| Unavailable | An input calculation failed. The input is named. |
Studio shows the formula below the value. A finished screen in Use shows the value and a Calculated mark, without the formula. A total on a summary tile that does not fit exactly in its type reads Unavailable with the reason.
What screens cannot do with calculated fields
A calculated field can appear on any screen: a record page, a list column, a board card, or a related list. A list can also be sorted by one, or filtered on one: Tasks is 0 lists the records nothing points at. Nendo works that out over every record the list could show, up to 10,000 of them; past that, add a filter on an ordinary field to narrow it.
A calculated field still cannot group or total anything, because the database decides those over every matching record and a calculated field has no column. A screen that uses one for any of these is refused with NUI214, and the refusal names the field:
- group a board or a breakdown chart;
- place a record on a calendar, a timeline, a trend chart or an activity grid;
- be the rows, columns or rank of a matrix or ranked list;
- feed a summary tile, breakdown chart, trend chart or range tile;
- be set by a command step.
A form made only of calculated fields is refused with NUI215, because it has nothing to save. See Screens.
Approval on this computer
A file that carries a trigger cannot be edited until a person approves its actions on this computer. A file with calculations and no trigger needs no approval, because a calculation writes nothing.
Why. Actions change data without a person pressing anything. A file you receive can carry actions that somebody else wrote. Nendo does not let them run until you have seen what they may do: add records, change records or delete records.
Until you approve. The file opens, reads, calculates, exports and backs up. Only editing is off, for people and agents alike. The status pill reads Approval needed. Nendo never switches a trigger off to let a save through.
Where to approve. Use Approve automatic actions under Health, in File status, or on the Agent page. A proposal’s review says beforehand whether accepting it needs approval first, or pauses editing until you approve. Withdraw approval takes it back. The panel shows a short digest of the rules, so two different sets of rules look different.
What it covers. An approval names the application, this copy of the file, the exact rules, the behaviour contract version, the definition revision, and the kinds of change the actions can make. Any accepted change to the definition asks again, also one that adds only a screen.
Where it lives. Approval is stored on this computer, under %LocalAppData%\Nendo, never in the .nendo file. A file cannot carry its own permission. A copy, a Duplicate, a Fork, a restored backup, or the same file on another computer asks again. A missing or damaged approval record approves nothing.
The Unattended exception. At the Unattended access level, accepting a change set over MCP also records this computer’s approval for the actions that the change installs. The person can withdraw it under Agent or Health. Below Unattended, no MCP tool or resource can reach the approval. See Agents.