Set and supersede a rule
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 "X-Compass-Key: $COMPASS_KEY_RW" -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 "X-Compass-Key: $COMPASS_KEY_RW" -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. A way to make such details unrecognisable while keeping the history itself (version, time, chain) does not exist yet. Until then: avoid putting personal details into the wording of rules where you can, and contact the operator of your installation in individual cases.