The bakery example
In this introduction to ergo, we’ll show how to define policy decisions with rego and ergo through the example of a bakery’s allergen requirements (hat tip to Toby Weston for the original inspiration). You’ll learn how to validate cake batches against three compliance rules and produce explanatory reports of decisions. No deep technical expertise or prior experience with rego is required.
We will look at the same policy written two ways, in plain Rego and with ergo, and compare the results. You can follow along. The files are in examples/baking.
The policy
- Must not contain nut allergens
- Bake temperature 175–200°C inclusive
- Bake time 25–40 minutes inclusive
- Compliant only if all clauses pass
The input file
Our input, batches.json describes four batches of cake baking:
{
"batches": [
{
"id": "cake-batch-2026-03-18-001",A good batch
"allergens": ["milk", "eggs"],
"bake": { "minutes": 32, "temp_c": 180 }
},
{
"id": "cake-batch-2026-03-18-002",Nuts in the list
"allergens": ["nuts", "milk"],
"bake": { "minutes": 32, "temp_c": 180 }
},
{
"id": "cake-batch-2026-03-18-003",Nuts as a string
"allergens": "nuts",
"bake": { "minutes": 32, "temp_c": 180 }
},
{
"id": "cake-batch-2026-03-18-004",No allergen record
"bake": { "minutes": 32, "temp_c": 180 }
}
]
}
- 1
A good batch
Milk and eggs, baked at 180°C for 32 minutes. It passes every clause.
- 2
Nuts in the list
The allergens are a list, and
"nuts"is in it. It should fail the nut check. - 3
Nuts as a string
The allergens were recorded as the string
"nuts"instead of a list. It contains nuts, so it should fail too. - 4
No allergen record
There's no
allergensfield at all, so nobody knows whether it has nuts. It shouldn't pass either.
Only batch 001 should pass.
The same policy in rego and ergo
In Rego
package baking
batches[b.id] := {
"compliant": compliant(b),
"nut_free": nut_free(b),
"temp_ok": temp_ok(b),
"time_ok": time_ok(b),
} if {
some b in input.batches
}
default nut_free(_) := false
nut_free(b) if {
not has(b.allergens, "nuts")
}
default temp_ok(_) := false
temp_ok(b) if {
b.bake.temp_c >= 175
b.bake.temp_c <= 200
}
default time_ok(_) := false
time_ok(b) if {
b.bake.minutes >= 25
b.bake.minutes <= 40
}
default compliant(_) := false
compliant(b) if {
nut_free(b)
temp_ok(b)
time_ok(b)
}
has(arr, x) if {
arr[_] == x
}
This is the example’s policy with three changes. It reads every batch in the list instead of a single one, so it can say which batch each result is about. The rules need if, because OPA 1.19 won’t load them without it. And the contains helper is renamed has, because it clashes with a function OPA now has built in.
with ergo
baking:
requirements:
cake_batch:
subject_type: batch
from: [batches]
id: [id]
checks:
nut_free:
description: Must not contain nut allergens
op: excludes
path: [allergens]
value: nuts
temp_ok:
description: Bake temperature 175–200°C inclusive
op: range
path: [bake, temp_c]
min: 175
max: 200
time_ok:
description: Bake time 25–40 minutes inclusive
op: range
path: [bake, minutes]
min: 25
max: 40
ergo/baking.rego, which runs it:
package baking
import data.ergo
import data.workings
report := ergo.report(input, data.baking.requirements)
violations := ergo.violations(report)
workings_table := workings.by_subject(report)
The requirements are plain data, so they can live in YAML. Each clause becomes a check, with the clause’s own words as its description. Clause 4 needs nothing: a requirement passes only when every check passes, unless you say otherwise.
What plain Rego outputs
opa eval -d examples/baking/plain-rego -i examples/baking/batches.json -f pretty 'data.baking.batches'
{
"cake-batch-2026-03-18-001": {passes, but says nothing more
"compliant": true,
"nut_free": true,
"temp_ok": true,
"time_ok": true
},
"cake-batch-2026-03-18-002": {fails, but not why
"compliant": false,
"nut_free": false,
"temp_ok": true,
"time_ok": true
},
"cake-batch-2026-03-18-003": {has nuts, but passes
"compliant": true,
"nut_free": true,
"temp_ok": true,
"time_ok": true
},
"cake-batch-2026-03-18-004": {looks just like 002
"compliant": false,
"nut_free": false,
"temp_ok": true,
"time_ok": true
}
}- 1
Passes, but no workings
The result returns
true, but nothing else. The output doesn't say what was evaluated, so there's nothing to inspect about the decision. - 2
Fails, but no reason
It has nuts, and
nut_freeisfalse. That's the right answer, but there's no explanation it's because"nuts"was found in the list. - 3
Passes, but shouldn't
Its allergens were recorded as the string
"nuts"rather than a list.arr[_]finds nothing inside a string, sohasnever matches, andnotturns that into a pass. A batch with nuts is declared nut-free. - 4
Fails, but input invalid
There's no allergen record at all, so nobody knows whether it has nuts. But its result is exactly the same as batch 002, so a missing record looks like a batch with nuts - although it needs a different fix.
What ergo outputs
opa eval -d ergo.rego -d examples/baking/ergo -i examples/baking/batches.json -f pretty 'data.baking.report'
The report has a row for every check it ran on every batch, with the value it read:
| batch | check | value read | passed | cause |
|---|---|---|---|---|
$well_formed | count(checks) = 3, require = "every" | true | satisfied | |
$min_subjects | in-scope batch count = 4 | true | satisfied | |
$unique_ids | repeated batch ids = [] | true | satisfied | |
| 001 | nut_free | allergens = ["milk","eggs"] | true | satisfied |
| 001 | temp_ok | bake.temp_c = 180 | true | satisfied |
| 001 | time_ok | bake.minutes = 32 | true | satisfied |
| 002 | nut_free | allergens = ["nuts","milk"] | false | value |
| 002 | temp_ok | bake.temp_c = 180 | true | satisfied |
| 002 | time_ok | bake.minutes = 32 | true | satisfied |
| 003 | nut_free | allergens = "nuts" | false | unusable |
| 003 | temp_ok | bake.temp_c = 180 | true | satisfied |
| 003 | time_ok | bake.minutes = 32 | true | satisfied |
| 004 | nut_free | allergens = null | false | absent |
| 004 | temp_ok | bake.temp_c = 180 | true | satisfied |
| 004 | time_ok | bake.minutes = 32 | true | satisfied |
ergo adds the checks that start with $ to every report itself. They make sure the requirement is well formed, that there was at least one batch to check, and that no two batches share an id.
In this example, Batch 003 fails because ergo’s excludes needs a list, and "nuts" isn’t one. All three batches fail, but you can see the different reasons. cause: value means ergo read the value and it didn’t pass (allergens contain nuts), cause: unusable means the value was there but not the kind the check needs, and cause: absent means there was no value at all.
The report also writes each check as an expression, so the table comes straight out of it. ergo/workings.rego builds it from the report, and the policy exposes it as workings_table:
opa eval -d ergo.rego -d examples/baking/ergo -i examples/baking/batches.json -f pretty 'data.baking.workings_table'
Here are the rows for batch 001:
Policy clause (description) | Predicate (check) | Inputs used (inputs) | Evaluated expression (expression) | Result (passed) |
|---|---|---|---|---|
| Must not contain nut allergens | nut_free | allergens = ["milk","eggs"] | not contains(allergens, "nuts") | true |
| Bake temperature 175–200°C inclusive | temp_ok | bake.temp_c = 180 | bake.temp_c >= 175 and bake.temp_c <= 200 | true |
| Bake time 25–40 minutes inclusive | time_ok | bake.minutes = 32 | bake.minutes >= 25 and bake.minutes <= 40 | true |
When you only want to know what to fix, ask for the violations. There’s one for each of the three failing batches. Here’s the one for batch 004:
opa eval -d ergo.rego -d examples/baking/ergo -i examples/baking/batches.json -f pretty 'data.baking.violations'
{
"cause": "absent",
"check": "nut_free",
"description": "Must not contain nut allergens",
"expression": "not contains(allergens, \"nuts\")",
"inputs": [
{
"name": "allergens",
"value": null
}
],
"requirement": "cake_batch",
"subject": {
"id": "cake-batch-2026-03-18-004",
"type": "batch"
}
}
Learn more…
This example showed you how to build to rego policies with ergo to meet two ket requirements in compliance decisions: being able to inspect what was checked, and to explain why it passed or failed. To continue learning, dive into the docs.