Patterna Docs

Reasons

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

Everyone 6 min read View as Markdown

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#

WinnerReason keyDefault (nl / en)
item_to_item with an anchorsimilar_to_readOmdat je dit net las / Because you just read this
item_to_item without an anchorsimilarLijkt op wat je leest / Similar to what you read
content_affinity on declared goalsmatches_goalsPast bij je doelen / Matches your goals
content_affinity on behaviourmore_about_tagMeer 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 documentpairs_withPast 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 anchormore_about_tagas above
session_itemssimilarLijkt op wat je leest / Similar to what you read
persona_bayes, user_bayes, user_to_userpeople_like_youPopulair bij leden zoals jij / Popular with members like you
trending on this surface's own logtrendingDeze week veel gelezen / Widely read this week
trending on the tenant-wide priorpopularPopulair / Popular
curatededitors_pickGekozen door de redactie / Chosen by the editors
surface_specific, item under 30 daysnewNieuw / New
any winner whose signal is not positive; a cold persona; an old item under surface_specificrecommendedAanbevolen / 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.

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.

esc
  • Type to search every page, heading and code sample.