# Reasons

> Every slot carries a reason key and a sentence in the user's language. No reason, no recommendation.

Source: https://www.patterna.nl/docs/reasons

Every item Patterna returns comes with a reason the user could read. The reason is not decoration. It is a promise that the system knows why it chose the item, and it is the first thing a user, an editor or a regulator asks.

## How a reason is made

1. Several generators score the candidates (trending, curated, item-to-item, session items, content affinity, persona model, user model, surface-specific, and the silent `time_fit` and `format_fit`).
2. The ranker adds the scores up. For each slot, the generator with the largest contribution is the **winner**. `time_fit`, `format_fit` and the region boost never win: they move scores, but nothing they could say would be a reason a person accepts.
3. The winner maps to a **reason key**, for example `item_to_item` with an anchor → `similar_to_read`, but only when the reason is true (next section). Otherwise the key is `recommended`.
4. Serve looks up the sentence for that reason key: your override from the flow's `reasons` map if there is one, otherwise Patterna's default. It returns the sentence in every locale it has.

A slot in the response looks like this:

```json
{
  "item_id": "doc_4821",
  "kind": "document",
  "position": 0,
  "reason_key": "similar_to_read",
  "reason": {
    "nl": "Omdat je dit net las",
    "en": "Because you just read this"
  },
  "generator": "item_to_item",
  "score": 0.82
}
```

Positions start at 0. Your app shows `reason[locale]` as one line under the title. If the user's locale is missing from the map, fall back to your default locale.

## The truthfulness rule

A slot names its winner only when what the sentence says is true for that item:

- **The winner's own signal is positive.** The raw similarity, affinity or model score for the item must be above zero. An item can win a slot on a z-score while its raw signal is zero or negative (it was merely the least bad); then there is nothing true to say about it.
- **"Trending" needs this surface's own log.** Before the surface has one, trending runs on a tenant-wide popularity prior, and the slot says `popular`, not "widely read this week".
- **"People like you" needs a real persona.** A user scored with the cold-start persona gets `recommended`.
- **"New" means new.** `surface_specific` says `new` only for an item published in the last 30 days.

When the rule fails, the slot carries `reason_key: "recommended"` with "Aanbevolen" / "Recommended" and nothing more. That is a correct answer, not an error: show it like any other reason, or hide the reason line for this key if your design prefers. Expect more `recommended` slots on a new surface and for new users; their share falls as the log and the personas grow.

## Reason keys and defaults

| Winner | Reason key | Default (nl / en) |
|---|---|---|
| `item_to_item` with an anchor | `similar_to_read` | Omdat je dit net las / Because you just read this |
| `item_to_item` without an anchor | `similar` | Lijkt op wat je leest / Similar to what you read |
| `content_affinity` on declared goals | `matches_goals` | Past bij je doelen / Matches your goals |
| `content_affinity` on behaviour | `more_about_tag` | Meer over dit onderwerp / More about this topic (a template such as "Meer over {tag}" names the tag) |
| `item_to_item` with an anchor of another kind that pairs: a recipe or a workout with a product, a community with a document | `pairs_with` | Past bij dit recept / Pairs with this recipe; Past bij deze workout / Pairs with this workout; Gelezen in deze community / Read in this community |
| `item_to_item` with any other cross-kind anchor | `more_about_tag` | as above |
| `session_items` | `similar` | Lijkt op wat je leest / Similar to what you read |
| `persona_bayes`, `user_bayes`, `user_to_user` | `people_like_you` | Populair bij leden zoals jij / Popular with members like you |
| `trending` on this surface's own log | `trending` | Deze week veel gelezen / Widely read this week |
| `trending` on the tenant-wide prior | `popular` | Populair / Popular |
| `curated` | `editors_pick` | Gekozen door de redactie / Chosen by the editors |
| `surface_specific`, item under 30 days | `new` | Nieuw / New |
| any winner whose signal is not positive; a cold persona; an old item under `surface_specific` | `recommended` | Aanbevolen / Recommended |

The editorial list (the control arm, and the fallback) carries `editors_pick` for a curated list, `popular` for the 7-day popular list and `new` for the newest list. The full table is in [Reason keys](https://www.patterna.nl/docs/reference-reason-keys).

`trending` is the smoothed 7-day tap rate of each item on this surface, all locales together (the request's locale decides which items are candidates). It is not computed per persona or segment.

`pairs_with` is said only where it is true: a product under a recipe (a document in a collection whose name contains recipe, recept, maaltijd or meal), a product under a workout (workout, training, oefening, exercise), or a document under a community. Each pairing has its own default text. Any other cross-kind anchor, such as a product under an ordinary article, says `more_about_tag`. `context.anchor_item_id` is looked up in the whole catalog, whatever its kind.

## Overrides

Overrides live in the flow config under `reasons`, keyed by reason key or by generator kind, then by locale:

```json
{
  "reasons": {
    "similar_to_read": { "nl": "Omdat je dit net las", "en": "Because you just read this" },
    "trending": { "nl": "Veel gelezen deze week", "en": "Read a lot this week" },
    "content_affinity": { "nl": "Meer over {tag}", "en": "More about {tag}" }
  }
}
```

**Precedence.** For each slot serve takes the reason-key template if there is one, else the template of the winning generator, else the default.

An override replaces the whole locale map for that key. If you override `trending` with only `nl`, the `en` default is gone for that key, so always give every locale you serve.

Because overrides are part of the flow, changing a reason creates a new flow version. You edit them per locale in the flow editor. The change is versioned, audited and can be rolled back like any flow change.

## Placeholders

- `{anchor_title}`: the anchor item's title in the request locale.
- `{tag}`: the winning tag's label per locale, from the taxonomy. Phase tags and health claims are never named.
- `{segment}`: the user's persona label (taxonomy kind `segment`).

When a placeholder cannot be filled for a locale, that locale gets the default text instead, so a user never sees a literal `{tag}`.

## Validation

`POST /surfaces/{key}/flows` refuses an unknown reason key or generator key, an unknown locale or an unknown placeholder with `400 invalid_reason_template`, so a typo never reaches a user.

## Drafts from the jobs

The `reasons` job drafts missing sentences with the LLM and saves them as a **new, inactive** flow version with a note that starts with `REVIEW NEEDED`, plus an audit entry. It never overwrites a text and never activates anything.

The job keys its drafts by generator kind. Serve reads generator-keyed templates, so the drafts work as they are.

## Locales

Reasons come back as a map per locale, today `nl` and `en`. The app picks the sentence to show. The request's `context.locale` filters items; it does not choose the reason.

## Rules for good reasons

- **True.** The sentence must describe what the generator actually used. "Because you read X" only from item-to-item with an anchor; "Pairs with this recipe" only under a recipe. The engine enforces the rule above for its own keys; your overrides must keep to it too.
- **Short.** One line under a title.
- **No health values.** A reason never states a measurement, a symptom or a cycle phase.
- **Plain.** No superlatives, no "just for you".
- **Approved.** Have an editor sign off every sentence per locale before activation.

You may choose not to render the reason on a placement. Patterna still returns and logs it.

## Read next

- [Reason keys](https://www.patterna.nl/docs/reference-reason-keys)
- [What the engine uses, and what it refuses](https://www.patterna.nl/docs/signals)
- [Flow config reference](https://www.patterna.nl/docs/reference-flow-config)
- [Surfaces and flows](https://www.patterna.nl/docs/surfaces-and-flows)
