Skip to main content
You can put the numbers you already keep into the correction beside the engine’s answer (a plan’s seat count, an invoice amount, how long ago a contract was signed, how many invoices are overdue, what the engine said last time). The fit weighs them against your outcomes, so numbers the engine does not weigh can move the probability and can reorder your documents. Since the engine never reads a feature, a feature costs no engine call. Like any correction, a fit with features is applied only when it beats the engine’s own numbers on your held-out outcomes. A bool, a choice and a score can all have features, and a judgment does not need relations to have them.

Add features

Name the features in features. The renewal_risk judgment reads an account’s notes and keeps three features (the account’s seats, the days since it signed, and the engine’s previous answer).
  • state.seats is the number on the account itself.
  • age(attributes.signed_at) is the days since signed_at, measured from the account’s own updated_at at the revision Brussle judged.
  • previous.p is the engine’s own p the last time it judged the account, and is missing for the first answer.
Each answer and its evaluation show the feature values in features. When a write changes only a feature (such as state.seats), Brussle records the answer again with no engine call and no charge. A judgment with an age feature does the same for a write of the same content, because the age moved. Once you have 100 outcomes with 20 of each kind, the fit can weigh the features with the engine’s answer. A feature change is a version change, so adding, removing or renaming a feature makes a new version with its own fit. Once you activate the new version, the earlier version’s answers read stale. The new version starts with the engine’s p until its own outcomes are enough.

What a feature can read

A judgment keeps up to 12 features, from any of these sources: Ages. An age is a number of days, with a fraction (36 hours is 1.5).
  • Brussle measures each age from the judged document’s own updated_at at the revision it judged, never from a related document’s write or the clock. So a write to a related document never moves an age, while a new revision of the judged document does, even with the same content.
  • The time must be an RFC 3339 string, such as "2026-10-01T09:30:00Z". A number (seconds since 1970, say) or any other text reads as missing.
  • A time later than the judged document’s updated_at gives a negative age.
  • Brussle refuses age(updated_at), since at the judged revision it is always 0. Name the time you mean instead, such as age(attributes.approved_at).
previous.p is the engine’s p from the most recent time it judged the document, with the same engine version, under a version of the judgment that asks the same way.
  • It is missing for the first answer, after the document is deleted and created again, after the engine version changes, and on a composite, which has no single p.
  • It is the engine’s p, never the corrected one.
  • It stays the same when only a feature moves (see reuses).
  • Outcomes replayed after the engine changes read it as missing, because their earlier answers came from the other engine.
What Brussle refuses. Brussle refuses a feature with invalid_request when its path and a path an outcome rule reads are the same, or one is inside the other (for example state.refund.amount and a rule on state.refund), because such a feature would read the outcome itself. Brussle checks this against every version of the judgment, both when you create a version and when you set rules. It does not check a rule’s path on a related document.

Ask about what will happen

The engine answers well about what a document shows now, but near chance about what will happen. So for a question about what will happen:
  1. Ask a narrow question about what the document shows now.
  2. Keep counts from related documents as features, with "render": false so the engine does not read them.
  3. Set a horizon, and record what happened as outcomes.
  4. Let the fit on your outcomes weigh the engine’s answer and the counts.
The goes_overdue judgment asks what the customer’s messages say about an invoice, and keeps one count (the customer’s invoices paid late in the last year). Its outcome records whether the invoice went overdue within 30 days.
Measure what the engine adds. Each epoch’s held_out_metrics in the report scores features (the features and the engine’s answer together) and features_only (the features alone) on the same held-out outcomes. If features_only scores as well as features, the engine adds nothing your counts do not already carry, and a feature threshold on the counts is enough.

