Arkveil

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. and is the operator — uppercase AND does 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 in or where cannot 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 slotuser.*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

LiteralSyntaxExamples
Stringdouble quotes — \" for a quote, \\ for a backslash"admin", "O\"Brien"
Integerdigits, optionally signed42, -5
Decimaldigits on both sides of the dot, optionally signed99.95, -0.5
Booleanlowercasetrue, false
Arraynon-empty brackets["a", "b"], [1, 2, 3], [-1, 2.5]
  • .5 and 1. do not parse. Write 0.5 and 1.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.

RootRefers toShape declared by
user.who is making the requestthe workspace user schema
context.runtime circumstancesthe workspace context schema
action.the action under decision, action.tags includedthe workspace action schema
request.the parameters of this particular callthe request schema in scope
data.one column of the dataset row in scopethe dataset's data schema
dataset.the identity of the dataset in scopefixed — four paths
  • Paths nest with dots where the schema nests: user.profile.age.
  • data.<column> names exactly one column and never nests — data.a.b is rejected.
  • dataset.* has exactly four paths — dataset.datasource, dataset.schema, dataset.table and dataset.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

OperatorMeaningExample
=equaluser.role = "admin"
!=not equaluser.status != "blocked"
>greaterdata.amount > 99.95
>=greater or equalrequest.total >= 100
<lessdata.amount < 0
<=less or equaluser.age <= 65
  • Equality is a single =. There is no ==.
  • There is no between — combine two comparisons with and.
  • 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

PredicateTrue when the left side
contains, containsIgnoreCasecontains the right side
startsWith, startsWithIgnoreCasestarts with the right side
matchesmatches 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

CheckTrue when
x is nullx has no value
x is not nullx 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 emptythe array x has no elements
x is not emptythe array x has elements
x is uniformall elements of the array x are equal
x is diversethe 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.

QuantifierTrue whenOn an empty array
anyat least one element matchesfalse
existsat least one element matchesfalse
allevery element matchestrue
everyevery element matches, and there is at least onefalse
noneno element matchestrue

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. or request., or an array literal. Not data.*, not it, not an alias path like g.members, not a parenthesized expression.
  • A condition that combines and or or is 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 bare and or or after the condition is rejected as ambiguous.
  • For nested iteration, name the outer element with as and read it as <alias>.it. Bare it is 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.it

Dataset 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.table code. 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 where is 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:

aba and ba or b
truetruetruetrue
truefalsefalsetrue
truenullnulltrue
falsefalsefalsefalse
falsenullfalsenull
nullnullnullnull
anot a
truefalse
falsetrue
nullnull

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

WrongRightWhy
user.role == "admin"user.role = "admin"equality is a single sign
user.role = 'admin'user.role = "admin"strings are double-quoted
a AND ba and bkeywords are lowercase
request.rate > .5request.rate > 0.5digits on both sides of the dot
ctx.countrycontext.countrythe root is context.
user . roleuser.rolethe dot is part of the root
user.manager = nulluser.manager is nullthere is no null literal
user.role in []—array literals are never empty
data.owner.iddata.owner_iddata. 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 = truea 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.

On this page