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

# Relations in a recipe

> Every key of a recipe's related block: which documents a relation reads, how it orders and counts them, what the engine sees, and what Brussle cuts first when the context is too long.

A relation in a [context recipe](/guides/context-recipes) shows the engine other documents beside the one it judges, and Brussle judges the answer again when what the relation shows changes. [Judge a document with its related documents](/guides/related-documents) walks through one relation, and [relations](/concepts/relations) compares the kinds.

## Define a relation

A payment reads the open invoices in its block (the invoices with the same customer and currency). One relation shows the 10 newest as text, and a second counts all of them from the last 180 days and adds their amounts:

```json theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{
  "context": {
    "fields": ["state.amount", "state.reference", "state.payer"],
    "related": {
      "open_invoices": {
        "match": {"attributes.kind": "invoice", "attributes.status": "open"},
        "join": {"theirs": "attributes.block", "mine": "attributes.block"},
        "window": "180d",
        "last_n": 10,
        "fields": ["state.number", "state.amount", "state.issued_at"]
      },
      "open_totals": {
        "match": {"attributes.kind": "invoice", "attributes.status": "open"},
        "join": {"theirs": "attributes.block", "mine": "attributes.block"},
        "window": "180d",
        "aggregate": {"count": true, "sum": ["state.amount"]}
      }
    }
  }
}
```

