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

# When a new fit replaces the current one

> Brussle fits every night, but your numbers change only when a new fit is clearly better on the outcomes that arrived since. Promote a fit to take it now, or pin one to hold it.

Your numbers do not change night to night for no reason. Brussle fits every night, and a new fit replaces the one your answers read only when it is clearly better on the outcomes that arrived since. When the numbers change, something changed in your data. To take a new fit at once, promote it. To hold one you have audited, pin it.

The fit that answers read is the **current fit**. Each run makes a new fit, the **challenger**.

## When fits run

* **The first run** starts within a few minutes of the first outcome you post, or of the judgment's first [outcome rules](/train/outcomes#record-outcomes-from-your-own-writes). A run makes a fit only for an epoch that meets the [minimums](/train/correction#at-least-15-of-each-kind), so the first run usually makes none.
* **Until there is a fit,** Brussle runs again every hour, for the current epoch and for a new epoch with no fit yet. The first fit comes within the hour after your outcomes meet the minimums.
* **Once there is a fit,** Brussle makes a new fit each night, on all the outcomes so far, rule outcomes included. It replaces the current fit only under the [rule below](#the-rule).
* **When Brussle re-judges answers to correct them,** the next run starts within a few minutes after the correction ends. See [when answers are corrected](/train/correction#when-answers-are-corrected).

### Fit now

To fit at once, for example right after you post the outcomes that meet the minimums, call `POST /namespaces/{ns}/judgments/{name}/calibration/fit` (`fitCalibration`, `fit_calibration`), or select **Fit now** in the judgment's **Calibration** section. The run usually ends within a minute or two. Then read the [report](/train/report).

* You can ask once an hour for each judgment. Within that hour, the call returns `rate_limited` (429), and `Retry-After` gives the seconds until the next call is accepted. [Named runs](/train/fits#fit-on-outcomes-you-choose) share this limit.
* If a run is already waiting or running, that run serves the call.
* It runs on every plan, and it is never billed.

Fit now follows the rule below like a nightly run. To make its fit current regardless, [promote](#promote-the-newest-fit).

## The rule

The challenger replaces the current fit only when both are true:

1. **It is clearly better on the outcomes that arrived since the current fit was made.** The current fit never saw them, so they are a fair test of both. The challenger is scored on each of them without that outcome, and must beat the current fit by more than two standard errors of the mean difference in log loss.
2. **It moves the number by at least a point:** 0.01 on average over all the judgment's outcomes. For a `choice` or `score`, this is the probability that moves between the options.

The test runs only when the outcomes since hold at least 20 of each kind. Otherwise the current fit stays, with the same `fit` id, and nothing is judged again.

* **An outcome arrives** when you post it, not when it was observed. An [implicit negative](/train/horizons#turn-on-implicit-negatives) arrives when its window closes. After the engine changes, a [replayed](/train/engine-changes) outcome arrives when it is replayed. An outcome whose answer was [judged again with changed inputs](/train/correction#when-answers-are-corrected) counts as new too.
* **The current fit is withdrawn** when the engine's raw numbers beat it by the same margin on the outcomes since. Answers then read the night's fit, or the raw numbers if that did not beat them.
* **A new start takes the night's fit at once:** the first fit, a new engine epoch, a new version, and a change from fitting on all outcomes to fitting on labelling-queue labels alone, or back.
* **Below 100 outcomes,** while the current fit or the night's rests on fewer than 100, each night's fit replaces the current one, under the [margin below 100](/train/correction#below-100-outcomes) rather than this test.
* **When a run cannot fit again,** because outcomes were deleted and the epoch fell below the minimums, the current fit stays.
* **It applies to every judgment:** to `calibrated` on a `bool`, a `choice` and a `score`, to `p` of a judgment with [features](/train/features), and to a template tenant's own fit and its own weights, on that tenant's own outcomes. The exception is a [choice among candidates with features](/train/features#features-on-a-choice-among-candidates): each run's fit replaces the current one, as a first fit does.
* **On a steady process, the rule changes the fit by chance about once in three to five years** for each version and epoch. A real change in your outcomes, such as a new base rate, shows in the outcomes since, and the new fit replaces the current one once it is clearly better.

## Read the current fit and the challenger

Each epoch of the [report](/train/report) shows both:

* **`current`**: the fit answers read now. `fit` is its id, or `null` when it did not beat the raw numbers. `run` holds it. `pinned` is `true` for a pinned fit, and `pin_suspended` is `older_preprocessing` when the pin is [suspended](#a-suspended-pin). `fitted_at` is when the run that made it ran; a fit kept since an earlier run keeps that date, so it can be weeks older than the newest run. `since` measures it against the raw numbers on the outcomes that arrived after it was made: `outcomes`, `fallback_log_loss` (the raw numbers'), `log_loss` (the current fit's) and the `standard_error` of the difference. `since` is `null` while those outcomes hold fewer than 20 of each kind, and on the night the fit was made. `current` is `null` while the epoch has no fit of its own.
* **`challenger`**: the newest run's fit when it did not replace the current one; `null` when it did, and when there was no current fit. It has its own `fit`, `run`, `fitted_at`, `outcomes` and `held_out`, and `versus_current`: the challenger against the current fit on the outcomes since. `gain` is the current fit's log loss minus the challenger's, so a positive `gain` favours the challenger. `shift` is how far the challenger would move the number on average over every outcome. `gain` and `standard_error` are `null` with fewer than 20 of each kind since. On a `choice` or `score` with features, `moved_picks` is the share of outcomes whose most probable option would change.

On a template's tenant with a fit of its own, `tenant.current` and `tenant.challenger` say the same of the tenant's own fit, against the pool (`fallback: pooled`). See [per-customer corrections](/train/tenants).

## Why a new fit was held

`challenger.held` says why the current fit stays:

* **`too_few_outcomes_since`**: fewer than 20 of each kind arrived since the current fit was made. Post more outcomes, or wait for them.
* **`not_clearly_better`**: on the outcomes since, the new fit did not beat the current one by more than two standard errors (`versus_current.gain` and `standard_error`). The difference is within noise.
* **`too_small_a_move`**: the new fit is better, but would move the number by less than 0.01 on average (`versus_current.shift`). On a judgment with features, each move re-judges the judgments that read `p`, billed, so a move this small is not worth it.
* **`pinned`**: a pin holds the fit.

The dashboard's list of fits shows the same in its **Held** column, with the new fit on the row under the current one. The lift chart draws what the new fits would score, dotted, beside what answers read. `current.since` tells whether the current fit still beats the raw numbers on outcomes it never saw; when it stops doing so by the same margin, the next run withdraws it.

## Promote the newest fit

To make the newest fit current for every version and epoch now, whether or not it is clearly better, call Fit now with `promote`:

<CodeGroup>
  ```ts TypeScript theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  const answer = await ns.judgments.fitCalibration("material_change", { promote: true });
  if (!("queued_at" in answer)) {
    // An estimate: other judgments read this one. Read `answer.downstream`, then confirm.
    await ns.judgments.fitCalibration("material_change", { promote: true, confirm: true });
  }
  ```

  ```python Python theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  answer = ns.judgments.fit_calibration("material_change", promote=True)
  if "queued_at" not in answer:
      # An estimate: other judgments read this one. Read answer["downstream"], then confirm.
      ns.judgments.fit_calibration("material_change", promote=True, confirm=True)
  ```
</CodeGroup>

* **The run makes its own new fit and applies it.** It fits all the outcomes so far, as a nightly run does, and that fit becomes current for each version and epoch that is not pinned, tenants included. A promote never moves a pin.
* **With readers, confirm first.** On a `bool` with features that other judgments read, a new current fit judges their documents again, billed. Without `confirm`, the call returns the `downstream` estimate (200), made against last night's fits, and queues nothing. With `confirm: true`, the run is queued, and the answer carries the estimate too.
* **A confirmed promote is not held by the hourly limit.** You accepted its cost, so it is queued even within an hour of Fit now. Without readers, a promote is queued at once and counts as Fit now's request for the hour.
* **Templates.** On a namespace that inherits the judgment, a promote is `invalid_request`; promote on the template. Fit now without a body still queues the template's run.

In the dashboard, the challenger's row has **Promote**, **Pin** and **Compare**. With readers, Promote first shows the estimate and a confirm checkbox, and the row reads **promotion queued** until the run lands.

**Promote or pin?** Promote is for "I trust the rule, but I want tonight's fit now". To hold one fit you have audited, pin it: the challenger has an id, and a pin by that id makes it current and holds it.

## Pin a fit

By default, each answer reads the current fit for its version and epoch (`auto`). A **pin** holds one fit you choose for one version and epoch: to hold a fit you checked, or to go back to an earlier one. Name the fit as the [list of fits](/train/fits#list-the-fits) shows it: its `run`, `version`, `engine` and `engine_version`. `fit` is optional; when you give it, Brussle checks it is that run's fit.

```json theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{"calibration": {"pins": [
  {"run": "01JA2J0P2R4T6W8Y0A2C4E6G9B", "version": 3, "engine": "jev", "engine_version": "current+2026-09-24.1", "fit": "fit_9f3a2c7e1b04d6a8"}
]}}
```

<CodeGroup>
  ```ts TypeScript theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  const pins = [{ run: "01JA2J0P2R4T6W8Y0A2C4E6G9B", version: 3, engine: "jev", engine_version: "current+2026-09-24.1", fit: "fit_9f3a2c7e1b04d6a8" }];
  await ns.judgments.update("material_change", { calibration: { pins } });
  ```

  ```python Python theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  pins = [{"run": "01JA2J0P2R4T6W8Y0A2C4E6G9B", "version": 3, "engine": "jev", "engine_version": "current+2026-09-24.1", "fit": "fit_9f3a2c7e1b04d6a8"}]
  ns.judgments.update("material_change", calibration={"pins": pins})
  ```
</CodeGroup>

* **Send `calibration` alone** in its `PATCH /namespaces/{ns}/judgments/{name}`, without other settings, or the request is `invalid_request`.
* **The list replaces all the pins.** Include the pins you want to keep. A pin sent again from the same run stays as it is. Two pins for one version and epoch is `invalid_request`.
* **The pin writes a run** (`cause: pin`) that holds the pinned fit, and every later run holds it too. The response has the judgment, with `calibration: {"mode": "pinned", "pins": [...]}`, and each pin has `pinned_at`. `mode` is `auto` when no pin applies.
* **Answers of that version and epoch read the pinned fit,** also when a later fit would do better. Every other version and epoch stays `auto`. Each run still makes a fit and lists it as the challenger; when it clearly beats the pinned fit on the outcomes since, the report warns `pinned_fit_beaten`.
* **A pin is a setting,** like thresholds. It makes no version, and a new version starts in `auto`.
* **On any judgment without features, a pin changes only `calibrated`.** Readers use the raw numbers, so nothing is judged again or billed, and there is nothing to confirm.

**On a `bool` with features that other judgments read,** a pin also stops the nightly re-judging of readers for that version and epoch. A pin of a fit other than the one `p` reads now changes `p` once, so Brussle judges again the readers whose view of it changes, billed as any re-judge. Confirm it first, as for a [threshold change a reader reads](/guides/roll-up-rules#changing-a-threshold-a-reader-reads):

<CodeGroup>
  ```ts TypeScript theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  const result = await ns.judgments.update("material_change", { calibration: { pins } });
  if ("downstream" in result && !("judgment" in result)) {
    // Nothing changed. Show each reader's cost, then confirm.
    console.log(result.downstream);
    await ns.judgments.update("material_change", { calibration: { pins }, confirm: true });
  }
  ```

  ```python Python theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  result = ns.judgments.update("material_change", calibration={"pins": pins})
  if "downstream" in result and "judgment" not in result:
      # Nothing changed. Show each reader's cost, then confirm.
      print(result["downstream"])
      ns.judgments.update("material_change", calibration={"pins": pins}, confirm=True)
  ```
</CodeGroup>

Without `confirm`, the response is only the [`downstream`](/guides/roll-up-rules#what-it-costs) estimate, and nothing changes. With `confirm: true`, the pin applies and a `resync` job starts; the response has the judgment, `job_id` and `downstream`. The dashboard shows the cost first and asks you to confirm.

**Brussle refuses a pin** with `invalid_request` when the run is not in the judgment's list of fits; the run has no fit for that version and epoch, for example because it did not beat the raw numbers; `fit` is not that run's fit; the fit uses features and was made before Brussle last changed how it keeps features; or the namespace inherits the judgment from a template (set the pin on the template; see [per-customer corrections](/train/tenants#pin-a-fit-for-every-tenant)). If the pins changed between your read and your write, the request is `conflict`: read the judgment and send the list again.

### A pin and a new epoch

A pin never reaches another epoch. After the engine changes by itself, the new epoch starts in `auto`. Until it has its own fit, its answers read the previous epoch's fit as usual, which here is the pinned fit, with `"from_previous_epoch": true` and `"pinned": true`. The [30-day limit](/train/engine-changes#while-the-replay-runs) for `calibrated` applies as for any fit. `p` of a `bool` with features keeps the pinned fit until the new epoch has its own.

### A suspended pin

Sometimes Brussle changes how it keeps features, and a fit with features made before the change cannot read the features after it. At the next run its version and epoch read `auto`: the pin shows `"suspended": "older_preprocessing"`, the answer shows `"pinned": false`, and the report warns `pin_suspended`. Pin a fit made since, or lift the pin. A pin of a fit without features is never suspended.

## Lift a pin

Send the list of pins without it. `{"calibration": {"pins": []}}` lifts them all:

<CodeGroup>
  ```ts TypeScript theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  await ns.judgments.update("material_change", { calibration: { pins: [] } });
  ```

  ```python Python theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  ns.judgments.update("material_change", calibration={"pins": []})
  ```
</CodeGroup>

* **The next run comes at once.** Brussle brings it forward, usually within a few minutes. It does not count toward Fit now's limit, and the list of fits shows it as `cause: nightly`.
* **Until that run ends, answers read the pinned fit** and still show `"pinned": true`. `GET` on the judgment already shows `mode: auto`.
* **A lift keeps the number until a clearly better fit arrives.** The formerly pinned fit stays current, now in `auto`, and that run's fit replaces it only when it is clearly better on the outcomes since the pinned fit was made, usually many by then. To take tonight's fit anyway, [promote](#promote-the-newest-fit). On a `bool` with features, when the fit `p` reads changes, readers are re-judged and billed, with no confirm.

**After the engine changes, lift a pin only when the replay is done.** While the [replay](/train/engine-changes) runs, the new epoch's answers read the pinned fit. Lift it first, and they read the previous epoch's new fit, then the new epoch's own fit when the replay ends: they change twice, and on a `bool` with features each change re-judges the readers. The report's `replay.status` is `done` when it has ended.

## Events when the fit changes

On a judgment with features, `p` is the corrected number and readers act on it. When the fit that `p` reads changes (its first fit, a promotion, a withdrawal, a pin or a lift), Brussle brings up to date the judgments that read it: it judges again the documents whose view of it changed, billed like any re-judge. A night that keeps the current fit re-judges nothing. Then:

* **`judgment.fit_applied`** reports how many documents crossed each of the judgment's thresholds. When `p` goes back to the engine's, the event is **`judgment.fit_withdrawn`**. Both carry `fit`, `run` and `pinned`.
* The event gives each reader's re-judges: `engine_judgments`, which are billed, and `reuses`, re-judges where only a feature the reader keeps moved, which use their engine answer again at no charge.
* The counts come from those re-judges, so the event arrives after they have started.

See [roll-ups](/guides/roll-up-rules#when-a-child-is-calibrated-differently) and the [event types](/guides/events-feed#event-types). On a judgment without features, nothing reads `calibrated`, so a new fit judges nothing again.


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