Write a Data Policy
Goal
Add a rule the platform does not ship — "not over your approval limit", "not once it has been invoiced" — and filter a role's permission by it.
Prerequisites
- You are signed in to the Admin Control Center, an environment is selected, and you have opened the solution the policy should belong to — that is where the screen lives, because the policy travels in that solution's package.
- You know the logical names of the fields your rule reads. They are on the table's Fields screen.
Three kinds, and picking the right one
The built-in policies answer which records — mine, my unit's, everybody's. They cover most of what a role needs. Write one of your own when they do not, and the first thing to decide is which kind:
| Which records | Rule on each record | |
|---|---|---|
| What it is | a condition turned into a database filter | a rule checked as a record is saved |
| Use it when | the answer depends on who is asking — their region, their team | the answer depends on what the record says — an approval limit, a status |
| What a user sees | the records simply are not there | a message saying why the save was refused |
| Example | Record.par_region == CurrentUser.par_region | "That total is over your approval limit." |
A Which records policy is the better tool whenever it fits: records it excludes never leave the server, so it costs nothing to enforce and there is no message for anybody to read. It cannot do everything, though — it becomes a query, so it can only compare fields against values.
A Rule on each record policy is the one that can say Total is under my approval limit,
because that is not a question about which records exist. It runs on each record being saved,
and it can explain itself.
The third kind, Whose records (portal), is not for signed-in people at all — it answers the same "which records" question for a visitor on a portal, and it is declared on a table rather than granted to a role. If that is what you are after, this is not the page you want: see Decide whose records a visitor sees.
If your rule is really "everyone must fill this in", you want a business rule or a plugin step on the table instead — those apply to everybody, which is what a data-quality rule means. A policy applies to whoever holds a permission under it, which is what a permission means.
Steps
-
Open Solutions, open the solution your application lives in, and select Data policies in its navigation. The screen lists the policies this solution carries; the default ("Main") solution lists every policy in the environment.
-
Select New.
-
Give it a display name — what it will be called in the policy list on the roles screen. The logical name fills itself in under your publisher prefix; you can change it now but not later, because it is the name every grant will refer to.
-
Choose the type. This decides what the rest of the form asks, and it cannot be changed afterwards. The steps below are for Rule on each record; for Which records, skip to Writing a Which-records policy.
-
Choose Which records. For a rule policy this is the built-in that filters lists, and it is not optional — see Why a rule policy also needs a scope. Every record means "everything, subject to my rule".
-
Tick the operations to check the rule on. The default is create, update and delete. Leave Read off unless you mean it: a rule that refuses a read makes records vanish from lists for a reason nobody can see.
-
Write the rule. It is BS Lang, the same language as a business rule, and it runs against one record:
if (getValue("par_total") > currentUser("par_approvallimit")) {deny("That total is over your approval limit.");}Call
deny("…")to refuse. A rule that denies nothing allows the operation. The message you write is what the user sees — so say what would make the record acceptable, not that something was wrong. -
Select Save. If the rule does not parse, the save fails and says where. That is deliberate: the alternative is finding out when somebody is refused by a rule that cannot run.
-
Open the solution's Roles screen, and set the cell for the permission you want filtered to your new policy. It appears in the same list as the built-in ones.
Writing a Which-records policy
A Which records policy becomes a database query, so it is written differently and asks for different things.
-
Pick the table. Unlike a rule, this kind of policy is written against one table's fields, so it has to name one — and naming it is what lets the platform check those fields exist when you save. It will only be offered on that table's row on the solution's Roles screen.
-
Write the condition — one condition, not a script:
Record.par_region == CurrentUser.par_region && Record.par_status != "Closed"Record.<field>is a field on the chosen table.CurrentUser.<name>is something about the signed-in person — the same names listed under What a rule can read, plus any field on the user table. Join conditions with&&and||, and group them with brackets. -
Comparing against a set means "is one of":
Record.owning_unit == CurrentUser.UnitsAndBelowselects records owned anywhere in your unit or below it. You can compare against a written-out list too:Record.par_status == ["Draft", "Submitted"]. -
Select Save. Anything the platform cannot turn into a query is refused now, with the reason — see below.
What a condition cannot say
This is deliberately narrow, because everything in it has to become a database query:
| Not allowed | Because | Instead |
|---|---|---|
>=, <= | there is no query operator for them | > or < |
!(…) | a query cannot be negated | say what to keep, using != |
length(x), any function | it would have to be run, and a filter is not run | compare the field directly |
Record.total * 2 > 100 | arithmetic stops a query using an index | compare against the number you mean |
Record.account.owner | following a lookup is a join, and a hidden join is slow on every query | a field on this table |
| several conditions on several lines | a policy is one condition | join them with && |
All of these are refused when you save, naming the line. That is the whole reason the condition language is small: a condition the platform quietly could not enforce would show records somebody believed it hid.
What a rule can read
| Call | Gives you |
|---|---|
getValue("par_total") | The record as it will be after the change. |
getPreValue("par_total") | The record as it is stored now. Empty when creating. |
currentUser("par_approvallimit") | A field on the signed-in person's own user record. |
isOwner() | Whether this person owns the record. |
hasRole("Manager") | Whether they hold a role. |
operation() | Create, Read, Update or Delete. |
entityName() | The table's logical name. |
deny("…") | Refuse, with the reason the user will read. |
currentUser reads the user table in this environment, so to compare against an approval
limit you first add that field to it, exactly as you would to any other table.
Why a rule policy also needs a scope
A rule needs a record that has already been loaded. A list is precisely what you have before you have loaded any, so a rule cannot filter one — the platform would have to fetch every row to decide which rows to fetch. A Which records policy has the opposite property, and that is exactly why it is a separate kind rather than a cleverer rule.
So a rule policy carries both: Which records is a built-in the platform pushes into the database, and the rule is checked afterwards on each record somebody actually opens, saves or deletes. Set Which records to the narrowest built-in that is true, and let the rule handle the rest.
Turning one off
Untick In force and save. The policy still exists and every grant under it keeps working — under its Which records answer alone, with the rule not checked. That is the safe thing to do while working out why a rule is rejecting something; deleting the policy is not, and the platform will not let you delete one a role is still using.
A Which records policy has no such switch, and that is not an oversight: it is the answer to which records, so turning it off could only mean showing everything or showing nothing, and neither is what anybody would mean by pausing it. Change its condition, or change the grant.
Policies travel with the solution
A policy belongs to a solution, the same way a table or a form does, and it goes with that solution when you export it and lands when you import it somewhere else.
That is the point of writing one. "An invoice over my approval limit is not mine to approve" is part of how the application works, not a setting somebody configures per environment — so shipping the app without it would leave Test and Production silently permitting what Development refused, which is the worst possible direction for a security rule to fail in.
Three things follow from that:
- The rule is checked again on import, exactly as it was when you wrote it. A package carrying a policy that no longer compiles is refused at import rather than discovered by the first person it wrongly denies.
- A policy shipped switched off arrives switched off. Untick In force on a rule staged ahead of a go-live and the target environment gets it in that state; the import will not turn a rule on that nobody has asked for yet.
- Uninstalling the solution removes the policy — and the grants made under it, which is the one time those go without being asked about. A grant whose policy has gone would otherwise start denying everything it once allowed, with nothing on screen to say why.
Who is granted something under the policy does not travel: that names your people, and it stays in the environment where you decided it.
Troubleshooting
- "The policy's rule does not parse." — a typo in the rule. The message names the place.
- "… is one of the platform's own policies." — you have used a built-in name. Policies of your own carry your publisher prefix.
- "… still filters permissions held by N role(s)." — change those grants on the solution's Roles screen first. Deleting the policy now would deny every one of them.
- Nobody can save anything on the table. — check the rule's comparison. A rule reading a
field that does not exist gets an empty value, and
"" > 100is not the comparison you meant. Untick In force while you work it out. - The rule never seems to run. — check the operations you ticked, and that the role's grant actually points at your policy rather than at a built-in.
- A user is refused with a message about a policy that could not be evaluated. — the rule failed to run. The operation is refused rather than allowed, because a rule that could not run has not said the record is acceptable. Fix the rule, or untick In force.
- "This condition cannot be turned into a query." — a Which-records condition using something outside what a query can say. The message names the line and what to use instead; the table above lists them all.
- My Which-records policy is not offered on the row I want. — it is written against one table, so it is only offered on that table's row, and never on All tables. Write a separate policy for the other table.
- A Which-records policy shows nobody anything. — a condition that resolves to nothing selects
nothing, which is the safe direction the platform always takes. Check the field names and, if
you compared against a set like
CurrentUser.Teams, that the person is actually in one.