| Key | Effect |
| - | - |
| `match` | Which documents the relation reads, as an attribute filter in the shape of `applies_to` with at most 8 keys. It can also select by another judgment's answer, `answers.<j>.value`, a threshold or a cut on `p` or `score` ([roll-ups](/guides/roll-ups#when-the-contract-is-judged-again)), or by a document's newest outcome, `outcomes.<j>.value` ([what happened to them](/guides/related-documents#read-what-happened-to-related-documents)). |
| `join` | How a document relates to the judged one. See [the four joins](#the-four-joins). |
| `last_n` | Keep the first n documents in the relation's order (newest first unless `order` changes it). It takes 1 to 1,000. A blocking relation and a named list take at most 254, and a relation that a choice picks from with `options.from` takes at most 10. |
| `window` | Keep the documents created inside this time, such as `"90d"`. See [how window counts](#how-window-counts). |
| `fields` | Paths shown from each document: `state.*`, `attributes.*`, `images.<name>`, `id`, `created_at` and `updated_at`; another judgment's `answers.<j>.value` or `answers.<j>.thresholds.<name>`, and its `p`, `score` or `dist.<option>` only as a [band](#show-a-number-as-a-band); a document's newest outcome, `outcomes.<j>.value`. Any entry except an image can take a [`max_chars`](/guides/context-recipes#cut-a-long-field). |
| `order` | Which documents come first, such as `{"by": "state.due", "desc": false}`. See [pick documents with order](#pick-documents-with-order). |
| `aggregate` | Numbers over the same documents. See [count, sum and show the latest](#count-sum-and-show-the-latest). |
| `relative` | On a blocking relation only, read the `last_n` documents of the block created just `before` or `after` the judged one. See [records in sequence](/guides/related-documents#records-in-sequence). |

Each judgment has up to 4 relations, and each relation has `fields`, `aggregate` or both. A relation reads the answers or outcomes of at most 4 judgments, and a document and every document related to it are in the same namespace.

### The four joins

* **The documents that point at the judged one:** `{"theirs": "attributes.<name>", "mine": "id"}`. A document belongs to the judged document whose `id` is in its `theirs` attribute, and the relation takes `last_n`, `window` or both.
* **The document the judged one points at:** `{"theirs": "id", "mine": "attributes.<name>"}`. See [read the document the judged one points at](#read-the-document-the-judged-one-points-at).
* **The documents that share a key:** `{"theirs": "attributes.<key>", "mine": "attributes.<key>"}`, the same attribute on both sides. This blocking relation reads the candidates for [matching](/guides/entity-matching), and it needs `window` unless it sets `relative`.
* **The documents a matching judgment linked:** one side `id`, the other `answers.<j>.value` of a judgment that chooses among a blocking relation's candidates, with `when` naming one of its thresholds. See [read the matches from the invoice](/guides/entity-matching#read-the-matches-from-the-invoice).

Brussle refuses two different attributes on the two sides with `invalid_request`.

## Which documents a relation reads

* **Newest created first, unless `order` changes it.** Brussle orders by `created_at`, so `last_n: 8` is the 8 most recently created. It applies `window` first, then the order, then `last_n`.
* **At most the newest 1,000.** A relation reads at most the newest 1,000 matching documents, whatever its `last_n`, `window` and `order`. Brussle applies a `match` on answers or outcomes to each of those 1,000 before `last_n` keeps the newest that qualify.

### How window counts

The window counts back from the later of two times (when Brussle received the judged document's newest write, and the newest creation among its related documents) and never from the clock, so an edit to an old related document never moves it. You can set a document's `created_at` but not the time of a write, so after an [import](/guides/import-existing-data) a window counts back from the import. Use `last_n` to read imported history. On a blocking relation, the window counts back from the newest document in the block. A relation that reads the document the judged one points at takes no `window`.

## Count, sum and show the latest

`aggregate` computes numbers over the documents the relation selected, in the shape `{"count": true, "count_where": {...}, "sum": [paths], "avg": [paths], "min": [paths], "max": [paths], "latest": [paths]}`, with at most 8 paths in all.

* **`count`** is the number of documents selected. **`labelled`**, on a relation that reads an outcome, counts those that have one.
* **`count_where`** counts the documents whose answers or newest outcome meet a condition. See [count the matches of a matcher](/guides/roll-up-rules#count-the-matches-of-a-matcher).
* **`sum`, `avg`, `min` and `max`** skip values that are not numbers. With nothing to work on, `sum` is 0, and `avg`, `min` and `max` are null.
* **`latest`** is the value in the most recently created document that has one, whatever the order. It can name an [image](/guides/images) (`images.photo`, shown as `latest(images.photo)`), and an engine that reads images counts it like any other image.
* **`latest`, `min` and `max` can name `created_at` or `updated_at`**, which gives the time of the newest, oldest or most recent document as RFC 3339 text. The feature `age(<relation>.latest(created_at))` turns that time into days, measured from the judged document's own write.
* **`"render": false`** keeps the aggregates (all but `latest`) out of what the engine reads. [Features](/calibration/features#what-a-feature-can-read) still read them, so Brussle records a change that moves only them with no engine call.

To show a few documents and count many, use two relations with the same `match`. Brussle calculates aggregates before it cuts anything, and never cuts them.

## What the engine sees

Each relation becomes one `related.<name>` entry in the compiled context, after the `fields` entries, in the order of the relations' names:

```json theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{
  "state.amount": 1740.5,
  "state.reference": "INV-2043 ACME",
  "state.payer": "Acme Ltd",
  "related.open_invoices": {"records": [{"state.number": "INV-2043", "state.amount": 1740.5, "state.issued_at": "2026-09-02"}]},
  "related.open_totals": {"count": 4, "sum(state.amount)": 5210.0}
}
```

* **`records`** holds the selected documents in the relation's order, keyed by path.
* **One key per aggregate,** such as `count` and `sum(state.amount)`.
* **`capped: true`** appears when the relation may be missing a matching document, and the evaluation then shows `relations_capped: true`. A relation without `last_n` is capped when it matched more than 1,000 documents, since the numbers then cover the newest 1,000. A relation that selects by an answer or outcome is capped when it found fewer than `last_n` among the newest 1,000 and more matched. A [blocking relation](/guides/entity-matching) is capped when it found more candidates in its block than its `last_n`, so the right one may be unread.
* **`range_full: true`** appears on a relation with an `order` when more documents matched than the 1,000 it sorts.
* **`missing: true`** appears on a relation that reads the document the judged one points at, when that document does not exist. See [a missing referenced document](/guides/referenced-document#a-missing-referenced-document).

## What is cut first

Brussle removes content in this order when the context is over `max_tokens`, until it fits, and records `context_truncated: true`:

1. Whole [images](/guides/images), on an engine that reads them: those of the relations, then of `previous`, then of `fields`, the last one first. Each becomes `{"digest": "sha256:...", "omitted": "over_the_limit"}`, and if the text alone is over, every image goes.
2. The records of the last relation by name. Brussle keeps them whole, in the relation's order, while they fit, so its oldest records go first (or, with an `order`, those that sort last). Brussle cuts the first record that does not fit instead of removing it, by shortening its longest text value, and the records after it keep at most half of the space left. One long record (a meeting transcript, say) therefore does not push out all the others. An image in a record is never cut.
3. The records of the relation before it, and so on.
4. Any [`previous`](/guides/change-detection#cost-and-limits) entries.
5. The `fields`, from the end of the last field.

Raise `max_tokens` if the engine must read long records whole. Brussle bills each answer by the [size class](/pricing#judgments) of the context it sent rather than by `max_tokens`, so only the answers that read long records cost more.

## Read the document the judged one points at

A relation with `join: {"theirs": "id", "mine": "attributes.<name>"}` reads the one document whose `id` is in the judged document's attribute, its referenced document (an order line reads its product, a message its conversation).

```json theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{
  "related": {
    "project": {
      "match": {"attributes.kind": "project"},
      "join": {"theirs": "id", "mine": "attributes.project_id"},
      "fields": ["state.name", "state.status", {"path": "state.score", "bands": [0.3, 0.7], "labels": ["low", "medium", "high"]}]
    }
  }
}
```

* **It shows as any relation, with at most one record,** and takes no `window`.
* **With a `last_n`,** up to 254, it reads the documents that a [named list](/guides/referenced-document#name-several-documents) of ids names.
* **It can show the referenced document's [answers](/guides/roll-ups#when-the-contract-is-judged-again).**
* **It is empty** when the attribute is missing, is not a document id, or names a document outside `match`.
* **A change to what it shows** judges again the documents that point at it, inside the judgment's re-judge scope, which `freshness.fanout` sets. See [fan-out](/guides/freshness-policies#fan-out-when-a-referenced-document-changes) and [judge a document with the document it points at](/guides/referenced-document) for the cost and the controls.

## Pick documents with order

A relation keeps its newest documents by default, while `order` keeps the documents that come first by a field you choose (the invoices due soonest, or the oldest open item):

```json theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{
  "related": {
    "open_invoices": {
      "match": {"attributes.kind": "invoice", "attributes.status": "open"},
      "join": {"theirs": "attributes.account_id", "mine": "id"},
      "last_n": 5,
      "order": {"by": "state.due", "desc": false},
      "fields": ["state.number", "state.due", "state.amount"]
    }
  }
}
```

* **`by`** is `created_at` (the default), `updated_at`, a `state.<path>` or an `attributes.<name>`. **`desc`** is `true` (largest first) unless you set it.
* **Values sort by type first:** numbers, then strings, then `true` and `false`, then missing values and everything else. `desc` reverses the order inside each type, never the order of the types, so a missing value always comes last.
* **Inside a type,** numbers compare by value, whole numbers exactly up to 64 bits. Strings compare by Unicode code point, so an ISO date such as `"2026-10-02"` sorts as a date, and `false` comes before `true`.
* **Ties** go to the most recently created document, then the lower `id`.
* **`records` show in that order,** and over `max_tokens` the records that sort last go first. `latest` is still the newest document's value.
* **Where it works.** `order` works on a relation that reads the documents that point at the judged one, and on a [named list](/guides/referenced-document#name-several-documents), where it replaces the list order. Brussle refuses it on a blocking relation, whose candidates are always the newest, and on a relation that reads one referenced document.

An order sorts only inside the newest 1,000. With 1,001 open invoices, the oldest is outside the newest 1,000 before the order runs, so an order by due date cannot pick it however early it is due. `range_full: true` tells you this happened. Use a `window` or a narrower `match` to keep the documents that matter inside the newest 1,000. The create response warns you if a judged document already has more than 1,000 matching documents.

```mermaid theme={"theme":{"light":"css-variables","dark":"css-variables"}}
flowchart LR
  m["1,001 open invoices<br/>in the window"] --> n["newest 1,000"]
  m -. "left out" .-x o["the oldest invoice"]
  n -- "order: state.due" --> s["due soonest first"]
  s -- "last_n: 5" --> a["account"]
  a:::judged
  classDef judged stroke-width:3px
```

To sort, a relation with an `order` reads up to 1,000 documents for each judged document, where one without reads only its `last_n`. The price of an answer does not change, since Brussle bills judging by the context and not by what it reads. A judged document with many related documents does take a larger share of a pass of judging, so a catch-up over many of them takes longer. A relation whose `match` names an answer or an outcome reads the same 1,000 documents. The create response warns with the factor (a relation with `last_n: 5` reads up to about 200 times as much).

An edit never moves a document into or out of the selection when the order is by `created_at`. With an order by another field it can. A write that changes `state.due` can move that invoice in or out, and Brussle judges again if what the engine sees changed. The create response warns about this, and about an order by a long text field, which sorts up to 1,000 long values for each judgment.

## Show a number as a band

An entry in a relation's `fields` can show a number as its band in place of the number, so a score of 0.42 shows as `medium`:

```json theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{"path": "state.score", "bands": [0.3, 0.7], "labels": ["low", "medium", "high"]}
```

* **`bands`** are 1 to 9 cut points, each larger than the one before. **`labels`** has exactly one more entry, each up to 64 bytes.
* **A number shows as one label,** at the position given by the number of cut points at or below it, so 0.29 is `low`, 0.3 is `medium`, and 0.7 or more is `high`.
* **A value that is not a number** shows unchanged, and a missing value is left out.
* **Aggregates use the number before banding.** On a judgment with [features](/calibration/features), that `p` is the corrected one once a fit applies (see [which number readers use](/calibration/correction#which-number-your-thresholds-and-queries-read)).

The engine sees the word, so a move inside a band does not change the context. The answer stays, Brussle bills nothing, and a referenced document fans out to nothing. Use bands for a number that changes often but matters only past a few thresholds.


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