> ## 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 its related documents

> Judge an account with its tickets and invoices, a user with their posts or a conversation with its messages, and keep the answer current as any of them change.

A judgment usually reads one document. It can also read the documents that point at the one it judges: an account with its tickets and invoices, a user with their posts, a conversation with its messages. The answer stays current as any of those documents change, and each evaluation records exactly which ones it read.

Without it, you copy the related records into the judged document yourself and rewrite it whenever one of them changes. With it, Brussle tracks which judged documents each write touches, waits for bursts of writes to settle, and judges each one once per burst.

This guide covers documents that point at the judged one. A judgment can also read the one document the judged document points at, such as the product an order line names; see [judge a document with the document it points at](/guides/referenced-document). [Relations](/concepts/relations) compares the two.

## Start with the simplest context

Before you add related documents, decide what the question depends on. Try these in order:

1. **A few recent records, as raw text.** Use this when the answer is in what was said: an angry reply, a mention of a competitor, a question nobody answered. Keep `last_n` small, around 5 to 10, and show only the fields that carry meaning.
2. **Aggregates.** Use these when the answer depends on how much or how often: how many tickets, the total of overdue invoices, the status of the latest one. An aggregate costs a few tokens where raw text costs hundreds.
3. **Both, as two relations on the same documents.** Show the last few as text and count the last 90 days.

Nothing else is needed for most questions. As you choose:

* **Which works depends on the question.** Text, counts or both can do best, so try both against your outcomes.
* **A few recent records usually carry most of the signal.** Newest first with a small `last_n` is the right default.
* **Adding counts beside text rarely hurts.** A [composite judgment](/guides/composite-judgments) can take a relation's aggregates as features beside a judgment's answer.

Running a judgment on each related record only to feed the parent multiplies the cost, because every child judgment is billed; measure it against your outcomes before relying on it (see [measure, improve and tune](/guides/measure-improve-tune)). When you already run a judgment on the related records for its own sake, a relation can read its answers: see [roll up answers from related documents](/guides/roll-ups).

## Define one

Related documents point at the judged one through an ordinary attribute. In this example the judged documents are accounts, and each ticket and invoice carries `attributes.account_id`. The judgment is an ordinary judgment with `related` in its context recipe, usually with `applies_to` so it judges only accounts:

```json theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{
  "name": "churn_risk",
  "type": "bool",
  "applies_to": {"attributes.kind": "account"},
  "question": "Is this account at risk of cancelling in the next 60 days?",
  "context": {
    "fields": ["state.name", "attributes.plan", "state.renewal_date"],
    "related": {
      "tickets": {
        "match": {"attributes.kind": "ticket"},
        "join": {"theirs": "attributes.account_id", "mine": "id"},
        "last_n": 8,
        "fields": ["created_at", "state.subject", "state.status"]
      },
      "invoices": {
        "match": {"attributes.kind": "invoice"},
        "join": {"theirs": "attributes.account_id", "mine": "id"},
        "window": "180d",
        "aggregate": {"count": true, "sum": ["state.amount"], "latest": ["state.status"]}
      }
    }
  },
  "engine": {"name": "jev", "version": "current"},
  "freshness": {"policy": "on_change", "debounce_ms": 600000, "max_wait_ms": 3600000},
  "horizon": "60d"
}
```

The same shape fits a user and their posts (`match` on posts, `theirs: "attributes.user_id"`) or a conversation and its messages (`theirs: "attributes.conversation_id"`).

* **`applies_to`** limits which documents the judgment judges, answers and bills. Other documents have no answer for it: `answers` leaves it out, and filters treat it as missing.
* **`match`** picks which documents a relation reads, and **`join`** says how they point at the judged one: a document belongs to the judged document whose `id` equals its `theirs` attribute.
* **`last_n`**, **`window`** or both bound each relation, so a context cannot grow without limit. A relation reads at most the newest 1,000 documents either way.
* **`fields`** are the paths shown from each document, and **`aggregate`** adds numbers over the same documents. Each relation needs one or both.

