Set and supersede a rule
Who writes a rule. A rule is set by a human: send the session of a signed-in person
(Authorization: Bearer $COMPASS_SESSION, from POST /auth/anmelden, see the Quickstart);
a key alone gets 403. An agent proposes one (lehre_regel_vorschlagen, or a question with regel_vorschlag) and a
human confirms it. Superseding a rule is writing a rule — it needs the same session.
Write the first rule
A Sache with no Aussage has nothing that holds. Write one with art=regel:
curl -sS -X POST "$COMPASS_URL/aussagen" \
-H "Authorization: Bearer $COMPASS_SESSION" -H "Content-Type: application/json" \
-d '{"sache": "ci-timeout-minuten", "art": "regel",
"text": "CI-Jobs brechen nach 20 Minuten ab.",
"herkunft": "entschieden", "angelegt_von": "your-name"}'
201 with the new id. If a regel already holds for this Sache and
subject, and you didn't send ersetzt, you get 409 with the current
statement in the body — the same information GET /gilt/{sache} would
have given you, so you don't have to make a second call to find out what
you collided with:
{"detail": "es gilt bereits eine Regel für diese Sache",
"geltende": {"id": 41, "text": "CI-Jobs brechen nach 30 Minuten ab.",
"gilt_seit": "2026-08-01T09:00:00Z", "quelle": null}}
Supersede it
Point ersetzt at the id you just saw in the 409 (or from any earlier
GET /gilt/{sache}/GET /sachen/{slug} call):
curl -sS -X POST "$COMPASS_URL/aussagen" \
-H "Authorization: Bearer $COMPASS_SESSION" -H "Content-Type: application/json" \
-d '{"sache": "ci-timeout-minuten", "art": "regel",
"text": "CI-Jobs brechen nach 20 Minuten ab.",
"herkunft": "entschieden", "angelegt_von": "your-name", "ersetzt": 41}'
Inside the same request, the predecessor (41) gets status=abgeloest and
gilt_bis set to the new statement's gilt_seit — one write, two rows
change, nothing is deleted. GET /sachen/ci-timeout-minuten now shows 41
under aussagen.abgeloest and the new id under aussagen.gilt.
This is enforced below the API, too: uq_aussagen_eine_geltende_regel is a
partial unique index on (kunde_slug, sache, subjekt_id) where
art='regel' AND status='gilt'. Even a bug that skipped the API's own
409 check could not insert a second concurrent rule — Postgres would
reject the INSERT outright.
End a rule with no successor
Sometimes a rule should simply stop applying, with nothing replacing it — "we don't do CI timeouts anymore" is not the same statement as "CI now times out after N minutes." For that, don't write a new statement at all; end the current one directly:
curl -sS -X POST "$COMPASS_URL/aussagen/43/beenden" \
-H "X-Compass-Key: $COMPASS_KEY_RW" -H "Content-Type: application/json" \
-d '{"grund": "CI-Timeouts werden pro Job im Workflow selbst gesetzt.", "angelegt_von": "your-name"}'
200, sets status=abgeloest and gilt_bis=now(), and logs an event.
409 if the statement isn't currently gilt — you can't end something
twice, and you can't end something that was already superseded.
Ending a hypothesis, and other statuses
POST /aussagen/{id}/status changes status for hypotheses and for
offen/vorlaeufig statements ({"status": "widerlegt"|"offen"|
"vorlaeufig"|"gilt", "grund": "...", "angelegt_von": "..."}"). It does not
apply to a currently-held regel or entscheidung — use ersetzt or
beenden for those, as above.
Rules over time
To see what held on a past date rather than what holds now:
curl -sS "$COMPASS_URL/gilt-am/ci-timeout-minuten?datum=2026-08-15" \
-H "X-Compass-Key: $COMPASS_KEY_RO"
This returns every statement of any kind that held on that date, with status — useful for reconstructing "what did we believe when this decision was made," independent of what has changed since.
Selection runs on the time window alone, with no status filter: a row that
has since been superseded still appears here instead of quietly vanishing —
history is not deleted, not even at read time. Whether a row still holds
today is said by its status alone. Read it.
Why there is no delete
The most common question about this guide: "Can't I just delete a rule?" No, and that is deliberate.
COMPASS is meant to say what still holds. To do that, it also has to know what held before and when it stopped:
- An agent may already have read the old rule. If it acted on it
yesterday, that can only be traced while the rule is still there, with
status=abgeloestandgilt_bis. A deleted rule leaves an action without a reason. - Decisions rest on rules. A justification that points to a deleted rule points to nothing.
- "What held on 15 August?"
GET /gilt-am/{sache}can only answer that while the history is complete.
That is why the database itself refuses a DELETE on statements. No caller
can get around it, not even through a second path.
What people usually mean by "delete" is end: the rule disappears from
what holds (GET /gilt/{sache} no longer returns it), but stays readable
with its reason and time. If something else should take its place, that is
supersede.
Does the rule get a new number?
Each version is its own statement with its own id. So the id changes with
every change, and that is intended: it names exactly this wording. Anyone who
cited it can follow ersetzt to the current version.
The number your people know a rule by (say R12) belongs to the Sache,
not to the version. Pass it when you create the Sache (POST /sachen with
nummer). It stays put when the rule behind it is superseded.
GET /sachen/{slug} shows it together with the current and the superseded
versions.
What about personal data?
If a rule contains information about a person, and that person asks for it to be erased, "end" is not enough: the wording would stay readable in the history. COMPASS keeps the wording of every version. So keep personal details out of the wording of rules, and for an erasure request in an individual case contact the operator of your installation.