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

# 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 per [judgment](/pricing#judgments), each counted by its size class, which the tokens of compiled context and question together decide, so the recipe is your biggest lever on cost. It is also a lever on accuracy: the engine answers from what it is shown, and a few recent related records are usually enough.

```json theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{
  "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, the one document it points at, or the documents that share a key with it, rendered after `fields`. See [related documents](#related-documents) and [relations](/concepts/relations). |
| `previous` | The judged document as the judgment's last successful evaluation saw it, rendered as `previous.<path>` entries after `fields`. See [judge what changed since the last verdict](/guides/change-detection). |

A definition needs a recipe. Leaving out `context` is refused with one suggested from your documents; see [when you leave out context](#when-you-leave-out-context).

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.

The recipe is the part of Brussle that takes real thought. It decides three things:

* **Cost.** The tokens it sends, with the question, set each judgment's [size class](/pricing#judgments).
* **Accuracy.** The engine answers from what it is shown, and nothing else.
* **When anything is judged again.** A change to a field or related record that the recipe leaves out changes nothing the engine sees, so it costs nothing and re-runs nothing.

For each question, ask what a careful person would need to see to answer it, and send only that. Then [check what the engine saw](#check-what-the-engine-saw) on a few documents.

## Start from a starter

Starter judgments are ready-made definitions for the most common questions: ticket triage (is it urgent, which team), moderation, churn risk, lead quality and review queues. Each has its question, type, recipe and suggested thresholds already written, and its paths are placeholders you fill with your own. [Starter judgments](/guides/starter-judgments) lists every one, with what it reads and when to use it; `GET /v1/starters` returns the same list.

Fill one in with `from_starter` and `paths`, and add `dry_run` to see the definition it makes and what it would cost, without creating anything:

<CodeGroup>
  ```python Python theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  preview = ns.judgments.create(
      from_starter="ticket_triage.urgency",
      paths={"subject": "state.subject", "body": "state.body", "messages": "state.messages"},
      engine={"name": "jev", "version": "current"},
      dry_run=True,
  )
  ```

  ```ts TypeScript theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  const preview = await ns.judgments.create({
    from_starter: "ticket_triage.urgency",
    paths: { subject: "state.subject", body: "state.body", messages: "state.messages" },
    engine: { name: "jev", version: "current" },
    dry_run: true,
  });
  ```
</CodeGroup>

```json theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{
  "dry_run": true,
  "definition": {
    "name": "urgent",
    "type": "bool",
    "question": "Is this ticket urgent: does the customer need a reply within hours rather than days?",
    "criteria": "Urgent when a service is down or unusable, ...",
    "context": {"fields": ["state.subject", "state.body"], "last_n": {"state.messages": 3}, "max_tokens": 1500},
    "engine": {"name": "jev", "version": "current"},
    "thresholds": {"urgent": 0.8}
  },
  "warnings": [],
  "cost_per_answer": 1.0,
  "sampled_documents": 1000,
  "estimate": {"documents": 48210, "tokens": 30856400, "judgment_units": 48210, "cost_usd": 12.05, "duration_s": 4683},
  "replay": null,
  "outcomes": null
}
```

* `definition` is exactly what creating it makes. To change its question, criteria or recipe, edit it and create it as an ordinary definition.
* `cost_per_answer` is the mean [judgments](/pricing#judgments) per answer, each counted by its size class, over a sample of up to 1,000 of the documents it would judge: 1.0 when every answer is standard, 4.0 when every one is large.
* `estimate` is what [backfilling](/concepts/judgments#backfill) the documents you already have would cost. Creating a judgment never backfills by itself.
* For a starter that reads related documents (churn risk), `replay` is its estimated monthly cost.

The same body without `dry_run` creates it, and the response carries the same `definition`. A placeholder that is not optional must be given, and each must be the right kind of path; otherwise the request is refused with `invalid_request` and `details` names the problem. You can pass `name`, `engine`, `freshness`, `activate` and `confirm` beside `from_starter`, and nothing else. A starter sets no freshness policy, so without `freshness` the judgment is `on_read` and its answers cannot be filtered, sorted or subscribed to; pass `"freshness": {"policy": "on_change"}` for that, as the [starter judgments](/guides/starter-judgments) bodies do.

A starter can suggest how its [outcomes](/guides/measure-improve-tune#outcomes-from-your-own-data) are read. Churn risk turns on implicit negatives and, if you give the account's status path, adds an outcome rule for "status becomes cancelled". They are set when the judgment is first created: implicit negatives on every plan, and the rule only on a plan with outcome rules. On any other plan the response's `outcomes` returns the rule with `plan_required`, so you can set it with `PATCH` after you upgrade.

In the dashboard, "New judgment" has the same flow: pick a starter, choose each path from the fields in your documents, check the definition and its cost, then create.

## When you leave out context

A definition needs a context recipe. If you leave `context` out, the create is refused with `invalid_request`, nothing is created, and `details.suggested_context` suggests one from a sample of your documents:

```json theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{
  "recipe": {
    "fields": ["state.body", "state.messages", "state.subject", "attributes.plan"],
    "last_n": {"state.messages": 5},
    "max_tokens": 1000
  },
  "cost_per_answer": 1.0,
  "whole_state_cost_per_answer": 4.0,
  "sampled_documents": 100,
  "excluded": [
    {"path": "state.attachment", "reason": "binary"},
    {"path": "state.created_at", "reason": "timestamp"},
    {"path": "state.link", "reason": "url"},
    {"path": "state.ticket_id", "reason": "id"},
    {"path": "state.transcript", "reason": "long"}
  ]
}
```

It keeps short text, numbers, booleans and small attributes. It leaves out ids, timestamps, links, binary-looking and very long values, and rarely present fields, and `excluded` says why for each. Long arrays keep their most recent items, and `max_tokens` is set to keep the context and its question together in the standard size class: the cap leaves room for the question. A longer question you add later counts too, so if `cost_per_answer` rises above 1.0, lower `max_tokens` by about the question's growth, or shorten the question. The same documents always give the same suggestion.

Then either:

* send a recipe of your own as `context`, starting from the suggestion;
* send `"use_context": "suggested"` to create with the suggestion as it is. The response's `definition` shows the recipe it used;
* send `"use_context": "whole_state"` to send the whole `state`, up to the engine's limit. That is allowed but rarely what you want: it costs the most per answer, and any change to the document re-runs it. The response warns.

A namespace with no documents yet has nothing to suggest from, and `suggested_context` is null: pass a recipe or `whole_state`.

## Context size is the cost

Each judgment answered counts by its size class, which the tokens of its compiled context and its question together decide, whichever engine answers:

| Size class | Tokens of context and question | Counts as |
| - | - | - |
| Standard | up to 2,000 | 1 judgment |
| Large | up to 8,000 | 4 judgments |
| Extra-large | up to 32,000, the engine maximum | 16 judgments |

The question counts as the engine reads it, with its criteria and every option's description, so a choice between many long-described options can be large over a context where a yes/no question is standard. It costs that again every time the document changes. For one judgment whose context and question come to 2,000 tokens, a standard judgment:

| Workload | Judgments a month |
| - | - |
| 10M documents, each changing once a month | 10M |
| 10M documents, each changing 10 times a month | 100M |

Over a 6,000-token context each is large, and the same workloads are 40M and 400M.

[Pricing](/pricing#judgments) has the price per judgment, which falls as your organization's volume in a billing period grows. A smaller context lowers the bill when it moves judgments into a smaller size class: a recipe that sends the last three messages and the plan, under 2,000 tokens with the question, instead of a 6,000-token history makes each judgment standard instead of large, a quarter of the price. Within a class, fewer tokens cost the same: cutting a 1,500-token context to 800 changes nothing on the bill.

## Let your outcomes tune the recipe

Most recipes start generous: it is hard to know in advance which fields the question needs. Once you post [outcomes](/guides/measure-improve-tune#post-labelled-examples), Brussle checks for you. When calibration is fitted, and at most monthly after that, it tests cheaper variants of your recipe against your labelled outcomes. It uses the contexts already stored with your evaluations, sent to the same engine on the same terms; nothing is read from your documents again. A smaller recipe is cheaper only where it moves answers into a smaller size class. It suggests one only when it is meaningfully cheaper and accuracy is unchanged within noise. Tuning is never billed, and never applied for you.

```python theme={"theme":{"light":"css-variables","dark":"css-variables"}}
found = ns.judgments.recipe_suggestion("needs_escalation")
```

```json theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{
  "version": 3,
  "status": "ready",
  "reason": null,
  "message": "A context recipe 75% cheaper per answer, moving answers to a smaller size class, with accuracy unchanged within noise on 312 labelled answers.",
  "headline": "A context recipe 75% cheaper per answer, moving answers to a smaller size class, with accuracy unchanged within noise on 312 labelled answers.",
  "suggestion": {
    "recipe": {"fields": ["state.subject", "state.body", "attributes.plan"], "last_n": {"state.messages": 3}},
    "removed": ["the field state.internal_notes"],
    "definition": {"name": "needs_escalation", "type": "bool", "question": "...", "context": {"...": "..."}, "engine": {"name": "jev", "version": "current"}},
    "unit_reduction": 0.75,
    "cost_per_answer_before": 4.0,
    "cost_per_answer_after": 1.0,
    "held_out": {"log_loss_before": 0.412, "log_loss_after": 0.409, "accuracy_before": 0.84, "accuracy_after": 0.85, "interval": {"lower": -0.011, "upper": 0.005}},
    "labels": 312,
    "engine_version": "current+2026-09-24.1",
    "computed_at": "2026-10-03T03:12:40Z"
  },
  "computed_at": "2026-10-03T03:12:40Z",
  "next_run_after": "2026-11-02T03:00:00Z",
  "plan_required": null
}
```

To apply it, post `definition` as a new version and activate it through its [shadow report](/guides/measure-improve-tune#change-a-question-safely-with-a-shadow-report), which shows what the change does to your answers before you confirm. `definition` is your active version with only `context` changed, and it gives no thresholds, so your current ones carry over. [Measure, improve, tune](/guides/measure-improve-tune#let-your-outcomes-tune-the-recipe) has every field and when there is no suggestion.

## Share recipes where you can

Judgments that share a recipe are answered together, up to 32 questions per request, which is faster and lighter on rate limits, and that matters most for large backfills. Each is still billed on its own, by its own size class. 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 says which to try first.

```json theme={"theme":{"light":"css-variables","dark":"css-variables"}}
{
  "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`. It can also select by another judgment's answer: see [roll-ups](/guides/roll-ups#when-the-account-is-judged-again). |
| `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). Or the same attribute on both sides, `{"theirs": "attributes.<key>", "mine": "attributes.<key>"}`: a blocking relation over the documents that share a key, for [matching](/guides/entity-matching). |
| `last_n` | Keep the newest n, from 1 to 1,000; at most 254 on a blocking relation, and 10 on one a choice chooses among. 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. Required on a blocking relation, where it counts back from the block's newest document. 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`, and, except on a blocking relation, another judgment's answer at `answers.<judgment>.<field>` ([roll-ups](/guides/roll-ups)). A number can be shown as its band (below). |
| `aggregate` | `{"count": true, "count_where": {...}, "sum": [paths], "min": [paths], "max": [paths], "latest": [paths]}` over the same documents, at most 8 paths. `count_where` counts the documents whose answers meet a condition. |

* **Up to 4 relations,** each with `fields`, `aggregate` or both. Each one that reads the documents pointing at the judged one takes `last_n`, `window` or both; a blocking one needs `window`; one that reads the document it points at takes neither.
* **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={"theme":{"light":"css-variables","dark":"css-variables"}}
{
  "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 those of the relation before it, then any [`previous`](/guides/change-detection#cost-and-limits) entries, 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 `task` points at its `project`:

```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 renders like any relation, with at most one record, so it takes neither `last_n` nor `window`. It can also show that document's [answers](/guides/roll-ups#when-the-account-is-judged-again). 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={"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, 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).


## Related topics

- [Judgments](/concepts/judgments.md)
- [Starter judgments](/guides/starter-judgments.md)
- [Get the cheaper context recipe the outcomes support](/api-reference/outcomes-and-calibration/get-the-cheaper-context-recipe-the-outcomes-support.md)
- [Documents](/concepts/documents.md)
- [Run this month's recipe tuning now](/api-reference/outcomes-and-calibration/run-this-months-recipe-tuning-now.md)


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