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

# Judge a document with the document it points at

> Judge an order line with its product, and control what re-judging costs when that product changes.

A judgment can read the one document the judged document points at through its own attribute: its **referenced document**. An order line reading its product, a message reading its conversation and a task reading its project all have this shape. When the referenced document changes, Brussle re-judges the documents that point at it. That re-judging is a **fan-out**, and this guide is mostly about keeping it affordable.

For the other direction, where many documents point at the judged one, such as an account and its tickets, see [judge a document with its related documents](/guides/related-documents). [Relations](/concepts/relations) compares the two.

## Define one

The relation's `join` is `{"theirs": "id", "mine": "attributes.<name>"}`: the judged document's `mine` attribute holds the referenced document's `id`. In this example, documents of kind `order_line` each point at a `product` through `attributes.product_id`:

```json theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{
  "name": "needs_review",
  "type": "bool",
  "applies_to": {"attributes.kind": "order_line"},
  "question": "Does this order line need a person to review it?",
  "context": {
    "fields": ["state.title", "state.body"],
    "related": {
      "product": {
        "match": {"attributes.kind": "product"},
        "join": {"theirs": "id", "mine": "attributes.product_id"},
        "fields": ["state.name", "state.status", {"path": "state.score", "bands": [0.3, 0.7], "labels": ["low", "medium", "high"]}]
      }
    }
  },
  "engine": {"name": "jev", "version": "current"},
  "freshness": {
    "policy": "on_change",
    "fanout": {"scope": {"created_within": "30d", "where": {"attributes.status": "open"}}, "rolling_limit": 300000}
  },
  "confirm": true
}
```