How the fit with features works

  • One probability. The answer has no calibrated object, because p is the selected correction, applied when the answer is read. raw.p is the engine’s own answer, whose meaning never changes. fit says which correction p used, with id, interval, run, pinned, fitted_at, outcomes, method and from_previous_epoch, and is null while p is the engine’s. method is features when the fit uses the features, and early or full when it corrects only the engine’s p. fit.interval is the 95% interval of p at this answer’s features.
  • Chosen like any fit. Each run makes a fit that uses the features beside the fit on the engine’s answer alone, under the same minimums. Brussle chooses the fit with features only when it beats both the engine’s p and the other fit on outcomes it did not use, and a fit is never tested on a document whose other outcomes it saw. If neither fit beats the engine, p stays the engine’s. Below 100 outcomes Brussle reports the fit and never applies it (readers_need_full).
  • Labels must cover the range. A fit with features made on labels from one part of the range, such as the documents a review queue sent you, learns that part: inside it the engine’s answers barely differ, so the fit leans on the features and gives the engine’s answer almost no weight, and applied to the rest of the range it is wrong exactly where a threshold acts. When the labels leave more than 20% of the live answers outside their range, no correction is applied (truncated_sample): answers read the engine’s number until the labels cover the range, because below the labels even the correction on the engine’s answer alone is a guess. Label a random sample, or use the labelling queue. On skewed traffic the queue’s labels count for fewer than there are, and a fit with features needs more of them than a fit on the engine’s answer alone: see what the even half costs.
  • All readers use p. Thresholds, filters, ranking, subscriptions and the judgments that read it use the corrected p (on a choice or score, the corrected numbers). A threshold on a feature still reads the stored feature, though. Groups cannot count a judgment with features, and the evaluation log and the export keep the engine’s p and the features.
  • Automate on the bound. Queries and subscriptions can filter and rank on answers.<j>.interval.lower and .upper (see automate on the bound).
  • Nothing is rewritten. When the correction changes, each read uses the new one at once, and Brussle brings the judgments that read this one up to date, billed, without a confirm. That happens only when the fit p reads changes. A pin or a promote is confirmed first. See events when the fit changes and roll-ups.
  • Your thresholds on p. You chose each threshold against the engine’s p, so once p is corrected the same threshold selects different documents. GET on the judgment shows a fit_changed warning until you set a threshold again, and the threshold recommender works from the corrected p.
  • Templates. Each tenant reads the template’s pooled correction until it has enough outcomes of its own (see per-customer corrections).

Read each feature

Each epoch of the report gives the fit with features in held_out_metrics.features, and reads each feature on its own on the same held-out outcomes.
  • engine_weight: how much the fit weighs the engine’s answer. It is never negative, so for two answers with the same features, a higher raw p never reads lower.
  • per_feature lists its entries in the order of the definition.
  • weight: how strongly the feature moves p, on one scale for all features, so you can compare features in different units. A positive weight raises p, a negative one lowers it, and a weight near 0 means little importance. In the example, more seats make a cancellation less likely, and older accounts make it a little more likely.
  • center and scale: the feature’s average and standard deviation over the outcomes the fit used. A missing value counts as center.
  • missing: the share of those outcomes whose answer had no value, so a feature that is often missing can say little.
  • without.log_loss: the held-out log loss of the fit made again without this feature. In the example, the loss goes from 0.31 to 0.37 without seats.
  • alone.log_loss: the held-out log loss of a fit on this feature alone, without the engine’s answer.
  • warning: reads_like_the_outcome, or absent.
The judgment’s Calibration section shows the same list under With features, largest weight first.

A feature that reads like the outcome

A number written once the outcome is known (a refund amount, a cancellation date) predicts the outcome almost perfectly. A fit on it looks very good on your past outcomes. But the number is not there yet when an answer is needed, so the fit is worth nothing. Brussle cannot know when you write a field, but it can see the result. When a feature alone scores below a tenth of the log loss of always answering base_rate, its entry has "warning": "reads_like_the_outcome", the epoch’s warnings carry reads_like_the_outcome naming it, and the Calibration section shows reads like the outcome. The engine’s own answer gets the same test, on held_out_metrics.raw. An engine that reads a document changed to show the outcome, such as a note that the customer gave notice written before the document was judged again, predicts the outcome as perfectly as a leaked feature, and the warning names engine.
  1. Check when your system writes that field, and when it changes the document the engine reads.
  2. If a feature is written once the outcome is known, post a new version without it.
  3. If the document changes once the outcome is known, date each labelled example when the answer was acted on, or use an outcome rule. See post labelled examples.
