Patterna Docs

What the engine uses, and what it refuses

Every input the recommender reads, where it acts, and the data it turns away on purpose.

Everyone 8 min read View as Markdown

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:

CodeMeaning
Ga generator score (it proposes and scores items)
Fa filter or a cap (it keeps items out)
Ra feature of the nightly gbdt ranker (read when a flow uses ranker.kind: gbdt)
La ledger metric
Ethe 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.

FieldFormActs inNotes
localenl, en; nl-NL counts as nlFmatches the item's locales; a locale no item carries widens to all items
platformios, android, webR
app_versiona version stringloggedstored with the impression; not ranked on
hour_bucket0 to 23, from the device clockG time_fit, Rwith the weekday it picks one of eight time buckets: weekday or weekend × morning (06–11), day (12–17), evening (18–22), night
weekday0 (Monday) to 6, from the device clockG time_fit, Rwhen absent the engine uses its own clock in UTC, so send it
regionone of the region names you configured (a city or a province)G region_matchnever coordinates; a name outside your list is 400 unknown_region
subscription_tierfree, premium or a tier you defineFfor 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_daysdays since the account was createdloggedbucketed 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_idan external id of any kindG item_to_item, Eresolved against the whole catalog: an article can anchor a product list
session_itemsup to 20 item ids this session just saw or tappedG session_items, G vector_retrievalmerged 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_ofcommunity idsFjoined communities are never offered
control_item_idsyour editorial list for this pagecontrol armlogged as control_source: request
measured, deadline_msproof and timing controlssee 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.

AttributeFormActs in
formattext, video, audio, recipe, workout, tool, productG format_fit (against the user's learned format preference)
duration_min or reading_time_minnumberG 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
difficultyeasy, medium, hardread into the item model; not ranked on yet
authorstringread into the item model; not ranked on yet
available or in_stockboolean (products)F: false is never served, in both arms (filters.hide_unavailable, on by default)
expires_atRFC 3339F: never served after it, in both arms
starts_atRFC 3339 (events)F: a past event is never served, in both arms
repurchase_daysinteger (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 regionsG region_match
phase_fita list of phasesreturned 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.

EventTeachesWeightAlso acts in
rec_click, rec_dismiss (or the SDK's interaction report)what was taken or turned away on a slateclick +1.0, dismiss −1.0L tap_rate; F: a dismissed item stays out for exclude_dismissed_days (30)
rec_seenwhat was shownL
item_viewedweak interest+0.2G trending (cold start)
item_engagedreal interest+0.6 when 20 s or more, or 60 % scrolledG trending counts an engaged view three times; R engaged_rate; the user's format preference
item_completedstrong interest+1.5F: stays out for exclude_consumed_days (30, personalized); the user's format preference
item_pinned, item_unpinnedexplicit preference+2.0 / −1.0F: unpinned counts as dismissed
community_joined, community_leftmembership+1.5 / −1.0F: joined communities are never offered; L join_rate
community_post_created, community_reply, community_reactionactive membership+1.5 on the community, +0.5 on its tagsR activity_rate per community
community_post_viewedweak interest in a community+0.2
search_performed with clicked_item_idintent right now+0.8 on the clicked itemthe query arrives only as a hash
push_openedcontent that pulls people back+1.0R pullback_rate per item
product_viewedshop interest+0.5
order_placeda purchase+2.0 per productL purchase_rate; F: the repurchase window; co-purchase pairs for product neighbours
paywall_vieweda free user hit the wallF: not offered to that free user again for paywall_cooldown_days (7)
subscription_started, subscription_cancelleda tier change
event_rsvp, event_attendedevent interest+1.0 on event_rsvp
coach_product_shown, coach_product_tappedassistant outcomesreported, no holdout
consent_changed, user_deletedidentity lifecyclethe opt-out and deletion paths

What the nightly jobs learn from them#

ArtifactFromUsed by
item features: engaged_rate, pullback_rate, activity_rate, purchase_rateevents and impressionsR
item time profile: tap rate in each of the eight time buckets, smoothed toward the surface's own rateimpressions with their context and the taps on themG time_fit
the user's format preference and active time buckets (personalized only)item_engaged, item_completed, tapsG format_fit
purchases per user and productorder_placedF: the repurchase window; deleted with the user
embeddings and item neighboursitem text, embedded by a local multilingual model on Patterna's own hostG item_to_item, cold start
the vector index over the embeddings (HNSW, cosine)the same embeddings, indexed in the databaseG 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 rowG 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 orderG sequence_model
product co-purchase pairsorder_placed, or a shop's orders export read as aggregatesproduct neighbours
generator posteriors per surfacelogged contributions and tapsthe 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):

JSON
{
  "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.

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