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

# Writing a context recipe

> Context size is the cost. Send the engine what it needs and nothing else.

A judgment's `context` recipe decides which parts of a document the engine sees. Judging is billed in [judgment units](/pricing#judgments), one per started 1,000 tokens of compiled context and question together, so the recipe is your biggest lever on cost. It is also a lever on accuracy: an engine given the last three messages and the plan often does better than one given a year of history.

```json theme={null}
"context": {
  "fields": ["state.subject", "state.body", "attributes.plan"],
  "last_n": {"state.messages": 3},
  "window": {"state.events": "7d"},
  "max_tokens": 4000
}
```

| Key          | Effect                                                                                                                                                                 |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fields`     | Paths included verbatim, in this order. They may name `state.*` or `attributes.*`. Attributes are never sent unless named here.                                        |
| `last_n`     | For an array path, keep only its last n elements.                                                                                                                      |
| `window`     | For an array whose elements have a timestamp field `at`, keep only the elements within the window, such as `"7d"`, `"12h"` or `"30m"`. Elements without `at` are kept. |
| `max_tokens` | A hard cap. The compiler truncates from the end of the last field until the context fits, and records `context_truncated: true` on the evaluation.                     |
| `related`    | Documents that point at the judged one, rendered after `fields`. See [related documents](#related-documents).                                                          |

Leaving out `context` sends the whole `state`, up to the engine's limit. That is allowed but not recommended, and creating such a judgment returns a warning.

A recipe decides what the engine sees of a document. Which documents a judgment judges at all is `applies_to`, beside the recipe: `{"attributes.kind": "account"}` judges, answers and bills only accounts. Each key is an attribute; a value is equality, and a list of values matches any of them. Keys combine with and, up to 8. Other documents have no answer for the judgment. `applies_to` is part of the definition, so changing it creates a version.

## Context size is the cost

Each judgment answered costs one judgment unit per started 1,000 tokens of its compiled context and its question together: 1 unit up to 1,000 tokens, 2 units up to 2,000, and so on, whichever engine answers. The question counts as the engine reads it, with its criteria and every option's description, so a choice between many long-described options costs more than a yes/no question over the same context. It costs that again every time the document changes. For one judgment whose context and question come to 2,000 tokens:

| Workload                                      | Judgment units a month |
| --------------------------------------------- | ---------------------- |
| 10M documents, each changing once a month     | 20M                    |
| 10M documents, each changing 10 times a month | 200M                   |

[Pricing](/pricing) has the price per unit, which falls as your organization's monthly volume grows. A recipe that sends the last three messages and the plan, under 1,000 tokens, instead of a 6,000-token history cuts each judgment from 6 units to 1.

## Share recipes where you can

All of a document's judgments that use the same recipe go to the engine in one request, with up to 32 questions. The context is sent once, which makes them faster and lighter on the engine's rate limit, and that matters most for large backfills. Each judgment answered is still billed as its own units. Each distinct recipe is its own request. Before you give a new judgment its own recipe, check whether one you already have would do.

## Related documents

`related` lets a judgment read the documents that point at the judged one: an account's tickets and invoices, a user's posts. The answer is kept current as any of those documents change: see [judge a document with its related documents](/guides/related-documents). Start with a small `last_n` of raw text, or with aggregates; that guide shows what each did in our tests.

```json theme={null}
"context": {
  "fields": ["state.name", "attributes.plan"],
  "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"]}
    }
  }
}
```

| Key         | Effect                                                                                                                                                                                                                                                                                                                                   |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `match`     | Which documents the relation reads, as an attribute filter shaped like `applies_to`.                                                                                                                                                                                                                                                     |
| `join`      | `{"theirs": "attributes.<name>", "mine": "id"}`. A document belongs to the judged document whose `id` equals its `theirs` attribute. Or `{"theirs": "id", "mine": "attributes.<name>"}`: the one document the judged document points at (below).                                                                                         |
| `last_n`    | Keep the newest n, from 1 to 1,000. Not on a relation that reads the document the judged one points at.                                                                                                                                                                                                                                  |
| `window`    | Keep the documents created within this long, such as `"90d"`. It counts back from the later of the judged document's own newest write and the newest creation among its related documents, not from the clock, so an edit to an old related document never moves it. Not on a relation that reads the document the judged one points at. |
| `fields`    | Paths shown from each document: `state.*`, `attributes.*`, `id`, `created_at` and `updated_at`. A number can be shown as its band (below).                                                                                                                                                                                               |
| `aggregate` | `{"count": true, "sum": [paths], "min": [paths], "max": [paths], "latest": [paths]}` over the same documents, at most 8 paths.                                                                                                                                                                                                           |

* **Up to 4 relations,** each with `fields`, `aggregate` or both, and each reading the documents that point at the judged one with `last_n`, `window` or both.
* **Newest created first.** The documents are ordered by `created_at`, so `last_n: 8` is the 8 most recently created. `window` applies first, then `last_n`. A relation reads at most the newest 1,000.
* **Aggregates count the relation's selection.** `count` is how many it selected. `sum`, `min` and `max` skip values that are not numbers; `sum` of nothing is 0, and `min` and `max` of nothing are null. `latest` is the value in the newest document that has one. To show a few documents and count many, use two relations with the same `match`.
* **Relations stay in the namespace.** A document and everything related to it live in the same namespace.

Each relation becomes one `related.<name>` entry in the compiled context, after the `fields` entries. It holds `records`, the selected documents newest first, keyed by path; one key per aggregate, such as `count` and `sum(state.amount)`; and `capped: true` when more than 1,000 documents matched, so the numbers cover only the newest 1,000 (only a relation without `last_n` can match that many):

```json theme={null}
{
  "state.name": "Birch Health",
  "attributes.plan": "business",
  "related.tickets": {"records": [{"created_at": "2026-09-24T10:02:11Z", "state.subject": "Export failing", "state.status": "open"}]},
  "related.invoices": {"count": 4, "latest(state.status)": "overdue", "sum(state.amount)": 1740.5}
}
```

Over `max_tokens`, the compiler drops the last relation's oldest records first, then the previous relation's, and only then the `fields`. Relations are rendered, and cut, in the order of their names. Aggregates are computed before truncation and are never cut. An aggregate is a few tokens where raw text is hundreds, so it is the first thing to try when a question depends on volume rather than wording.

### The document the judged one points at

With `join: {"theirs": "id", "mine": "attributes.<name>"}`, a relation reads the one document whose `id` the judged document's own attribute holds: its referenced document. Illustrations across domains: an order line reading its product, a message reading its conversation. Here each `item` points at its `group`:

```json theme={null}
"related": {
  "group": {
    "match": {"attributes.kind": "group"},
    "join": {"theirs": "id", "mine": "attributes.group_id"},
    "fields": ["state.name", "state.status", {"path": "state.score", "bands": [0.3, 0.7], "labels": ["low", "medium", "high"]}]
  }
}
```

It renders like any relation, with at most one record, so it takes neither `last_n` nor `window`. When the attribute is missing, is not a document id, or names a document that does not match `match`, the relation is empty. A change to what it shows of the referenced document re-judges the judged documents that point at it and are inside the judgment's re-judge scope, which the judgment's `freshness.fanout` settings decide. See [judge a document with the document it points at](/guides/referenced-document) for what that costs and the controls, and [fan-out](/guides/freshness-policies#fan-out-when-a-referenced-document-changes) for the settings.

### Bands

An entry of a relation's `fields` can be an object that shows a number as the band it falls in, rather than the number:

```json theme={null}
{"path": "state.score", "bands": [0.3, 0.7], "labels": ["low", "medium", "high"]}
```

* **`bands`** are 1 to 9 cut points, strictly increasing, and **`labels`** has exactly one more entry, each up to 64 bytes.
* A number renders as the label whose position is how many cut points are at or below it: 0.29 is `low`, 0.3 is `medium`, and 0.7 or more is `high`.
* A value that is not a number renders unchanged, and a missing one is left out, as for any path.
* Aggregates always use the raw values.

The engine sees the word, and a move inside a band leaves the context exactly as it was. In a relation that reads the documents pointing at the judged one, that means the answer is kept and nothing is billed. In a relation that reads the document the judged one points at, it means no fan-out at all. Use bands for a number that moves often but matters only past a few thresholds.

## Check what the engine saw

Every evaluation stores its compiled context. Fetch it with `include=history,context` on a get, or open the document in the dashboard, to see the exact text the engine received. `context_tokens` and `context_truncated` on each evaluation tell you how close a recipe runs to its cap.

## Engine limits

The recipe must fit the engine you pin. Jev takes up to 32,000 tokens for the state plus the longest question. Laya, which is coming, will take at most 512 tokens in total and cut a longer context to fit, so it suits short text such as titles, single messages or search queries. See [limits](/limits) and the [engines](/engines/index).
