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

  1. Must not contain nut allergens
  2. Bake temperature 175–200°C inclusive
  3. Bake time 25–40 minutes inclusive
  4. 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. 1

    A good batch

    Milk and eggs, baked at 180°C for 32 minutes. It passes every clause.

  2. 2

    Nuts in the list

    The allergens are a list, and "nuts" is in it. It should fail the nut check.

  3. 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. 4

    No allergen record

    There's no allergens field 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

plain-rego/baking.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

ergo/baking.yaml

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

    Passes, but no workings

    The result returnstrue, but nothing else. The output doesn't say what was evaluated, so there's nothing to inspect about the decision.

  2. 2

    Fails, but no reason

    It has nuts, and nut_free is false. That's the right answer, but there's no explanation it's because "nuts" was found in the list.

  3. 3

    Passes, but shouldn't

    Its allergens were recorded as the string "nuts" rather than a list. arr[_] finds nothing inside a string, so has never matches, and not turns that into a pass. A batch with nuts is declared nut-free.

  4. 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:

batchcheckvalue readpassedcause
$well_formedcount(checks) = 3, require = "every"truesatisfied
$min_subjectsin-scope batch count = 4truesatisfied
$unique_idsrepeated batch ids = []truesatisfied
001nut_freeallergens = ["milk","eggs"]truesatisfied
001temp_okbake.temp_c = 180truesatisfied
001time_okbake.minutes = 32truesatisfied
002nut_freeallergens = ["nuts","milk"]falsevalue
002temp_okbake.temp_c = 180truesatisfied
002time_okbake.minutes = 32truesatisfied
003nut_freeallergens = "nuts"falseunusable
003temp_okbake.temp_c = 180truesatisfied
003time_okbake.minutes = 32truesatisfied
004nut_freeallergens = nullfalseabsent
004temp_okbake.temp_c = 180truesatisfied
004time_okbake.minutes = 32truesatisfied

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 allergensnut_freeallergens = ["milk","eggs"]not contains(allergens, "nuts")true
Bake temperature 175–200°C inclusivetemp_okbake.temp_c = 180bake.temp_c >= 175 and bake.temp_c <= 200true
Bake time 25–40 minutes inclusivetime_okbake.minutes = 32bake.minutes >= 25 and bake.minutes <= 40true

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.