Skip to content
docs
Arxo ↗

Coming from OPA / Rego

For LLMs8 sections

In short: if you write Rego, the grounds of an Arxo policy will look familiar: several bodies for one decision, a default, a negation, a deny that wins over allow. What changes is that each of those is a named construct with its own meaning rather than an idiom. A ground is a defeasible rule; default allow := false becomes a closure over a named register; not stays silent when the fact is unknown; deny-overrides is a contrary rule plus a priority with a stated reason; and “undefined” is an explicit answer, NEITHER, instead of a missing key. This page walks through the admission policy of the OPA comparison: two grounds for admission, an explicit refusal for suspended subjects, a default refusal, and a break-glass exception. The Arxo package quoted here passes law engine check; its scenarios and the Rego side of the experiment have not been run yet, so the page shows how the policy is written, not measured outcomes.

RegoArxoWhat changes for you
Incremental definitions: several bodies for allow (logical OR)Several rule … defeasible with the same head, admitted(s)Each ground is a rule with its own name, so the answer can say which ground supported it
default allow := falseclosure over a domain (subject_on_file) with a snapshot, a complete_as_of time and derive_explicit_negative trueThe negative is derived only for subjects in the domain that have no positive support; grounds are not touched. You name the register you treat as complete and the time it is complete as of
not (negation as failure)not p(x) in a rule bodyThe literal needs p(x) to be established false. If p(x) is unknown, the rule stays silent instead of firing
Deny overriding allow, written by hand with not deny, else and a defaultA contrary rule with head not admitted(s) and one priority per ground it beats, each with reason explicit_exceptionPrecedence is a declaration with a named reason, not a pattern spread over several rules
Undefined: a missing attribute makes the rule not fireNEITHER: not established; whyNot names the rule that did not fireAbsence of a fact, an explicit refusal and a conflict are three different answers
Two values for one complete rule: eval_conflict_error, “complete rules must not produce multiple outputs”Two contrary rules with no priority between them: the conflict is kept in the answer as BOTHA conflict is a result you can inspect, not an evaluation error
else chainsNo else form; the same refusal is reached through a priorityBranch order carries no meaning of its own
and / or keywords in a bodyand inside when; an alternative is another rule with the same headThe policy revision that rewrites grounds with and / or leaves no trace in the Arxo package
with to replace input in opa testThe facts of a scenario, written in its given blockA variant of a case is another scenario with different facts

In Rego, the input document is free-form JSON and a rule reads whatever paths it needs. In Arxo the facts a policy reads are declared first, each with a kind: empirical for observations about the world, institutional for statuses that exist because a rule or a register says so.

Arxo Law
language "law.core" version "0.2";
package opa.admission version "0.1.0";
namespace "urn:law:stand:opa-admission";
entity Subject;
relation subject_on_file(s: Subject) kind institutional;
relation is_editor(s: Subject) kind institutional;
relation is_owner(s: Subject) kind institutional;
relation resource_public(s: Subject) kind empirical;
relation active_flag(s: Subject) kind empirical;
relation suspended(s: Subject) kind institutional;
relation break_glass(s: Subject) kind institutional;
relation admitted(s: Subject) kind institutional;

The case supplies these facts; the JSON cases of the experiment are mapped onto them by a small adapter, and the verdict document (admitted, grounds, refusal reasons, policy revision) has the same shape on both sides.

The Rego policy closes the decision with a default:

Output
default allow := false

In Arxo, each ground is its own defeasible rule, and the default is a closure: a statement that the register of subjects on file is complete as of a given time, so a subject on file with no ground is explicitly not admitted.

Arxo Law
rule EditorAllow defeasible {
for s: Subject;
when is_editor(s);
then admitted(s);
}
rule OwnerPublicAllow defeasible {
for s: Subject;
when is_owner(s) and resource_public(s);
then admitted(s);
}
closure AdmissionDefault {
predicate admitted;
domain subject_on_file;
snapshot "urn:stand:opa-admission:cases:2026-09-30";
complete_as_of @2026-09-30T00:00:00Z;
derive_explicit_negative true;
}

Two differences from default matter in practice. The closure applies only inside its domain: a subject who is not on file and has no ground is NEITHER, not refused. And the closure produces a negative only where no rule supports admitted, so adding a ground never has to be coordinated with the default.

In Rego, allow and deny are ordinary rule names; a refusal that beats every ground is written out with negations, alternatives and the default. In Arxo the refusal is a rule with the opposite head, and its precedence over each ground is declared with a reason:

Arxo Law
rule SuspendedRefusal defeasible {
for s: Subject;
when suspended(s);
then not admitted(s);
}
priority SuspendedOverEditor {
prefer SuspendedRefusal over EditorAllow;
reason explicit_exception;
}
priority SuspendedOverOwner {
prefer SuspendedRefusal over OwnerPublicAllow;
reason explicit_exception;
}

The policy revision that adds a break-glass exception is one more ground. It needs the break_glass fact to fire, so cases without that fact are unaffected by the revision:

Arxo Law
rule BreakGlassAllow defeasible {
for s: Subject;
when break_glass(s);
then admitted(s);
}

The package deliberately declares no priority between BreakGlassAllow and SuspendedRefusal, because no case in the experiment combines them. A subject with both facts would get BOTH: the conflict stays visible until someone decides which rule wins and writes it down.

The OPA documentation illustrates not with a rule that fires when a key is missing:

Output
deny contains "missing email" if not input.email

Under negation as failure, a missing key is enough for not to succeed. In Arxo, a negative literal in a body needs the fact to be established false:

Arxo Law
rule InactiveRefusal defeasible {
for s: Subject;
when subject_on_file(s) and not active_flag(s);
then not admitted(s);
}

If the case says nothing about active_flag, the rule does not fire. To make silence count as “false”, close the predicate over a register, as AdmissionDefault does for admitted; then the negative is derived and the literal is satisfied. Absence becomes “false” only where you have said the list is complete.

In Rego, two complete-rule bodies that produce different values for the same document are an evaluation error. The experiment includes a mutant policy with exactly that defect. Its Arxo counterpart is two rules with contrary heads and no priority:

Arxo Law
rule AllowVersionA defeasible {
for s: Subject;
when subject_on_file(s) and is_editor(s);
then admitted(s);
}
rule AllowVersionB defeasible {
for s: Subject;
when subject_on_file(s) and is_editor(s);
then not admitted(s);
}

Arxo does not stop here: it keeps both supports and answers BOTH. The comparison records this pair as not directly comparable and examines it case by case, rather than counting it as a mismatch: both sides flag a policy that contradicts itself, by different means.

  • Gain: three answers where Rego has one silence. Not established, explicitly refused, and in conflict are separate outcomes, and an explanation names the rule that did not fire.
  • Gain: declared precedence. Which refusal beats which ground, and why, is written once as a priority with a reason.
  • Give up: Rego idioms with no direct form. There is no else chain and no with override; order is expressed by priority, and variants of a case are separate scenarios.
  • Different deployment model. OPA’s delivery machinery (signed bundles, decision logs with replay, the server, the portable module) is outside this comparison and has no counterpart on this page; the comparison is about the policy language and its answers. In the other direction, OPA has no notion of “the policy as of this date” with editions and pinned sources; the OPA comparison marks that as inferred from the documentation.

Documentation for Arxo. Writings — blog.arxo.io.

Anonymous visit counts on stats.arxo.io, no cookies.