* **`applies_to`** limits which documents the judgment judges, answers and bills: here the order lines, not the products.
* **One document.** The relation reads at most one, so it takes neither `last_n` nor `window`: sending either is `invalid_request`. `match`, `fields` and `aggregate` work as for any relation, so `related.product` has at most one record, and `count` is 0 or 1.
* **What it points at.** The judged document's `mine` attribute names the referenced document when it holds a valid document id. When it is missing, is not a string, or names a document that does not exist or does not match `match`, the relation is empty.
* **One snapshot.** The referenced document is read at the same log position as everything else, so the answer's `watermark` also says which version of it the answer read, and the evaluation's `related_documents` names that revision.
* **The reverse lookup** from a referenced document to the judged documents that point at it is a reference index on the judged documents' `mine` attribute. The first version that needs it builds it as a `reference_index` job, listed in the create response's `job_ids`, and the judgment's answers are `unavailable` until the job is done. It counts toward the 8 reference indexes a namespace can have, and an attribute used in several ways is one index. A version whose judged documents the existing index does not cover yet, such as one with another `applies_to`, or one using the attribute the other way round, gets a job that rebuilds it, and its answers wait for that job the same way.
* **Bands.** `{"path": "state.score", "bands": [0.3, 0.7], "labels": ["low", "medium", "high"]}` shows the label of the band a number falls in, not the number: 0.29 is `low`, 0.3 is `medium`, and 0.7 or more is `high`. Bands work in any relation. See [bands](/guides/context-recipes#bands).
* **`confirm`.** Creating a version with `related` on a judgment that runs `on_change` returns a [replay estimate](#what-the-replay-estimate-cannot-count) and creates nothing until you send `confirm: true`. What you confirm includes the [rolling limit](#the-rolling-limit): after that, no fan-out waits for a person.

## Fan-out

A write to a judged document re-judges it, as always. A write to its referenced document re-judges every judged document that points at it and is inside the judgment's **re-judge scope**, but only when the write changes what the relation shows of it. That is the fan-out, and it is what this kind of judgment costs:

> **re-judgments a month = referenced-document changes a month × judged documents per change inside the scope**

A referenced document with many judged documents pointing at it can turn one small edit into hundreds of thousands of evaluations. Every control below exists to shrink one of those two factors, or to cap what they can add up to.

## What a fan-out costs

The worked numbers use one namespace:

* 100,000 referenced documents, each pointed at by 200 judged documents on average: 20M judged documents.
* 4% of the judged documents were created in the last 30 days, so 8 per referenced document, and a quarter of those match the scope's `where` (an open status, say), so 2.
* Each judged document's context and question come to about 900 tokens, so each is a standard [judgment](/pricing#judgments), at \$0.25 per 1,000, and less past 100M judgments in a billing period.

| Case | Changes a month | Judged documents per change | Re-judgments a month | Cost a month |
| - | -: | -: | -: | -: |
| **A. Stable fields, narrow scope.** The relation shows a name and a status, which change about once a month per referenced document; scope 30 days and `where` | 100,000 | 2 | 200,000 | \$50 |
| **B. A volatile number across everything.** The relation shows a raw score rewritten daily; every judged document in scope | 3,000,000 | 200 | 600,000,000 | \$95,000 |
| B, scope 30 days | 3,000,000 | 8 | 24,000,000 | \$6,000 |
| B, scope 30 days and `where` | 3,000,000 | 2 | 6,000,000 | \$1,500 |
| **C. B with bands and the scope.** Bands at 0.3 and 0.7 hide the daily moves; the score crosses a band about once a month | 100,000 | 2 | 200,000 | \$50 |

B costs \$95,000 a month. The scope cuts the judged documents per change a hundredfold, and bands cut the changes thirtyfold, which brings B down to the size of A. Without bands, B with the scope and `where` is 6,000,000 re-judgments a month, twenty times the default rolling limit, so the fan-outs past it would be deferred: raise the limit or add bands.

Debounce is what keeps a much more volatile field bounded. A score rewritten every minute is 43,200 changes a month for each referenced document. The 10-minute fan-out debounce never settles on it, so the 1-hour ceiling fans it out once an hour for as long as it keeps changing, 720 times a month. With bands on top, only a change of band counts: a band change that settles fans out once, and a band that keeps flipping still fans out at least once an hour.

One large referenced document, pointed at by 200,000 judged documents, fans out to all of them on one change: \$50, and hours of background judging. It runs on its own and counts 200,000 against the judgment's rolling limit. With the 30-day scope and `where` it is about 2,000, which takes minutes.

Judged documents outside the scope cost nothing, and a re-judged document whose context did not change is not billed.

## The controls, in order of effect

Every default is a setting you change with a `PATCH` and no new version. The judgment's are in `freshness.fanout`, and `share` is the namespace's.

| Control | Setting | Default | What it saves |
| - | - | - | - |
| **The re-judge scope** | `fanout.scope.created_within`, `fanout.scope.where` | 30 days; no filter | Judged documents created longer before the change, or not matching `where`, keep their answer. `created_within: null` means every judged document; `where: null` removes the filter |
| **Only shown fields count** | always on | – | A write that changes nothing the relation shows, such as a field it leaves out, re-judges nothing and marks nothing `pending` |
| **Bands** | a relation's `fields` | none | A number that moves inside its band changes nothing |
| **The fan-out debounce and its ceiling** | `fanout.debounce_ms`, `fanout.max_wait_ms` | 10 minutes; 6 × the debounce, so 1 hour | A burst of changes to one referenced document costs one fan-out. Changes that never settle fan out at least once per ceiling, for as long as they keep coming |
| **The rolling limit** | `fanout.rolling_limit` | 300,000 judged documents re-judged in any 30 days; `null` for no limit | Fan-outs cannot add up past what you confirmed at create: past it, the next is deferred until there is room |
| **Background work** | namespace `fanout.share` | half the namespace's in-flight engine requests | A fan-out never slows the namespace's ordinary judging |

The fan-out debounce is separate from the judgment's own `debounce_ms`, which still governs the judged documents' own writes: a new judged document is judged within seconds, while changes to its referenced document wait 10 minutes. The scope's age counts from the change, not from now, so one change's scope stays fixed however long its fan-out waits. A document's age is from its `created_at`: when it was first written to Brussle, or the creation time that write gave. [Import](/guides/import-existing-data) judged documents with their original `created_at`, so only the recent ones fall inside the scope; imported without it, every one of them is inside the default scope for 30 days.

```json theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{"freshness": {"fanout": {"scope": {"created_within": "90d"}, "rolling_limit": 6000000}}}
```

A `PATCH` like this one changes those two keys and keeps the others, `scope.where` included. `GET` returns every value in effect, and `fanout_sources` says where each comes from: `judgment` or `default`. Widening the scope or raising the rolling limit needs no confirm and can raise the bill a lot; the namespace budget still caps what is spent.

[Fan-out when a referenced document changes](/guides/freshness-policies#fan-out-when-a-referenced-document-changes) lists each setting with its bounds.

## The rolling limit

`fanout.rolling_limit` is the most judged documents the judgment's fan-outs re-judge in any 30 days: 300,000 by default. You confirm it once, with the judgment, and no fan-out ever waits for a person after that.

* **Within the limit** a fan-out runs on its own, with no job, and its spend shows in the namespace's spend.
* **Past it** the fan-out is deferred. Its change is marked, the answers it would refresh read `stale` with `stale_reason` `limit_reached`, and the events feed records one `judgment.limit_reached` event for the change, with the judged documents it would re-judge in `deferred`:

```json theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{
  "type": "judgment.limit_reached",
  "data": {
    "namespace": "acme/prod",
    "judgment": "needs_review",
    "limit": "rolling_limit",
    "relation": "product",
    "document_id": "prod_17",
    "deferred": 48210,
    "url": "https://api.brussle.com/v1/namespaces/acme%2Fprod/judgments/needs_review"
  },
  "...": "..."
}
```

* **Catching up.** A deferred fan-out runs on its own once there is room: when older re-judgments leave the 30-day window, or at once when a `PATCH` raises `rolling_limit`. Raising it needs no confirm.
* **`null` removes the limit.** Every change then re-judges every in-scope document, with no deferral, and only the namespace's budget caps fan-out. Set it at create, in `freshness.fanout`, or later with a `PATCH`.
* **Background work.** Fan-out runs behind the namespace's own changes, like a backfill, on at most `share` of its in-flight engine requests (half by default). A budget pause stops it like all judging.

Subscribe a [webhook endpoint](/guides/webhooks) to `judgment.*` to hear about a deferral as it happens.

## What the answers show

* **In scope:** an answer is `pending` from the change until the fan-out re-judges it, and `fresh` again once an answer lands with a watermark at or above the change. It is `pending` from the moment the write to the referenced document acks, so no read after the ack sees it `fresh` against the old version. A write that turns out not to change what the relation shows leaves the answer `pending` only until Brussle has checked it, moments later, and then `fresh` again with no engine call. While the fan-out is deferred past the rolling limit, it is `stale` with `stale_reason` `limit_reached`, and the change in its `referenced_changes` has `deferred: true`.
* **Outside the scope:** nothing re-judges the answer, so it reads `stale`, with `stale_reason` `referenced_changed`, and `answers: "fresh_only"` leaves it out. It keeps its numbers until the judged document is judged again. Its `watermark` and its evaluation's `related_documents` say which version of the referenced document it read, and a referenced document whose `revision` is above the watermark shows it read an earlier one. On a get, the answer also lists `referenced_changes`: the newest write to each document it points at that changed what the relation shows, with its `revision` and time. The judged document's next own write re-judges it against the referenced document as it is then. The dashboard's document view says which of these applies.
* **Only `on_change` fans out.** Under `on_read`, `periodic` and `manual`, a change makes the in-scope answers `stale`, as any related write does.
* **`updated_at` changes on every write.** A relation that shows a referenced document's `updated_at` fans out on every write to it, so create warns about it.

## What the replay estimate cannot count

Creating a version with `related` on a judgment that runs `on_change`, or switching such a judgment to `on_change`, returns the [replay estimate](/guides/related-documents#the-replay-estimate) of its monthly cost and does nothing until you send `confirm: true`. It cannot count fan-out: the replay cannot tell which past writes to a referenced document changed what the relation shows. For this kind of judgment it returns `"excludes": ["fanout"]` and `lower_bound: true`, and the dashboard shows the figures as "at least, not counting fan-out". The replay still counts the judged documents' own writes and every relation that reads documents pointing at them.

What fan-out can add is bounded by the rolling limit instead, and the estimate prices it beside `replay`, in `fanout`, at the sample's judgments per answer:

```json theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{"fanout": {"rolling_limit": 300000, "cost_usd_at_limit": 75.0}}
```

`cost_usd_at_limit` is the most fan-out can cost in any 30 days, and it is what you confirm with `confirm: true`. To confirm with no limit, send `"freshness": {"fanout": {"rolling_limit": null}}` with the create: both are then `null`, every change re-judges every in-scope document with no deferral, and only the namespace's budget caps it.

## Namespace fan-out settings

`PATCH /namespaces/{ns}` sets how fan-out runs for every judgment in the namespace:

```json theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{"fanout": {"share": 0.5}}
```

**`share`** is the most of the namespace's in-flight engine requests fan-out may use: 0.5 by default, always at least one request, so the rest serve its ordinary judging. It is above 0 and at most 1. `GET` returns the value in effect. See [namespaces](/concepts/namespaces#settings).

## Not available yet

* Counting fan-out in the replay estimate.
* Limits on a namespace's total fan-out other than its budget and each judgment's rolling limit.


## Related topics

- [Judge a document with its related documents](/guides/related-documents.md)
- [Writing a context recipe](/guides/context-recipes.md)
- [Relations](/concepts/relations.md)
- [Choosing a freshness policy](/guides/freshness-policies.md)
- [Match one record to another](/guides/entity-matching.md)


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