COMPASS docs

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