docs← Back to article

Markdown for LLMs

Coming from OPA / Rego

The source Markdown for this article. Copy it into your assistant or download it as a text file.

Download this articlePlain text ↗
# Coming from OPA / Rego

**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](/comparisons/opa/):
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.

## Concept map

| Rego | Arxo | What 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 := false` | `closure` over a domain (`subject_on_file`) with a snapshot, a `complete_as_of` time and `derive_explicit_negative true` | The 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 body | The 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 default | A contrary rule with head `not admitted(s)` and one `priority` per ground it beats, each with `reason explicit_exception` | Precedence is a declaration with a named reason, not a pattern spread over several rules |
| Undefined: a missing attribute makes the rule not fire | `NEITHER`: not established; `whyNot` names the rule that did not fire | Absence 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 `BOTH` | A conflict is a result you can inspect, not an evaluation error |
| `else` chains | No `else` form; the same refusal is reached through a priority | Branch order carries no meaning of its own |
| `and` / `or` keywords in a body | `and` inside `when`; an alternative is another rule with the same head | The policy revision that rewrites grounds with `and` / `or` leaves no trace in the Arxo package |
| `with` to replace input in `opa test` | The facts of a scenario, written in its `given` block | A variant of a case is another scenario with different facts |

## The vocabulary

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.

```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.

## Grounds and the default

The Rego policy closes the decision with a default:

```text
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.

```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.

## Deny overrides allow

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:

```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:

```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.

## Negation and the unknown

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

```text
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:

```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.

## Two values for one decision

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:

```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.

## What you gain, what you give up

- **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](/comparisons/opa/)
  marks that as inferred from the documentation.

## Next steps

- [OPA / Rego](/comparisons/opa/): the full comparison, the studied
  version and what is still open.
- [Defeasible rules and unless](/constructs/rule-defeasible-unless/),
  [Priority](/constructs/priority/),
  [Negation and truth statuses](/constructs/negation-and-status/): the
  constructs used on this page.
- [Missing and conflicting facts](/language/missing-and-conflicting/):
  `NEITHER`, `BOTH` and `whyNot` in a worked example.
- [Guide](/guide/): getting started with Arxo.