The [context recipe guide](/guides/context-recipes#related-documents) describes every key, how the context is rendered, and the limits.

<CodeGroup>
  ```ts TypeScript theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  // churnRisk is the definition above: const churnRisk = { ... } satisfies CreateJudgmentRequest;
  const result = await ns.judgments.create({ ...churnRisk });
  if ("replay" in result) {
    // Nothing was created. Show the estimate, then create again with confirm: true.
    console.log(result.replay);
    await ns.judgments.create({ ...churnRisk, confirm: true });
  }
  ```

  ```python Python theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  # churn_risk is the definition above: churn_risk: CreateJudgmentFromDefinitionBool = {...}
  result = ns.judgments.create(**churn_risk)
  if "replay" in result:
      # Nothing was created. Show the estimate, then create again with confirm.
      print(result["replay"])
      churn_risk["confirm"] = True
      ns.judgments.create(**churn_risk)
  ```
</CodeGroup>

The first version that joins on a new attribute builds a reference index for it, as a `reference_index` job, and the create response lists it in `job_ids`, one job per new attribute. Each job's `attribute` names the attribute it indexes. A version that joins on an attribute that already has an index, but over documents the index does not cover yet (another `match`, or the same attribute used the other way round), gets a job for it too: the index is rebuilt to cover them, and still counts as one. A version that joins the same way as an existing one gets no job. The judgment answers once the job is done. Until then its answers are `unavailable`, except that under `on_change` a document written since the judgment was created reads `pending`: its evaluation waits for the index. A namespace has at most 8 reference indexes, one per attribute its relations join on, in either direction, blocking keys included. Reuse an attribute when you can: a create that would need a ninth is refused, and the error names the eight you have.

## Newest created first, and what an edit costs

A relation orders its documents by `created_at`, newest first. `window` keeps those created within the window, and `last_n` then keeps the newest n. So "the last 8 tickets" means the 8 most recently created. The window counts back from the later of the judged document's own newest write and its newest related document's creation, not from the clock. Editing a related document moves neither the list nor the window.

`created_at` is when a document was first written to Brussle, unless the write that created it gave its own `created_at`. When you [import existing data](/guides/import-existing-data), send each record's original creation time as `created_at`, so relations read your history in the order it happened, and a `window` counts it from when it happened.

A write to a related document always marks the judged document it points at as touched, but it only costs an evaluation when it changes what the engine would see:

* **A new related document** changes the list, so the judged document is judged again, once its writes settle.
* **An edit to one of the last 8, in a field the relation shows,** changes the context, so it is judged again.
* **An edit to an older one, or to a field no relation shows,** leaves the compiled context exactly as it was. The answer is kept, and nothing is billed.

The tradeoff is that activity on an old related document goes unseen. If that matters, record it as a new document, such as an event, rather than an edit to the old one.

Aggregates count the same selection, so they follow the same rule. To show a few documents but count many, use two relations on the same `match`: one with a small `last_n` and `fields`, one with a `window` and `aggregate`.

## Debounce and its ceiling

A judged document with busy related documents, such as an account whose tickets keep arriving, would be judged on every write without a debounce. `debounce_ms` makes Brussle wait until its writes have been quiet that long, so a burst of 100 tickets in five minutes costs one evaluation.

A debounce alone never ends for a judged document that gets a new related document every few minutes. The ceiling, `max_wait_ms`, judges it anyway once that long has passed since its oldest unjudged write. With a 10-minute debounce and a 1-hour ceiling, a ticket every minute for 3 hours costs 3 evaluations, not 180 and not 0.

* `max_wait_ms` defaults to 12 × `debounce_ms` for a judgment with related documents and a `debounce_ms` above 0. With no debounce, and for a judgment of a single document, it defaults to no ceiling.
* It must be at least `debounce_ms`, and `null` turns it off.
* Like the debounce, it is a setting: change it with a `PATCH` and no new version. `GET` returns the value in effect.

Answers therefore lag their related documents by the debounce, typically minutes, and by at most the ceiling. That is the price of judging an account once per burst of tickets. A question about a single event, such as whether one payment is fraudulent, should stay a judgment of that document.

## The replay estimate

The cost of a judgment with related documents depends on how often those documents change, not on how many judged documents you have. So before one runs `on_change`, Brussle replays your namespace's last 30 days of writes through its relations, debounce and ceiling, and tells you what it would have cost. Two requests return this estimate and create nothing until you send `confirm: true`: creating a version with `related` on a judgment that runs `on_change`, and switching such a judgment to `on_change`.

```json theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{"replay": {"replayed_days": 30, "entities": 1000000, "judgments_per_month": 8000000, "judgment_units_per_month": 8000000, "cost_usd_per_month": 2000.0, "bulk_pool_share": 0.22, "lower_bound": false}}
```

* **`entities`** is how many documents the judgment applies to now.
* **`judgments_per_month`** is how many evaluations the replay counted, and **`judgment_units_per_month`** the judgments they would bill, each counted by its [size class](/pricing#judgments), from the compiled context and question of up to 1,000 of your judged documents. Here each is standard, so the two are the same.
* **`cost_usd_per_month`** prices those judgments at the tiers your organization would be in, counting what it has already used this billing period.
* **`bulk_pool_share`** is the share of the background judging rate available to the judgment that those evaluations would use. Above 1, the judgment cannot keep up with your writes: raise the debounce or the ceiling, or narrow the relations.
* **`replayed_days`** is below 30 in a younger namespace, and the monthly figures are scaled up from it. A document counts as created at its `created_at`, so records [imported](/guides/import-existing-data) with their original creation time are left out when they are older than the days replayed. Imported without it, they count as created during the import, and the replay describes the import rather than your ongoing writes.

**"At least."** The replay cannot see every edit to a related document, only its creation and its latest write. For documents written once, such as events, messages and invoices, it is exact. For often-edited documents the figures are a lower bound: `lower_bound` is `true`, and the dashboard shows them as "at least". The replay also does not count the evaluations that unchanged contexts save, which only lower the bill.

The namespace budget still caps real spend. A confirmed request whose `cost_usd_per_month` is more than the namespace's budget is refused with `budget_exceeded`, and the dashboard says so before you confirm.

## What each answer and evaluation shows

An answer of a judgment with related documents carries a `watermark`: the position in the namespace's log that its context was read at. Every write at or below it, to the judged document or to any document that points at it, is reflected in the answer.

* A write to a related document makes the judged document it points at `pending` at the next read. It is `fresh` again once an answer lands with a watermark at or above that write.
* `revision` still names the judged document's own revision.
* `wait_for` and `wait_ms` wait for the written or read document's own answer. Writing a ticket with `wait_for: ["churn_risk"]` returns at once, because `churn_risk` does not apply to tickets.

Each evaluation also lists `related_documents`: every related document its context read, rendered or aggregated, with the relation and the revision it was read at. It comes back with `include=history,context` and from `GET /namespaces/{ns}/evaluations/{id}`. That list is what makes an audit exact after the documents have changed, and what an [outcome](/guides/measure-improve-tune) with a `horizon` joins to: "this account churned" labels the evaluation whose 60-day prediction window it falls in, and that evaluation names the tickets it read. The dashboard's evaluation page shows them grouped by relation.

## When the context is too long

A judgment's compiled context, the judged document's fields plus its related documents, is capped at the recipe's `max_tokens` or the engine's limit, whichever is lower. For Jev `current` that is 32,000 tokens for the context plus the longest question; see [limits](/limits). It rarely comes to that, because each relation is bounded by its `last_n` or `window`, and reads at most the newest 1,000 documents.

When a context is still over the cap, it is cut in this order:

1. **The oldest records of the last relation**, then those of the relation before it. Relations are cut in the order of their names.
2. **Then any [`previous`](/guides/change-detection) entries**, the last first.
3. **Then the judged document's own fields**, from the end of the last field.
4. **Aggregates are never cut.** They are computed over the full selection before anything is cut, so a count or a sum still covers every record in the window even when only the newest few fit as text.

Recent records carry most of the signal, so they are the last to go. The evaluation records `context_truncated: true` and `context_tokens`, and its `context` shows the exact text the engine saw. You are billed for the context after cutting. If a judgment is often truncated, lower `last_n`, show fewer or shorter fields, or move volume into aggregates. The full rules are in [context recipes](/guides/context-recipes#related-documents).

## Keep it affordable

A few habits keep the bill small, and most of them also make the answers better:

* **Keep `last_n` small.** The last 5 to 10 records usually suffice and keep the judgment standard; a year of history can make it large or extra-large, which count as 4 and 16.
* **Prefer aggregates when volume is the signal.** A count is a few tokens.
* **Show only the fields that matter.** Leave long bodies out of a relation unless the question depends on them.
* **Share recipes.** Judgments with the same context recipe, such as `churn_risk` and `expansion` on the same relations, are answered together. Each is still billed, but together they are lighter on rate limits, so `bulk_pool_share` stays low.
* **Pick the debounce for how fresh the answer must be,** not shorter. A 10-minute debounce with a 1-hour ceiling judges a busy account at most a few times an hour.
* **Read the replay estimate.** It is your own traffic, so it is the best guide to what a change will cost.

## The document each judged document points at

A relation can also run the other way, and read the one document the judged document points at through its own attribute, such as an order line reading its product. A change to that document re-judges the documents that point at it, which has its own costs and controls. See [judge a document with the document it points at](/guides/referenced-document).

## More kinds of relation

A relation can also show another judgment's answers for the documents it reads ([roll-ups](/guides/roll-ups)), or read the documents that share a key with the judged one and choose among them ([matching](/guides/entity-matching)). [Relations](/concepts/relations) lists every kind.

## Not available yet

* Oldest-first ordering, such as the opening messages of a thread.
* Named collections. Use `applies_to` on an attribute such as `kind`.
* Judgments that read the answers of a judgment with related documents: a relation reads only [plain](/concepts/relations#one-hop) judgments.


## Related topics

- [Judge a document with the document it points at](/guides/referenced-document.md)
- [Relations](/concepts/relations.md)
- [Writing a context recipe](/guides/context-recipes.md)
- [Roll up answers from related documents](/guides/roll-ups.md)
- [Judge what changed since the last verdict](/guides/change-detection.md)


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