> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vainona.ai/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.

export const productName = "Vainona";

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, {productName} 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={null}
{
  "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"}}}
  },
  "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 3 reference indexes a namespace can have, and an attribute used in both directions is one index.
* **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`.

## 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 make a large fan-out visible before it runs.

## 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 1 [judgment unit](/pricing#judgments), at \$0.10 per 1,000 units, and less past 100M units a month.

| 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 |         \$20 |
| **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 |     \$50,500 |
| B, scope 30 days                                                                                                                                               |       3,000,000 |                           8 |           24,000,000 |      \$2,400 |
| B, scope 30 days and `where`                                                                                                                                   |       3,000,000 |                           2 |            6,000,000 |        \$600 |
| **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 |         \$20 |

B costs over \$50,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.

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: \$20, and more than a day at the default share of 2 engine requests, about 2 documents a second. That is above `job_above`, so it waits for you to confirm its estimate. With the 30-day scope and `where` it is about 2,000, which runs on its own in under 20 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`; `job_above` and `confirm_jobs` fall back to the namespace's, and `share` is the namespace's only.

| 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                       |
| **Jobs above a size**                    | `fanout.job_above`, `fanout.confirm_jobs`           | 10,000 judged documents; `true`                | A larger fan-out waits for you to confirm its estimate                                                                                                                              |
| **A daily limit**                        | `fanout.auto_daily_limit`                           | 100,000 judged documents in any 24 hours       | Many fan-outs each under `job_above` cannot add up unseen: past the limit, the next waits for confirm                                                                               |
| **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 {productName}, 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={null}
{"freshness": {"fanout": {"scope": {"created_within": "90d"}, "job_above": 50000}}}
```

A `PATCH` like this one changes those two keys and keeps the others, `scope.where` included. `null` for `job_above` or `confirm_jobs` drops the judgment's own value, so it follows the namespace's again. `GET` returns every value in effect, and `fanout_sources` says where each comes from: `judgment`, `namespace` or `default`. Widening the scope, raising `job_above` or turning `confirm_jobs` off 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.

## Fan-out jobs and the daily limit

A fan-out that would re-judge more than `job_above` judged documents is a `fanout` job, visible under `/jobs` and in the dashboard like a backfill:

```json theme={null}
{
  "id": "job_01J...",
  "type": "fanout",
  "status": "awaiting_confirm",
  "judgment": "needs_review",
  "referenced": {"relation": "product", "document_id": "prod_17", "revision": 88213},
  "estimate": {"documents": 48210, "tokens": 43389000, "judgment_units": 48210, "cost_usd": 4.82, "duration_s": 24105},
  "...": "..."
}
```

* **The estimate** counts the judged documents in scope whose answers are older than the change, and prices them like a backfill. `duration_s` is at the namespace's share of engine requests, about 2 documents a second at the default share, which is why it is hours for a large one.
* **Confirm.** The job waits in `awaiting_confirm` until `POST /jobs/{id}/confirm`, unless `confirm_jobs` is `false`, when it starts at once. Confirming is refused with `budget_exceeded` when the estimate does not fit what is left of the namespace's monthly budget, and a job that would start on its own but does not fit waits instead.
* **One job per referenced document.** Another change while the job waits moves `referenced.revision` to it and refreshes the estimate, so one confirm covers every change so far.
* **Cancel** leaves the answers as they are. The ones in scope read `stale` until the judged document or the referenced document next changes. `pause` and `resume` work as for a backfill.
* **The daily limit.** A fan-out at or below `job_above` runs on its own, with no job, and its spend shows in the namespace's spend. `auto_daily_limit` caps how many judged documents those automatic fan-outs re-judge in any 24 hours. A fan-out that would pass it becomes a `fanout` job that waits for confirm, however small, and even when `confirm_jobs` is `false`. `null` means no limit.
* **Background work.** Fan-out runs behind the namespace's own changes, like a backfill, on at most `share` of its in-flight engine requests (2 of 4 by default). A budget pause stops it like all judging.

## What the answers show

* **In scope:** an answer is `pending` from the change until the fan-out re-judges it, including while its job waits for confirm, and `fresh` again once an answer lands with a watermark at or above the change. If the job is cancelled, it is `stale`.
* **Outside the scope:** the answer keeps its freshness and is not marked `pending`. 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: storage keeps only a referenced document's newest version, so the replay cannot tell which of its past writes 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. Each fan-out above `job_above` shows its own estimate before it runs.

## Namespace fan-out settings

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

```json theme={null}
{"fanout": {"share": 0.5, "job_above": 10000, "confirm_jobs": true}}
```

* **`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.
* **`job_above`** (10,000) and **`confirm_jobs`** (`true`) are the values its judgments follow unless they set their own.

Each key is optional in a `PATCH`, and `GET` returns the values 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 daily limit.