The warning is about your data and on its own never changes which fit answers read, because Brussle chooses the fit on held-out outcomes only. One case is held: when what reads like the outcome is also what the labels measured, because more than a tenth of them joined an answer made after the document already had one, the fit is not applied, and the report says labels_read_like_the_outcome. See why an epoch has no correction.

Drop a feature that adds nothing

A feature whose without is not higher than the fit’s own log_loss adds nothing the engine and the other features do not already give. A weight near 0, with without equal to log_loss, marks a candidate. Drop it in a new version to keep the budget of 12 for numbers that matter. Compare the new version’s report with the old one before you activate it, or run a shadow report.

Features on a choice or a score

A choice and a score take features as a bool does (the same sources, the same limit of 12, the same rules), and their fit weighs the features beside the engine’s whole distribution.
  • The answer’s numbers are the corrected ones: dist, escape_p and value on a choice, and dist and score on a score. raw holds the engine’s numbers ({dist, value, escape_p} or {dist, score}). There is no calibrated object, and fit names the correction as on a bool.
  • The pick can change. value is the most probable option of the corrected dist, so with features the fit can make another option, or none_of_the_above, the most probable. Without features the order among your options never changes, since the fit only moves answers toward or away from none_of_the_above.
  • Every reader uses the corrected numbers: thresholds on dist, filters and ranking on escape_p, score, value and dist.<option>, subscriptions, the judgments that read it and links. A new fit that changes a pick moves a link to the new document and brings its readers up to date, billed.
  • fit.interval is on escape_p for a choice and on score for a score. Filter and rank on its bounds.
  • Chosen like any fit: only when it beats the engine’s numbers and the fit without features on outcomes it did not use.
  • In the report, held_out_metrics scores the whole distribution, with accuracy (and on a score, mean_level_distance). On a fixed choice, per_feature[].by_option gives each feature’s weight on each option, and moved_picks the share of picks the fit changes.

Features on a choice among candidates

A choice among candidates (options.from) can keep two kinds of feature:
  • Numbers about the judged document, such as candidates.count or the payment’s state.amount, help decide whether a match exists (1 - escape_p).
  • Numbers about each candidate, <from>.each(<path>), help decide which candidate it is (for example candidates.each(state.open_amount) or candidates.each(age(created_at))). <path> is the candidate’s state.<path> or attributes.<name>, or age(created_at), age(updated_at) or age(<state or attributes path>) of the candidate, as of the judged document’s revision. The answer keeps the numbers of each candidate the engine read under candidate_features, by candidate id, and they count toward the 12.
How they act:
  • A candidate the engine gave no probability stays at none, since the fit only weighs the candidates on the engine’s shortlist.
  • An answer with no candidates stays none_of_the_above, with escape_p 1, whatever the fit.
  • Without numbers about candidates, the engine’s order among them is kept. With them, a candidate your numbers favour can rise above one they do not at equal engine confidence (the invoice with nothing paid against it can rise above one already settled).
  • Never a worse pick. Brussle uses the fit only when its pick is right at least as often as the engine’s on outcomes it did not use. A pick is right when it names the true match, or the escape where there was no match. When numbers about candidates would make the fit pick worse, Brussle fits again without them and keeps the engine’s order.
  • A candidate’s answers cannot be a feature. Brussle refuses candidates.each(answers.<judgment>.<part>) with invalid_request, because through a link it would read the matcher’s own pick back into its next answer. Keep the number on the candidate’s state and name that instead.
  • Not under the promotion rule yet, so each run’s fit replaces the current one.
The report’s held_out_metrics reads whether a match exists for the four readings, with the ranking, and held_out_metrics.candidates gives the rest:
  • joint: the held-out log loss of the whole answer (the escape and every candidate), raw, with the fit without features, and with the fit with features. Brussle chooses on it.
  • selection: how often the pick was right, raw (raw_accuracy) and corrected (fitted_accuracy), over every outcome, so a fit that rightly says there is no match counts as picking right. guard.state is fitted, kept_engine_order (with both accuracies before the fit was made again), or too_few_matched (fewer than 20 outcomes name a match among two or more candidates, so the engine’s order holds). accuracy_holds is true when the fit picks at least as well.
  • moved_picks: the share of the outcomes the fit used whose pick it changes. Brussle sets no condition on it; it is there for you to read.