Formula language
Look up every construct of the formula language — literals, operators, predicates, iteration, dataset references, and where each is allowed.
The language is growing
This reference is not the final shape of the language. The formula DSL will be substantially extended in the near term. The page tracks what ships today, and expands with the language.
Every condition and filter in the access model is a formula — one boolean expression, stored as data, validated when it is saved, evaluated against the attributes of a request. This page is the formal reference for the language. What formulas mean for decisions — targets, permit-only combination, explainability — lives in the Access model.
user.role in ["manager", "admin"]
and context.country = "US"
and (any user.permissions where it startsWith "billing:")Ground rules
- A formula is a single boolean expression. There are no statements, no variables, no comments.
- Keywords are lowercase and reserved.
andis the operator — uppercaseANDdoes not parse. - Attribute names are case-sensitive identifiers: letters, digits and underscores, not starting with a digit.
- A reserved word cannot be a path segment. A schema attribute named
inorwherecannot be referenced. - Whitespace and line breaks carry no meaning. A formula may span several lines.
Where formulas appear
Every formula slot parses in its own context, and the context decides which attribute roots are available. A reference outside its context fails when the formula is saved — not at evaluation time.
| Formula slot | user.* | context.* | action.* | request.* | data.* | dataset.* |
|---|---|---|---|---|---|---|
| Permission policy condition | ✓ | ✓ | ✓ | ✓¹ | — | — |
| Data policy condition | ✓ | ✓ | — | — | — | — |
| Data policy filter² | ✓ | ✓ | — | — | ✓ | — |
| Action target condition | — | — | ✓ | — | — | — |
| Data target condition | — | — | — | — | — | ✓ |
¹ Through the request schema the policy's target provides — see Action policies. Under an ALL target there is no request schema, and request.* is rejected.
² Row filters exist on INDIVIDUAL data targets only — see data targets.
Dataset references — exists <dataset> where … — are allowed in permission policy conditions only. After the where, data.* additionally names the referenced dataset's columns — see Dataset references.
Literals
| Literal | Syntax | Examples |
|---|---|---|
| String | double quotes — \" for a quote, \\ for a backslash | "admin", "O\"Brien" |
| Integer | digits, optionally signed | 42, -5 |
| Decimal | digits on both sides of the dot, optionally signed | 99.95, -0.5 |
| Boolean | lowercase | true, false |
| Array | non-empty brackets | ["a", "b"], [1, 2, 3], [-1, 2.5] |
.5and1.do not parse. Write0.5and1.0. The same rule holds inside arrays.- Array elements are literals, in any form a scalar literal takes — signs and decimals included. An attribute reference cannot appear inside an array.
- All elements share one type, with one relaxation. Integers and decimals mix, and the array becomes a decimal array —
[1, 2.5]. Any other mix is rejected when the formula is saved. - There is no null literal. Absence is tested with
is null.
Attribute references
Formulas read attributes through six roots. The dot is part of the root — write user.role, not user . role.
| Root | Refers to | Shape declared by |
|---|---|---|
user. | who is making the request | the workspace user schema |
context. | runtime circumstances | the workspace context schema |
action. | the action under decision, action.tags included | the workspace action schema |
request. | the parameters of this particular call | the request schema in scope |
data. | one column of the dataset row in scope | the dataset's data schema |
dataset. | the identity of the dataset in scope | fixed — four paths |
- Paths nest with dots where the schema nests:
user.profile.age. data.<column>names exactly one column and never nests —data.a.bis rejected.dataset.*has exactly four paths —dataset.datasource,dataset.schema,dataset.tableanddataset.code, each a lowercase string from the canonical dataset code.- A reference the schema in scope does not declare fails at save.
Attribute types come from the schemas — JSON Schema with boolean, integer, number and string scalars, array for collections, and nested objects flattened into dot paths. String formats uuid, date, time and date-time refine a string into the matching value type.
Comparisons
| Operator | Meaning | Example |
|---|---|---|
= | equal | user.role = "admin" |
!= | not equal | user.status != "blocked" |
> | greater | data.amount > 99.95 |
>= | greater or equal | request.total >= 100 |
< | less | data.amount < 0 |
<= | less or equal | user.age <= 65 |
- Equality is a single
=. There is no==. - There is no
between— combine two comparisons withand. - A type-mismatched comparison, like
user.name > 5, parses but never evaluates to true. - On data columns, string ordering follows the column's collation.
String predicates
| Predicate | True when the left side |
|---|---|
contains, containsIgnoreCase | contains the right side |
startsWith, startsWithIgnoreCase | starts with the right side |
matches | matches the right side as a regular expression |
matches has no IgnoreCase variant.
request.path startsWith "/admin/"
user.email containsIgnoreCase "@arkveil.com"
user.name matches "^A.*"Presence and collection checks
| Check | True when |
|---|---|
x is null | x has no value |
x is not null | x has a value |
x in ["a", "b"] | x equals one of the elements |
x not in ["a", "b"] | x equals none of the elements |
x is empty | the array x has no elements |
x is not empty | the array x has elements |
x is uniform | all elements of the array x are equal |
x is diverse | the elements of the array x are not all equal |
in and not in compare numbers by value. An integer 2 matches the decimal 2.0 in x in [1, 2.0].
Boolean logic and precedence
and, or and not, with parentheses for grouping.
Precedence from loosest to tightest: or, then and, then not, then comparisons and predicates. not binds looser than a comparison — not user.active = true negates the whole comparison.
A where condition that combines and or or must be wrapped in parentheses. See Iterating arrays.
not (user.suspended = true) and user.role in ["admin", "editor"]Iterating arrays
<quantifier> <collection> [as <alias>] where <condition> tests the elements of an array. Inside the condition, it names the current element.
| Quantifier | True when | On an empty array |
|---|---|---|
any | at least one element matches | false |
exists | at least one element matches | false |
all | every element matches | true |
every | every element matches, and there is at least one | false |
none | no element matches | true |
all and every differ only on the empty array. all grants vacuously — every element of nothing matches. every demands elements. Choose by what an empty array should mean for the grant.
- The collection is an array attribute of
user.,context.,action.orrequest., or an array literal. Notdata.*, notit, not an alias path likeg.members, not a parenthesized expression. - A condition that combines
andororis wrapped in parentheses:any user.tags where (it = "a" or it = "b"). To combine the whole predicate with something outside, parenthesize the predicate instead. A bareandororafter the condition is rejected as ambiguous. - For nested iteration, name the outer element with
asand read it as<alias>.it. Bareitis always the innermost element. - A quantifier over a missing array is null — it does not grant.
any user.permissions where it startsWith "billing:"
none action.tags where it = "blocked"
any user.tags where (it = "vip" or it = "partner")
(any user.tags where it = "vip") or user.isOwner = true
any user.groups as g where any context.allowedGroups where it = g.itDataset references
A permission condition may ask whether a matching row exists in a dataset, looked up at decision time:
exists demo_billing.public.invoice where (data.id = request.invoiceId and data.owner_id = user.id)- The reference is a bare table name, or the full
datasource.schema.tablecode. Two segments are rejected. - A bare name binds when the formula is saved and must match exactly one dataset in the workspace. The binding is pinned — a dataset registered later never changes what a stored policy means. The full code is for when several datasets share a table name.
- Write the reference in canonical lowercase. The text is not normalized — a case variant is rejected.
- The dataset must already be registered when the formula is saved.
- After the
where,data.<column>names the referenced dataset's columns. The outer roots stay available, as the example shows. - The condition after the
whereis flat — no nested dataset references, no quantifiers inside it. - Evaluating a dataset reference requires a connected runtime. Asked of Arkveil Cloud alone, the reference does not grant — the decision stays denied, fail-safe.
The same keyword iterates arrays. What follows exists decides the form: a root like user.… or an array literal is an iteration, a bare name or dotted code is a dataset reference.
Dataset exists is the text form of a wider reference family. FETCH_ONE and FETCH pull a matching row's values into a condition instead of only checking presence. Neither can be authored today — text syntax for them is planned, with the same reference rules.
Evaluation
A formula evaluates to one of three values — true, false or null. There is no null literal to write. The value arises during evaluation: a missing attribute value, a comparison over one, a type mismatch, a dataset reference no runtime can serve.
The logic is three-valued, the same as SQL. Both operators are symmetric:
a | b | a and b | a or b |
|---|---|---|---|
| true | true | true | true |
| true | false | false | true |
| true | null | null | true |
| false | false | false | false |
| false | null | false | null |
| null | null | null | null |
a | not a |
|---|---|
| true | false |
| false | true |
| null | null |
Note that true or null is true — a condition with one satisfied alternative grants even when another alternative cannot be evaluated.
What makes the semantics safe is the last step. Only true grants. A condition contributes only when it evaluates to true, and a filter admits a row only when it holds true for that row. False and null deny alike, and any evaluation failure counts as false. A formula can fail safe. It cannot fail open.
Common mistakes
| Wrong | Right | Why |
|---|---|---|
user.role == "admin" | user.role = "admin" | equality is a single sign |
user.role = 'admin' | user.role = "admin" | strings are double-quoted |
a AND b | a and b | keywords are lowercase |
request.rate > .5 | request.rate > 0.5 | digits on both sides of the dot |
ctx.country | context.country | the root is context. |
user . role | user.role | the dot is part of the root |
user.manager = null | user.manager is null | there is no null literal |
user.role in [] | — | array literals are never empty |
data.owner.id | data.owner_id | data. names one column, never nested |
x matchesIgnoreCase "a" | — | only contains and startsWith have IgnoreCase variants |
any user.tags where it = "vip" or user.isOwner = true | (any user.tags where it = "vip") or user.isOwner = true | a bare or after where is rejected — parenthesize the condition or the predicate |
exists public.invoice where … | exists demo_billing.public.invoice where … | one segment or three, never two |
Validating formulas
arkveil formula parse checks a formula against a chosen slot and returns the parsed tree or the exact error:
arkveil formula parse --context ACTION_PERMISSION --dsl 'user.role = "admin"'The contexts mirror the slot table: ACTION_PERMISSION, ALL_ACTION_PERMISSION, ACTION_TARGET_CONDITION, DATA_TARGET_CONDITION, DATA_TARGET_FILTER. Permission conditions take --request-schema to resolve request.*. A filter's data.* columns are validated against the dataset when the policy is saved.
The same reference as this page ships in the CLI as arkveil formula syntax — offline, no authentication.
Next steps
- Access model — the semantics around the language: targets, permit-only combination, explainable decisions.
- Action policies — permission conditions in context.
- Data policies — data conditions and filters in context.
Runtime
How Arkveil runs in production — a self-hosted sidecar that mirrors the access model, evaluates every decision locally, and keeps answering even if Arkveil Cloud becomes unreachable.
SDK Overview
Enforce decisions from your application — a lightweight, fail-closed SDK with packages for core, Node.js, and NestJS.