> ## Documentation Index
> Fetch the complete documentation index at: https://docs.brussle.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Record what happened

> Post labels, upload a CSV, or let outcome rules turn your own writes into outcomes. Each outcome sits beside the exact answer it measures and trains the judgment's correction.

Each outcome you record measures the answer it sits beside, and enough of them train the judgment's [correction](/train/correction). Post labels, upload a CSV, or let rules make outcomes from the writes you already send.

An **outcome** records what happened to a judged document. For example, the judgment `material_change` asks whether an edit to a contract section changed what the parties must do. Your legal team reviews the edit to section `sec_118` and finds that it changed the payment terms. You post `true` as the outcome for `sec_118`.

Outcomes are data. Brussle keeps each one beside the exact answer it measures, which names its document revision, judgment version and engine version. Each outcome counts for the judgment version and engine [epoch](/train/engine-changes) of the answer it joins. There are two kinds:

* **Labelled examples:** a person's answer to the question for a document, as of a time. Use them to measure a judgment before you rely on it. This page shows how.
* **Results that come later:** what happened after the answer, such as "this invoice went overdue". These need a `horizon` on the judgment. See [outcomes that arrive later](/train/horizons).

## Post labelled examples

A labelled example is an outcome on a judgment with the default `horizon` of `0s`. It records what was true of the document at `observed_at`.

1. Write the documents.
2. Post their labels to `POST /namespaces/{ns}/outcomes`.

```json theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{
  "outcomes": [
    {"document_id": "sec_118", "judgment": "material_change", "value": true, "observed_at": "2026-10-01T09:00:00Z"},
    {"document_id": "sec_119", "judgment": "material_change", "value": false, "observed_at": "2026-10-01T09:00:00Z"}
  ]
}
```

<CodeGroup>
  ```ts TypeScript theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  await ns.outcomes.append([
    { document_id: "sec_118", judgment: "material_change", value: true, observed_at: "2026-10-01T09:00:00Z" },
    { document_id: "sec_119", judgment: "material_change", value: false, observed_at: "2026-10-01T09:00:00Z" },
  ]);
  ```

  ```python Python theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  ns.outcomes.append([
      {"document_id": "sec_118", "judgment": "material_change", "value": True, "observed_at": "2026-10-01T09:00:00Z"},
      {"document_id": "sec_119", "judgment": "material_change", "value": False, "observed_at": "2026-10-01T09:00:00Z"},
  ])
  ```
</CodeGroup>

`value` is what was true of the document at `observed_at`:

| Judgment type | `value` |
| - | - |
| `bool` | `true` or `false` |
| `choice` | An option's `value`, such as `"billing"`. `"none_of_the_above"` is allowed. |
| `score` | A level's `value`, as a JSON integer, such as `4` |

How a label joins its answer:

