COMPASS docs

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=abgeloest and gilt_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.