Skip to main content
Polar supports several operators used to construct rule bodies. These operators define how expressions are evaluated during rule execution.

Unification (=)

Unification is Polar’s core matching operation. Two values unify if they are equal or if variables can be consistently bound to make them equal. Lists unify if all corresponding elements unify. The unification operator (=) checks if its left and right operands unify; for example, "a" = "a", x = "a", or ["a", "b"] = [x, "b"] where the variable x is either bound to "a" or unbound. Note that in the case of String values, unification is case sensitive, e.g.:
  • "x" does not unify with "X"
  • User{"Alice"} does not unify with User{"alice"}

Conjunction (and)

and requires both conditions in a rule body to be true.
oso_employee(first, last) if
  is_user(first, last) and
  is_employee("Oso", first, last);

Disjunction (or)

or succeeds if either the left or right operand is true. Any or expression can be rewritten as multiple rules with the same head but different bodies.
is_user(first, last) if
  oso_employee(first, last) or
  is_guest(first, last);

# The `or` can be rewritten as a pair of rules:
is_user(first, last) if oso_employee(first, last);
is_user(first, last) if is_guest(first, last);

Negation (not)

not checks that a specific fact does not exist. This would look like below: For example, you might use it to say that a user should only be allowed to perform an action if they are not banned, and that the policy grants them that permission:
allow(user, action, resource) if
  not is_banned(user) and
  has_permission(user, action, resource);
Negation rules:
  • You can negate only a single fact, not a compound expression (like one using and or or), or a fact that refers to another policy rule.
  • Variables inside a negated fact must also appear in a non-negated fact in the same rule. This ensures Polar knows the set of possible values before evaluating the negation.
These restrictions allow Polar to compile negation into efficient NOT EXISTS SQL subqueries. Polar supports a wide range of patterns using negation, including common use cases like:
  • Checking that a user meets all required conditions (e.g., has every required role)
  • Allowing access from a parent resource unless explicitly overridden

List membership (in)

in iterates over a list of strings.
x in ["a", "b", "c"]
x is unified with each list element in sequence. The right operand must be a list of strings. In the following example, the variable x will be bound to "a", "b", and "c", in turn, and then the x = "a" check will evaluate. This expression will only succeed for the first item in the list, "a".
x in ["a", "b", "c"] and x = "a"
x is unified with each list element in sequence. The right operand must be a list of strings. This succeeds only for the first element, "a". The left operand can be a value instead of a variable:
"a" in ["a", "b", "c", "a"]
This succeeds twice, once for the first element and once for the fourth.

Integer Comparisons

<, <=, >, and >= compare integer values. If a fact stores a Unix timestamp:
***polar expires_at(File:foo, 1670280790)
You can write:
expires_after_y2k38(resource) if  
  expires_at(resource, time) and time > 2147483647  

matches Operator


`matches` asserts that a variable is of a specific type.  

```bash
<variable> matches <Type>
Typically used when introducing a new variable, such as when joining two rules.
has_role(user: User, role: String, resource: Resource) if
  group matches Group and
  has_group(user, group) and
  has_role(group, role, resource);
This binds group to a Group type and requires both has_group and has_role to succeed with the same Group value.

Operator Precedence

Polar operators are evaluated from highest precedence to lowest as follows:
  1. in, matches
  2. =, <, <=, >, >=
  3. not
  4. and
  5. or
For example, the following assertion passes:
actor User{}

e(user: User) if
  a(user) or
  b(user) and
  c(user) or
  d(user);

test "parent-child permissions" {
  setup {
    a(User{"alice"});
  }

  assert e(User{"alice"});
}
Oso evaluates the conditions as follows:
actor User{}

e(user: User) if
  a(user) or # <-- true
  b(user) and c(user) or # <-- false
  d(user);            # <-- false

test "parent-child permissions" {
  setup {
    a(User{"alice"});
  }

  assert e(User{"alice"});
}
That is, b(user) and c(user) is evaluated first. It returns false, but because a(user) is true, the entire condition evaluates to true or false or false, which returns true. Use () to explicitly control evaluation order. For instance, in the example above, if you instead wanted to state that e is true only if at least one of a and b is true, and at least one of c and d is true, you could rewrite the rule as follows:
e(user: User) if  
  (a(user) or b(user))  # <-- true  
  and  
  (c(user) or d(user)); # <-- false  
This evaluates the or expressions before applying and.