What the engine uses, and what it refuses
Every input the recommender reads, where it acts, and the data it turns away on purpose.
This page lists every input Patterna's recommender reads, in the form it accepts it, and where each one acts. Anything not on this page is not used. The proof rules hold for all of it: every served slot keeps its exact logged propensity, caps live inside the sampler, and the control arm never reads a personal signal.
Where a signal acts:
| Code | Meaning |
|---|---|
| G | a generator score (it proposes and scores items) |
| F | a filter or a cap (it keeps items out) |
| R | a feature of the nightly gbdt ranker (read when a flow uses ranker.kind: gbdt) |
| L | a ledger metric |
| E | the reason text |
Request context#
Sent per call in context on POST /recommendations. It is stored only with that request's impression, never on the user.
| Field | Form | Acts in | Notes |
|---|---|---|---|
locale | nl, en; nl-NL counts as nl | F | matches the item's locales; a locale no item carries widens to all items |
platform | ios, android, web | R | |
app_version | a version string | logged | stored with the impression; not ranked on |
hour_bucket | 0 to 23, from the device clock | G time_fit, R | with the weekday it picks one of eight time buckets: weekday or weekend × morning (06–11), day (12–17), evening (18–22), night |
weekday | 0 (Monday) to 6, from the device clock | G time_fit, R | when absent the engine uses its own clock in UTC, so send it |
region | one of the region names you configured (a city or a province) | G region_match | never coordinates; a name outside your list is 400 unknown_region |
subscription_tier | free, premium or a tier you define | F | for free, an item that showed this user a paywall in the last 7 days is left out. The premium cap follows context.premium or the profile, not this field |
tenure_days | days since the account was created | logged | bucketed by the engine to new (<7), settled (<90) and long, and stored with the impression as that bucket; not a ranker feature yet. You may send it already rounded (0, 7 or 90); the exact day count is never stored |
anchor_item_id | an external id of any kind | G item_to_item, E | resolved against the whole catalog: an article can anchor a product list |
session_items | up to 20 item ids this session just saw or tapped | G session_items, G vector_retrieval | merged with what the session reported through /interactions and its signed events; works in every mode. An interaction is reflected in the next slate of the session: with Redis the engine keeps the session's last two hours and its running content vector there and reads them first; without Redis, or with Redis down, it reads the log |
member_of | community ids | F | joined communities are never offered |
control_item_ids | your editorial list for this page | control arm | logged as control_source: request |
measured, deadline_ms | proof and timing controls | see Timeouts and the ledger |
A bad value for weekday, subscription_tier or tenure_days answers 400. The TypeScript, Go and Python clients (SDK 0.4.1) carry typed fields for them.
Catalog attributes#
Sent in attributes on PUT /catalog/items. They are editorial data about the item, never about a person. Unknown keys are kept for the trace and ignored.
| Attribute | Form | Acts in |
|---|---|---|
format | text, video, audio, recipe, workout, tool, product | G format_fit (against the user's learned format preference) |
duration_min or reading_time_min | number | G time_fit pairs short items with the day and long ones with the evening and the weekend until the item has its own time profile |
difficulty | easy, medium, hard | read into the item model; not ranked on yet |
author | string | read into the item model; not ranked on yet |
available or in_stock | boolean (products) | F: false is never served, in both arms (filters.hide_unavailable, on by default) |
expires_at | RFC 3339 | F: never served after it, in both arms |
starts_at | RFC 3339 (events) | F: a past event is never served, in both arms |
repurchase_days | integer (products) | F: a product the user bought is not offered again inside this many days; set a small number for consumables |
region (or the item's region field) | one of your regions | G region_match |
phase_fit | a list of phases | returned only when the tenant turned phase_tags on; it never ranks |
Kept for the panel and your own cards, not ranked on: price_cents, compare_at_cents, currency, categories[], image_url, url, product_type.
Events#
Sent with POST /events. The weights are the nightly affinity learner's defaults. They are fixed in the learner today, not set per tenant. Every weight decays with a 30-day half-life, and the learners read slates of arm treatment and none only, never control. The full field list: Event types.
| Event | Teaches | Weight | Also acts in |
|---|---|---|---|
rec_click, rec_dismiss (or the SDK's interaction report) | what was taken or turned away on a slate | click +1.0, dismiss −1.0 | L tap_rate; F: a dismissed item stays out for exclude_dismissed_days (30) |
rec_seen | what was shown | L | |
item_viewed | weak interest | +0.2 | G trending (cold start) |
item_engaged | real interest | +0.6 when 20 s or more, or 60 % scrolled | G trending counts an engaged view three times; R engaged_rate; the user's format preference |
item_completed | strong interest | +1.5 | F: stays out for exclude_consumed_days (30, personalized); the user's format preference |
item_pinned, item_unpinned | explicit preference | +2.0 / −1.0 | F: unpinned counts as dismissed |
community_joined, community_left | membership | +1.5 / −1.0 | F: joined communities are never offered; L join_rate |
community_post_created, community_reply, community_reaction | active membership | +1.5 on the community, +0.5 on its tags | R activity_rate per community |
community_post_viewed | weak interest in a community | +0.2 | |
search_performed with clicked_item_id | intent right now | +0.8 on the clicked item | the query arrives only as a hash |
push_opened | content that pulls people back | +1.0 | R pullback_rate per item |
product_viewed | shop interest | +0.5 | |
order_placed | a purchase | +2.0 per product | L purchase_rate; F: the repurchase window; co-purchase pairs for product neighbours |
paywall_viewed | a free user hit the wall | F: not offered to that free user again for paywall_cooldown_days (7) | |
subscription_started, subscription_cancelled | a tier change | ||
event_rsvp, event_attended | event interest | +1.0 on event_rsvp | |
coach_product_shown, coach_product_tapped | assistant outcomes | reported, no holdout | |
consent_changed, user_deleted | identity lifecycle | the opt-out and deletion paths |
What the nightly jobs learn from them#
| Artifact | From | Used by |
|---|---|---|
item features: engaged_rate, pullback_rate, activity_rate, purchase_rate | events and impressions | R |
| item time profile: tap rate in each of the eight time buckets, smoothed toward the surface's own rate | impressions with their context and the taps on them | G time_fit |
| the user's format preference and active time buckets (personalized only) | item_engaged, item_completed, taps | G format_fit |
| purchases per user and product | order_placed | F: the repurchase window; deleted with the user |
| embeddings and item neighbours | item text, embedded by a local multilingual model on Patterna's own host | G item_to_item, cold start |
| the vector index over the embeddings (HNSW, cosine) | the same embeddings, indexed in the database | G vector_retrieval: the k items nearest to what the person looked at join the candidates per request; a new item counts from the night it was embedded |
| the person's 200 nearest users (user ids and similarities only, never items) | the same matrix factorisation as user_bayes, or shared items for people without a row | G user_to_user; deleted with the user |
| the next-item model (a small neural network over the order in which people open items, exported to ONNX; one per tenant, replaced only when it beats the plain path rule on held-out data) | the same positives the collaborative learners read, each person's or session's items in order | G sequence_model |
| product co-purchase pairs | order_placed, or a shop's orders export read as aggregates | product neighbours |
| generator posteriors per surface | logged contributions and taps | the Thompson mixture when ranker.bandit is on |
Refused on purpose#
Patterna does not accept: cycle phase or symptoms, mood, conversations with an assistant, precise location, names, email addresses, IP addresses, free-text search queries (only their hash), and anything tied to a user id in non_profiled mode. An event that carries a field like this is rejected with pii_rejected and the field's name, never stored with the field quietly removed. The catalog importers do not read health or conversation tables. See Data we never store.
How a flow turns them on#
Each signal is opt-in per surface through its flow (Flow config reference):
{
"generators": [
{ "kind": "item_to_item", "weight": 0.5 },
{ "kind": "session_items", "weight": 0.3 },
{ "kind": "time_fit", "weight": 0.15 },
{ "kind": "format_fit", "weight": 0.15 },
{ "kind": "trending", "weight": 0.2 },
{ "kind": "surface_specific", "weight": 0.1 }
],
"filters": {
"diversity": 0.2,
"exclude_dismissed_days": 30,
"paywall_cooldown_days": 7,
"region_boost": 0.3
},
"ranker": { "kind": "gbdt", "bandit": false }
}Every generator is optional. A surface without the signal gets zero from it and still serves. The personal exclusions (dismissed, completed, paywalled, bought) are on by default and relaxed when nothing else would be left; the hard rules (expired, past events, out of stock) are never relaxed and apply in both arms. Simulate and the ledger read the logged contributions, so adding a signal never changes what was already proven.
What a reason may say#
A slot's reason names the generator that won it, and only when that is true:
- the winner's own signal must be positive for the item (a similarity, an affinity or a model score above zero);
- "trending" needs this surface's own log; before it has one, the slot says "popular";
- "people like you" needs a real persona, not the cold-start one;
- "new" means published in the last 30 days.
Otherwise the slot gets the reason key recommended ("Aanbevolen" / "Recommended") and nothing more. time_fit, format_fit and region_match never name a reason: "because it is 9 pm" is not a reason a person would accept. curated is a boost by rank (the first item +2), so an editor's list of three items never drowns a catalog of five hundred. See Reasons.