Write a Business Rule
Goal
Make a form respond to what the person filling it in is doing: show the Manager field only on expenses over £100, disable Close date until the deal is won, default Currency from the account.
Prerequisites
- You are signed in as a maker, with the table's solution open.
- The table has the fields the rule talks about, and — if you want to bind the rule to one form — that form exists.
What a business rule is, and what it is not
A business rule runs in the browser, on the form, and changes how the form behaves. It has four verbs: show or hide a field, enable or disable it, make it required or not, and set a value.
It does not decide what may be saved. Someone can turn a rule off, use a different screen, or call the API directly, and the record still saves. Anything that must be true of the data belongs in a server plugin step, which runs whatever the browser did. Use a business rule to make a form pleasant; use a plugin step to make a rule real. It is normal to want both, and they are written separately on purpose.
Steps
-
Open the form the rule is for — the table, then Forms, then the form — and select Events in its toolbar. The list shows every rule that runs on this form, including any bound to every form of the table.
-
Select New. The rule designer opens.
-
Type a display name. The logical name fills itself in, prefixed with your publisher's prefix, and is fixed once the rule is saved.
-
Check the form. It is already the form you came from, which is nearly always right — a rule talks about fields, and the fields it talks about are the ones in front of you. Switch it to Every form for something that should hold wherever the table is shown, like "hide this unless the status is Approved". A rule bound to every form still appears in each form's Events list, marked as such.
-
Choose when it runs — one event per rule:
- When the form opens applies the rule as the record is displayed.
- When a field changes shows a field picker. Pick a field and it appears as a chip; add as many as the rule needs, and remove one with the ✕ on its chip.
- When the form is saved runs the rule between the press and the write, so it can fill
something in — or stop the save with
cancelSave, below.
A rule with no trigger never runs, so Save stays disabled until a field-change rule has at least one field.
One event per rule is deliberate. When the same thing has to happen in two moments — the usual case, "show this when the status is rejected", which must be right when the form opens and whenever the status changes — write two rules that import one solution script, each two lines long. The logic is written once; where the two moments genuinely differ, the script asks
eventType()and decides for itself. Several triggers on one rule would put that decision nowhere: nothing on the screen would say which half of the script belonged to which event. -
Write the script in the editor. It underlines mistakes as you type, completes the platform's functions, and shows each one's arguments as you type the opening bracket. The eye in the editor's top corner opens What a script can call over the code.
-
Leave Enabled ticked, or clear it to keep the rule without running it.
-
Select Save.
What a script can call
| Call | What it does |
|---|---|
getValue(field) | The field's current value on the form. |
setValue(field, value) | Sets a field on the form. |
setVisible(field, visible) | Shows or hides a field. |
setDisabled(field, disabled) | Makes a field read-only, or not. |
setRequired(field, required) | Makes a field required, or not. |
eventType() | "OnLoad", "OnChange" or "OnSave" — which event is running. |
changedField() | The field whose change fired an OnChange, otherwise "". |
isNewRecord() | true until the record has been saved once. Use it to lock a field that is chosen once and then stays, such as an activity's type. |
cancelSave(message) | On save only: stops this form's save and shows the message. |
isEmpty(value) | Whether a value is not filled in. Blank text and nothing at all are empty; 0 and false are values somebody chose. |
A field is named by its logical name (par_amount), not its display name.
A field with nothing in it reads as empty text, so getValue("par_currency") == "" is the
check for "not filled in yet" — that is how the currency example below works. For a number, a
date or a lookup there is no empty text to compare against, so ask isEmpty(getValue("par_due"))
instead. It uses the same definition of empty as the form's own required check, which is why a
rule cannot default a field the form has just accepted as filled.
getValue and setValue are spelled and behave exactly as they do in a server plugin
step, so the logic you work out once reads the same on both sides.
A number field reads as a number — 1200.5, not "1200.5" — even while it is being typed, and
an unfilled one still reads as empty. That matters the moment you write a function of your own:
func needsApproval(amount: number) will not accept a piece of text, and the failure is
Function overload for needsApproval not found. rather than anything about types. So declare an
Amount as number, not int — a Decimal column holds 1200.50, and a whole-number parameter
refuses it — and stand a zero in for an empty box before you do arithmetic on it:
func claimAmount(): number {
var raw = getValue("par_amount");
if (isEmpty(raw)) { return 0; }
return raw;
}
…and anything a solution script exports. When the same few lines start appearing in
several rules, move them into a solution script and import it by
typing import 'par_expense_helpers'; at the top of the rule — the editor completes the name,
and from then on it knows what the module exports.
Choices, and showing a section for one of them
A choice reads as its number, the value the option was given (110000001), whether the form
has just opened or somebody has just picked it. So compare it with a number, not with text:
if (getValue("pw_type") == 110000001) { setVisible("pw_to", true); }
Before this, a choice somebody had just picked read as text ("110000001"), and the comparison
above quietly stopped working after the first pick. A rule that compared against text in quotes
needs the quotes taken out.
To show a section only for one choice, hide the fields in it. A section whose fields are all hidden disappears, title and all, and comes back when a rule shows one of them again. That is how the Sales activity form shows an Email section for an email and a Phone call section for a call.
To let a field be chosen once, disable it when the record is not new:
setDisabled("pw_type", !isNewRecord());
When a new record is saved, the rules that run on load run again, so the field locks straight away. Like every rule, this only stops the form. To stop the API or an import changing the field too, write a server plugin step as well.
Stopping a save
A rule that runs when the form is saved can refuse the save:
if (getValue("par_status") == "rejected" && getValue("par_reason") == "") {
cancelSave("Give a reason before rejecting this claim.");
}
The save stops, your message appears at the top of the form, and nothing is lost — the form stays open with everything typed into it.
This is still not enforcement. It stops this form, in this browser: the API, an import, a custom app, and any other form without the rule all save the record regardless. It exists because "do not let me submit this half-filled in" is worth saying on the form — but if the thing must be true of the data, write a server plugin step as well.
An example
Show the manager field, and require it, only on expenses over 100 — on load and whenever the amount changes:
var over = getValue("par_amount") > 100;
setVisible("par_manager", over);
setRequired("par_manager", over);
Note that it says what should be true both ways. A rule that only ever shows a field will leave it on screen after the amount comes back down, because nothing asked for it to be hidden again.
Two rules on one form
Every enabled rule that matches the form and the event runs, in order of logical name, each with its own variables. They share the values on the form, so a rule that sets a field is seen by the rules after it. If two rules speak about the same field, the last one wins — which is stable, but it is a coincidence rather than a design, so prefer one rule per field.
A rule cannot run forever
Every rule runs with a budget, and a rule that passes it is stopped and reported against its own name. There are three limits, and each catches a different mistake:
| It stops | When |
|---|---|
| a loop that never ends | while (true), or a condition that is never quite met |
| a rule that is simply doing too much | it is still going after half a second |
| a function that calls itself for ever | it is a hundred calls deep with no end in sight |
The form keeps working, the person keeps whatever they were typing, and the message names the
rule and the line. Wrapping the loop in a try does not help — the budget is not something a
script can catch.
You will not meet this by accident: the largest rule in any sample uses well under a hundredth
of the allowance. If you do meet it, the rule has a bug — most often a while whose condition
the body never changes.
A note on comparing values. == compares within a kind: two numbers, two pieces of text,
two true/false values. Two different kinds are never equal — 1 == "1" is false — and nothing
equals nothing, so an unset field is not equal to "". That is why a field with no value reads
as empty text, and why isEmpty() exists for numbers and dates.
Troubleshooting
- A rule that never seems to run. Check it is enabled, that its form binding matches the form you are looking at, and that its trigger includes the field you are changing.
- "BS Lang script is invalid: …" on save — the script does not parse. The editor underlines the same mistake; the message names the line.
- "Form '…' was not found on '…'." — the rule is bound to a form the table does not have. Choose one from the list, or bind to every form.
- A red message at the top of a form in the app, naming a rule. The rule threw while running. The form keeps working; the rule stopped where it failed.
- Underlines under
getValue— the editor no longer reports the platform's own functions as undefined. If you see this, the script is calling something that is not in the list above.
Related scenarios
- Create a Form.
- Make a Field Required or Read-Only — the fixed version of the same idea, for when the answer never depends on the record.