* **It joins the answer for the revision that was current at `observed_at`.** A label that you post before that answer exists still joins it when it arrives.
* **A label observed while its document was deleted joins nothing,** because no revision was current. A document that you write again remembers the last 8 deletions of its `id`, so this holds however late you post the label, as long as the deletion is still recorded when you post or read the label, or when you write the `id` again.
* **An answer keeps one label.** A later label for the same answer replaces the earlier one in the correction, the report and the recommender: the label with the latest `observed_at` counts, and of two with the same `observed_at`, the one you posted last. A document that you label each period without a change counts once. Brussle still stores every label, and the report counts the replaced ones in `unmatched_outcomes.same_window`. A [labelling-queue](/train/thresholds#label-a-random-sample) label is counted apart: an answer also keeps one queue label.
* **Outcomes are append-only, and retries are safe.** The same document, judgment, value and `observed_at` is stored once.
* **Outcomes belong to a namespace.** Post them on the namespace's own path, never on a [template](/guides/templates) prefix.
* **Errors.** A value that does not fit the judgment's type is `invalid_request`. An unknown judgment is `not_found`. A post is at most 2 MB.

To measure a judgment on records you already have, replay them one period at a time and label each period with what happened next. See [backtest on history](/train/backtest).

### Upload labels as a CSV

1. In the dashboard, open the judgment.
2. Go to its **Labels** tab.
3. Upload a CSV with the header `document_id,value`, and optionally an `observed_at` column in RFC 3339:

```text theme={"theme":{"light":"css-variables","dark":"css-variables"}}
document_id,value,observed_at
sec_118,true,2026-10-01T09:00:00Z
sec_119,false,2026-10-01T09:00:00Z
sec_120,true
```

Your browser checks each row against the judgment's type and lists the row errors before it sends anything. It posts valid rows in batches of 1,000. A row without `observed_at` uses the time of the upload.

## Correct an outcome

Post the correct value as of now. If the document has not changed, the correction joins the same answer and replaces the earlier label.

* **A correction must differ.** Its value or its `observed_at` must differ from each earlier outcome of its document. The same value at the same time again is a retry, stored once at its first place. To set an outcome back to an earlier value, use a new `observed_at`.
* **Date it now, not in the past.** The correction and the report keep the label with the latest `observed_at`, and so does a relation that [reads the outcome](#outcomes-that-other-judgments-read). Brussle keeps a correction dated before the outcome it corrects, but a relation never reads it.
* **On a rule's outcome,** a posted outcome always wins in its answer or window. Post one to correct a rule by hand.

## Record outcomes from your own writes

Most outcomes already reach you as ordinary writes: a ticket closes with the team that handled it, your team records which invoice a payment settles, a moderator reverses a removal. **Outcome rules** turn those writes into outcomes, so you get labels without posting them.

A rule watches one path on the judged document. When a write changes the path to the value you name, that write is an outcome. Set rules with `PATCH /namespaces/{ns}/judgments/{name}`, under `outcomes`.

**Triage.** A `choice` judgment routes tickets to a team. When a ticket closes, the team that closed it is the right answer. `from` takes the outcome from the document after the write:

```json theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{"outcomes": {"rules": [
  {"when": {"path": "state.status", "in": ["closed", "resolved"]}, "from": "state.team"}
]}}
```

**Matching.** The [`settles`](/guides/entity-matching#record-the-match-the-reconciliation-shape) judgment chooses which open invoice a payment settles. When your team records the match in `state.settles`, the invoice it names is the right answer. `on` fires when the path gets a value for the first time:

```json theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{"outcomes": {"rules": [{"on": "state.settles", "from": "state.settles"}]}}
```

**Moderation.** A `bool` judgment asks "does this post break the rules?", and its answers cause removals. A reversed removal means the answer was wrong. An upheld removal means it was right:

```json theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{"outcomes": {"rules": [
  {"when": {"path": "attributes.reversed", "becomes": true}, "value": false},
  {"when": {"path": "attributes.upheld", "becomes": true}, "value": true}
]}}
```

### How a rule fires

* **Each rule has `when` or `on`.** `when.path` is `attributes.<name>` or `state.<key>[.<key>...]` on the judged document itself: for a judgment with relations, the payment, not a related document. `becomes` names one value. `in` names up to 16. `on` names one path, and fires when its value changes from missing or `null` to any other value.
* **It fires on the write that changes the value** to that value, or into the list, from any other value. A write that keeps the value fires nothing, so an unrelated edit, a repeated sync or a retried request adds no second outcome.
* **A delete never fires.** A write that creates the document fires if the document has the value when it is created.
* **`value`** is the outcome: `true` or `false` for a `bool`, an option for a `choice`, a level for a `score`. A `bool` takes only `value`.
* **`from`** (`choice` and `score` only) takes the value at that path after the write. If that value is not an option or level, or the path is missing, Brussle keeps the outcome but it counts nowhere: the report counts it in `unmatched_outcomes.does_not_fit`. Use that count to find a rule that points at the wrong field.
* **A value that changes back and forth fires each time it arrives.** A status that goes overdue, then open, then overdue again gives two outcomes. On a judgment with a horizon, two outcomes in one window still count once.

How rule outcomes count:

* **They join as posted outcomes do,** and the report counts them as `rule` in `outcomes_by_source`.
* **A posted outcome wins** when you post one and a rule makes one for the same answer or window.
* **With the default `0s` horizon,** a rule outcome joins the answer to the document as it was immediately before the write that fired the rule. That answer is the one the write responded to, not the answer the write itself causes.
* **Its `observed_at`** is the time the write was committed.

### Rule limits

* **Plans.** Outcome rules are on the Team plan and above. Below Team, a `PATCH` that sets `rules` returns `plan_required`, and one that clears them (`"rules": []`) always works. After a downgrade takes effect, rules keep running, but the correction leaves out what they make from then on: the report counts those outcomes in `rule_outcomes_paused`, and the dashboard's **Calibration** section shows the count. Outcomes collected before stay in use. After an upgrade, new rule outcomes count again. See [plans](/pricing#plans).
* **Up to 4 rules for each judgment,** each on one path. Each `PATCH` replaces the whole `outcomes` object, so send `implicit_negatives` and every rule together.
* **Rules apply from when you set them.** Brussle does not replay earlier writes. To label the past, post outcomes for it.
* **Rules read only the judged document's own writes.** A write to a payment is not an outcome for the invoice it settles. Post that outcome yourself.
* **Templates.** Neither a namespace that gets a judgment from a [template](/guides/templates) nor the template's prefix path can set rules. Set rules on a namespace's own judgment.
* **Errors.** A rule that does not fit the judgment's type, such as `from` on a `bool`, and a fifth rule are `invalid_request`, with the rule and the reason.
* **Features.** A [feature](/train/features) cannot read a path that a rule reads, because that path holds the outcome.

## Post what did not happen too

Most teams record what happened: the payment was refunded, the match was reversed. Few write down what did not happen. The correction needs both: at least 15 of each kind, and the [recommender](/train/thresholds) 20.

* **For labelled examples,** label documents where the answer is "no", with `"value": false`, as well as "yes".
* **For results that come later,** post `false` when an answer's window passed without the event, or turn on [implicit negatives](/train/horizons#turn-on-implicit-negatives) when no outcome really means "no".

## Label more than the reviewed documents

Outcomes usually come from documents that someone already looked at, and people look at the documents that crossed a threshold. Then almost all outcomes sit on answers with a high raw probability, the fit has nothing for the rest of the range, and its corrected numbers for low-probability answers are a guess.

The [report](/train/report) warns about this for each epoch, in `warnings`:

* **`above_thresholds`:** more than 80% of the outcomes are on answers that meet one of the judgment's thresholds.
* **`high_band`:** on a `bool`, more than 80% of the outcomes are on answers with `p` of 0.7 or more.

Its `coverage` gives the lowest and highest raw value the outcomes cover, and the reliability curve shows how many outcomes fall in each band.

**What to do:** label a random sample of documents as well. The [labelling queue](/train/thresholds#label-a-random-sample) draws one for you. Keep labelling the reviewed documents too; both count.

## Outcomes that other judgments read

Outcomes also feed [relations](/concepts/relations#what-happened-to-related-documents). Another judgment can read the newest outcome of a related document as a fact in its context. For example, a judgment on a customer can read whether the customer's past orders were refunded.

* A relation reads the outcome with the latest `observed_at`. See [correct an outcome](#correct-an-outcome).
* **Outcomes posted before relations could read them,** including one posted before this feature existed and one posted for a document that did not exist yet: a relation reads such an outcome for the document's current life only if no deletion of the document is recorded at or after its `observed_at`. A document remembers the last 8 deletions of its `id`, so a later life of a document deleted long ago can read such an outcome.

## Outcomes that join nothing

Brussle keeps an outcome that joins no answer, and it counts if a later answer takes it. The [report](/train/report) counts these in `unmatched_outcomes`, by reason:

| Reason | Means |
| - | - |
| `no_evaluation` | The document has no successful answer from this version. With the default `0s` horizon, the revision current at `observed_at` has no answer. Also an outcome observed while its document was deleted. |
| `before_first_evaluation` | Observed at or before the document's first answer, so no answer came before it. |
| `after_horizon` | With a horizon: an event (`true`) observed after a window closed and before the next answer opened one. A `false` at that time answers the window that closed. |
| `same_window` | With a horizon: a different outcome already labels that window, such as a second event, or a posted outcome that won over a rule outcome. With `0s`: a rule outcome whose answer another outcome already labels, a posted label whose answer has a later posted label, or a queue label whose answer has a later queue label. |
| `after_positive` | With a horizon: observed in a window opened after a `true` for the same document. See [after the event](/train/horizons#after-the-event). |
| `does_not_fit` | The value does not fit this version's type: after a type change, or when a rule's `from` field holds a value that is not an option or level. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.