Patterna Docs

Changelog

What changed in the engine, SDKs, jobs, contract and docs, newest first.

Everyone 17 min read View as Markdown

Newest first. Each entry says what changed and what it means for you.

2026-10-14: engine 0.3.3 (round 8)#

  • Hosting: Patterna moved to its own server on 15 October 2026 with its own domain. The API is https://api.patterna.nl/v1, the panel and these docs https://patterna.nl; staging is api-staging.patterna.nl and staging.patterna.nl. The previous *.orven.am names keep working (the API names as aliases, the panel names as redirects) so nothing built against them breaks; update your base URL when convenient.
  • Engine and contract 0.3.3 (engine_version on /health). New generator vector_retrieval: per request the engine builds one query vector from what the person looked at (the session's items in every mode, the recent positives in personalized mode, with session_items' weights) and reads the params.k nearest items by content (default 100, at most 500) from a vector index over the catalog's embeddings; they join the candidates with their cosine as the score, so an item nothing else proposed can be recommended. Reason similar. model_versions names the embedding model as embeddings.
  • Real-time session state: with Redis configured, a session's reports (POST /interactions) and signed rec_* / item_engaged events are kept for two hours with the session's running content vector, and the next slate of the session reflects them. Without Redis, or when it is down, the session is read from the log as before. Nothing new to send; context.session_items still comes first.
  • Flow editor: the generator with its k; the signals cards and docs describe it in English and Dutch.
  • New generator sequence_model: a small neural network trained every night on the order in which people open items, exported to ONNX and evaluated inside the engine (ONNX Runtime, no Python at serve time). It reads what this session looked at and, for personalized users, their last 50 items, and scores its best top_items (default 100) as the likely next ones, a shortlist that sits above every other item like user_bayes's. Every mode, reason similar. The jobs activate a model only when it beats the plain path rule on held-out data; without a model or without the ONNX Runtime library on the engine host the generator scores nothing and the flow serves as before. model_versions carries sequence.
  • Nothing new to send; the contract adds the kind to the generator enum only.
  • A storage failure on the key or session lookup now answers 503 unavailable with Retry-After: 1 instead of 401 unauthorized, and a serve request whose safety net could not read answers the same instead of 500. Read 503 as "retry or fall back", never as a bad key. The SDKs already fall back on any non-200 from recommend.
  • The engine's 60-second catalog refresh reads only what changed since the last one (new, changed, unpublished, deleted and re-tagged items), so a large catalog no longer costs a latency spike once a minute or gigabytes of memory; the first request after a start still builds the cache once. Several engine instances may now start with AUTO_MIGRATE=true at the same time (the migration takes a lock).

2026-10-14: engine 0.3.2, the audit round (r8-issues)#

  • Flow switch ranker.gap_blend (default true): a flow can turn the round-7 return-visit blend off, so a course or a playlist keeps its session weight on every visit. In the flow editor's ranker node and the signals grid; the gap bucket is still logged.
  • The gradient-boosted ranker reads the visit gap (ctx:gap:visit|hours|days|weeks|unknown) as a context feature on both sides, so it can tell a return visit from an in-session request. Replay numbers in the Gorse comparison, "Round 8".
  • ranker.kind: learned_linear now applies the nightly ranker's feature_weights (the item outcome rates fitted beside the generator multipliers), logged per candidate as learned_features; until now only the per-generator weights were read.
  • Settings: the Learning window (learn.history_days, 30 to 730 days; empty = deployment default) is edited on the Recommendations tab.
  • Example mode: the attribution card works on the example set. The demo seed and the weekly twin write generator_attribution rows flagged synthetic (engine migration 0020), and GET /proof/attribution?include_synthetic=true returns them, never mixed with real rows.
  • Imports keep an editor's label: the jobs' item sink preserves attributes.editor_label across runs, as the engine does on PUT /catalog/items.
  • Contract: contracts/schema.sql now carries the notifications, key usage and account tables (memberships, invites, password resets, sign-in attempts, mail outbox).

2026-10-13: SDKs 0.4.1#

  • SDKs 0.4.1 (Go, TypeScript, Python): GetItem(ctx, kind, id) / getItem(kind, id) / get_item(kind, id) reads one catalog item (GET /catalog/items/{external_id}?kind=, ingest key) with its attributes, tags, status and enrichment, so a backend can hydrate product cards from the catalog without a raw HTTP call. New sentinel ErrItemNotFound (item_not_found, 404, catalog) next to ErrUnknownItem (unknown_item, 422 from /interactions); the SDK docs had the two swapped. No wire changes: 0.4.0 keeps working. Install: npm [email protected], Go go get github.com/payam-h/patterna-sdk/[email protected] (tag go/v0.4.1), Python from tag v0.4.1.

2026-10-14: engine 0.3.2 (round 7)#

  • Engine and contract 0.3.2 (engine_version on /health). New generator user_to_user: what the person's nearest users (learned nightly from the same factorisation as user_bayes) did last, personalized mode only, weight off by default; its params recent_items and half_life_days. Reason people_like_you now also names it.
  • Return visits: the engine logs the bucketed time since the person's previous visit (gap in the impression context: visit, hours, days, weeks, unknown) and, when a flow carries session_items next to a long-term generator, fades the session signal and raises the long-term ones on a return after hours or days. The blended weights are logged with the slate; nothing new to send.
  • session_items defaults to sequence: both: the directed next-item shares where a session item has them, its neighbours otherwise; next and neighbours stay selectable.
  • Exclusions read the whole history: exclude_consumed_days goes up to 3650 (0 keeps the rule off), fed by a nightly user_consumed set. Cold items reach item_to_item through their nearest established items by content.
  • The learned gbdt ranker reorders the linear ranker's top 60 inside their score range instead of rescoring the whole pool; the ALS shortlist keeps every listed item above every unlisted one. On the public MovieLens benchmark the round-3 flow goes from 0.086 to 0.12 HitRate@10, the in-session flow to 0.21–0.22, return visits over an hour from 0.083 to 0.10 (offline, no live A/B yet).
  • Settings: learn.history_days (30–730) sets how far back the collaborative learners read.

2026-10-13: engine 0.3.1, SDKs 0.4.0 published#

  • Engine and contract 0.3.1 (engine_version on /health). The contract now describes 429 quota_exceeded with its details (quota, used, resets_at), makes occurred_at on interactions optional (the engine stores the receive time when absent), and says what the code does for weekday (UTC when absent), tenure_days (logged as a bucket, not ranked on) and member_of (every mode). The OpenAPI file is binding; if a page disagrees, the page is wrong.
  • SDKs 0.4.0 are installable: npm [email protected] (unscoped), Go go get github.com/payam-h/patterna-sdk/[email protected] (tag go/v0.4.0), Python from tag v0.4.0 (not on PyPI, by decision). They type weekday, subscription_tier, tenure_days, the catalog attributes and the new events.
  • Docs: one version scheme (engine 0.3.1, SDK 0.4.0); the Elvou import page is no longer public; the glossary gains round, tenant seed, Example mode, SERVE_BUDGET, device_resort, blocks_shown, pre-register, minimum detectable effect, shadow (status versus request flag), quota and baseline_rate; llms-full.txt has absolute links and a header listing its sections.

2026-10-13: engine 0.3.0 (round 5), the signals#

The engine now reads every input listed on What the engine uses, and what it refuses, and nothing else. Contract 0.3.0 (unchanged version; the additions are optional fields), engine migration 0012.

  • Request context. weekday (0 = Monday), subscription_tier and tenure_days next to hour_bucket, platform, region and session_items. Bad values answer 400. The context is logged with the impression for the learners and never stored on the user.
  • Catalog attributes the engine reads. format, duration_min or reading_time_min, difficulty, author, available or in_stock, expires_at, starts_at, repurchase_days, next to region and phase_fit.
  • Hard rules, the same in both arms. An item past expires_at, an event past starts_at and a product with available: false are never served (filters.hide_unavailable, on by default). min_slots counts on them.
  • Personal exclusions, relaxed when nothing else is left. Dismissed or unpinned items stay out for exclude_dismissed_days (30), completed items for exclude_consumed_days (30, personalized), an item that paywalled a free user for paywall_cooldown_days (7), a bought product inside repurchase_days (30; the item's own attribute wins). The fallback fill honours them too.
  • Generators time_fit and format_fit. The item's tap rate in the request's time bucket (else a duration rule), and the user's learned format preference (personalized). Both are silent: they move scores and never name a reason.
  • filters.relevance_floor (0 to 1, off by default): on an anchored request only items similar enough to the anchor count toward min_slots, judged the same in both arms, so a block with nothing related hides.
  • Truthful reasons. A slot names its winner only when the winner's own signal is positive; "trending" needs the surface's own log (before that it says "popular"), "people like you" a real persona, "new" an item under 30 days. Otherwise the new reason key recommended ("Aanbevolen" / "Recommended"). See Reasons.
  • pairs_with only where it is true: a recipe or a workout with a product, a community with a document, each with its own default text. Other cross-kind anchors say more_about_tag.
  • curated is a boost by rank (the first item +2), not a z-score; a kind whose catalog has one collection is no longer capped per collection.
  • Ranker features by name in gbdt: engaged_rate, pullback_rate, activity_rate, purchase_rate, the time-bucket tap rate, the hour, the weekend and the platform. Older models keep scoring.
  • Events community_post_created, community_reply, community_reaction feed a community's activity rate. The learner's weight for every event, including push_opened, community participation, search picks and orders per product, is now listed in Event types.
  • Products from a shop. The jobs sync a Shopify store's public products.json feed (text, tags, price, stock, image; no API token) and read a Shopify orders export into co-purchase pairs without reading any customer field. See Go live in six steps.
  • SDK 0.4.0 types the new context fields, the catalog attributes, builders for the new events, the new flow filters and generators, and recommended. SDK 0.3.0 still works with this engine: it sends the new context fields in context as given.
  • Docs: a new page, What the engine uses, and what it refuses; the event reference gets a weight per event (fixed in the learner, not a per-tenant setting); the flow config reference gets the ten generators and the new filters; trending is described as per surface (all locales together), which is what it always was.

2026-10-13: engine 0.3.0 (round 4) and SDKs 0.3.0#

  • Contract 0.3.0; engine_version on /health reports 0.3.0.
  • context.region: one of the region names you configure in Settings (coarse, such as a province; never coordinates; 400 unknown_region otherwise). Items carry a region too, and filters.region_boost (0.3 by default) lifts same-region items; other items are never hidden.
  • context.session_items and the session_items generator: neighbours of what the session just looked at, in every mode.
  • Trending counts a tap followed by an engaged view three times; engaged_rate per item.
  • ranker.bandit: a Thompson-sampling mixture over the generator weights, fitted nightly per surface; the drawn weights are logged, so propensities stay exact. Never in the control arm.
  • No exploration on slates with measured: false.
  • A monthly request quota per plan: a warning at 90 %, then 429 quota_exceeded.
  • SDKs 0.3.0 (Go, TypeScript, Python): control_item_ids, member_of, control_source, fill_count and fill, shown on every result (a shown: false slate is never abandoned), mode_required and not_shown errors, coach product events, patterna surface apply, ledger by block. Go: go get github.com/payam-h/patterna-sdk/[email protected]; TypeScript and Python install from the repository at tag v0.3.0.

2026-10-13: engine 0.3 (contract 0.2.0)#

Released. The contract is version 0.2.0; engine_version on /health reports that API version.

  • Anchored control: control serves, in order, your context.control_item_ids (up to 50, no surface setting needed), else the surface's fallback.by_anchor list for the anchor (an object from anchor item id to item ids), else the surface's list. The request's rules apply (the anchor, exclude_item_ids, joined communities, kinds, premium, locale); newest items fill the rest. The answer says which in control_source (request, by_anchor, surface).
  • not_shown slates count in their arm as served with zero success (intention to treat), in both arms; a shown: false answer carries no items, and abandon on it answers 409 not_shown. min_slots is counted before the arm, from the items that pass the request's rules.
  • Fallback fill: a treatment slate that comes out short is filled from the editorial list, like control. fill_count is on the answer and the impression; each filled slot carries fill: true and propensity 1 in the trace.
  • stratify_by: "persona" is applied: the arm is drawn within each persona; users without one form their own stratum. Optional strata_shares per stratum.
  • readable follows the power calculation (readable_sample per arm) instead of a fixed 200 units.
  • Per-block ledger rows (by=block or block=) when interactions carry block.
  • model_versions on every impression, in trace and export.
  • Learning: the slate learners train on arms treatment and none, never control, from the interactions you report (joined to the slate by request_id) and signed item_completed events for served items; taps and dismissals on served slates also reach the per-user and per-item models. Filled slots are never ranking evidence.
  • Opt-out (PUT /users/{id}/mode with non_profiled, or consent_changed): the request deletes the user row, keeps only a salted hash as the opt-out marker and files a deletion request holding only that hash; the hourly job unlinks the user's impressions (interactions stay with their slate) and deletes events, assignments and the shortlist. Serve treats a personalized or cohort request for that id as non_profiled and logs it without the id (audited once a day as serve.optout_downgrade) until the mode is set again.
  • PUT /users/{id}/profile without mode: an unknown user gets 422 mode_required, an opted-out user 422 mode_conflict; a known user keeps the current mode.
  • context.member_of: communities the user already joined are excluded in every mode.
  • Events coach_product_shown and coach_product_tapped with product_id, conversation_id and position.
  • pairs_with stays the reason for any cross-kind anchor; limiting it to a recipe anchor and a product is not built. Its default text names a recipe, so override it with a reason template. (Replaced in rounds 4 and 5: pairs_with is now said only per pairing.)
  • phase_fit is sent only when the tenant turns phase_tags on (off by default).
  • Unknown routes answer 404 JSON with code: not_found.
  • engine quickstart: runs the migrations, creates a demo tenant with 40 example items, a live home surface and one key per scope, and prints the first three calls.
  • SDK 0.2.0 has no typed fields yet for control_item_ids, member_of, fill_count and control_source.
  • Docs: every page describes engine 0.3 as live; the "engine 0.3" markers for coming features are gone.

2026-10-13: engine 0.2 and SDKs 0.2.0#

Released (release 7a7683e).

Engine#

  • Deadline: header X-Deadline-Ms or body deadline_ms (10 to 10000); the engine's own budget is 120 ms by default. See Timeouts and the ledger.
  • POST /recommendations/{request_id}/abandon answers 200 and flags the slate client_abandoned; it stays in its arm and counts in the fallback rate.
  • A UUID you send as X-Request-Id becomes the slate's request_id.
  • assignment_unit (64 hex characters, non_profiled only): one holdout arm per person. See Identity across connectors.
  • context.measured; context.anchor_item_id resolved across kinds with the reason key pairs_with; more_about_tag split from matches_goals.
  • Reason templates keyed by reason key or generator kind; placeholders {anchor_title}, {tag}, {segment}; bad templates answer 400 invalid_reason_template. See Reason keys.
  • Exploration also runs on slates outside a holdout (arm none); control is never explored.
  • X-Patterna-Tester: 1 puts a request on a surface's testers path (422 bad_tester_id otherwise).
  • Surface settings min_slots (the slate comes back with shown: false) and device_resort (422 displayed_position_required).
  • Interactions: every item must be in the slate (422 unknown_item), kind and served position come from the logged slate, optional block.
  • Events judged one by one with a code per rejection (pii_rejected, invalid_signature, no_signing_secret, …); the batch still answers 202.
  • Catalog: GET /catalog/items?changed_since=, DELETE /catalog/items/{external_id} (200 with counts), locales per item.
  • Signing secrets are pss_ strings used as is; POST /settings/signing-secret creates the first one. A secret from before this release has no pss_ prefix: rotate once.
  • Webhooks: PostHog with X-Patterna-Token; Shopify at /webhooks/shopify/{tenant}, HMAC only (401 webhook_not_configured, 401 bad_signature).
  • Metrics second_item_rate, join_rate (community surfaces only, 400 metric_not_applicable) and engaged_after_tap; surface metrics gain fallback_rate, client_abandoned_rate, unmeasured_share.

SDKs (Go, TypeScript, Python 0.2.0)#

  • Abandon / AbandonSync; RecommendWithFallback reports abandonment itself.
  • A 120 ms deadline sent inside the 150 ms timeout (Config.RecommendDeadline in Go).
  • RecommendMany for several surfaces on one screen; WithTester(); Measured and AssignmentUnit on the request.
  • Catalog: ListItems, ItemsChangedSince, DeleteItem; CreateSigningSecret.
  • An admin package and a patterna CLI for surfaces, flows, experiments and activation, so flow configs can live in your repository.
  • Signing keys the HMAC with the secret string (pss_ secrets).
  • Published from payam-h/patterna-sdk: Go tag go/v0.2.0 (go get github.com/payam-h/patterna-sdk/[email protected]). TypeScript (patterna-sdk) is not on npm yet and Python (patterna-sdk) is not on PyPI; both install from that repository at tag v0.2.0.

Docs#

  • Every page now describes engine 0.2 as live; the "engine 0.2 is next" markers are gone. What engine 0.3 adds is marked "engine 0.3".
  • One budget everywhere: the engine's default is 120 ms.
  • Corrected: exploration outside a holdout, reason placeholders, trending per surface and locale, the PostHog destination status, connector-minted ids (keyed under a secret Patterna holds), the coach event fields, SDK installation.

2026-10-13: earlier the same day#

Engine#

  • Built and smoke-tested locally against a real customer catalog export (210 items): catalog upsert, profiles, recommendations in all three consent modes, interactions, experiments with sticky arms, the ledger, trace, export and deletion.
  • Exploration with sequential Plackett-Luce sampling and exact propensities. Caps (per collection, premium) and the diversity penalty (filters.diversity) are applied inside the sampler.
  • Rankers: linear, learned_linear and gbdt; lambdamart is refused.
  • Ledger intervals clustered on the randomised unit, with an SRM check. The primary metric is tap_rate.
  • Deletion is synchronous: DELETE /users/{id} removes the rows before it answers.
  • Signing-secret rotation: POST /settings/signing-secret/rotate.
  • POST /experiments/{id}/end.
  • MFA enrolment and confirmation, panel users with roles and invite tokens.
  • Per-key token-bucket rate limits: serve 200 per second (burst 400), ingest 50 per second (burst 100).
  • Optional Redis as a shared cache across engine instances (REDIS_URL); without it each instance caches on its own.
  • A load-test harness for the p99 budget (cmd/loadtest).
  • Not in this release: the S3 export target (exports go to a local directory) and the LightGBM ranker.

SDKs#

  • TypeScript SDK patterna-sdk: serve and ingest client, typed errors, 150 ms / 5 s budgets, one retry for idempotent calls, consent gate, opaque-user-id checks, withFallback, offline EventQueue, HMAC signing. Not published to npm yet.
  • Go SDK github.com/payam-h/patterna-sdk/go: the same, plus a circuit breaker per scope and fire-and-forget interaction reports drained on Close.

Jobs#

  • LLM layer: Anthropic and OpenRouter behind one interface, pinned models per job, a daily cost ceiling per tenant, a content-hash cache and a 30-day audit.
  • Enrichment: tagging into the tenant taxonomy with health-claim review, embeddings, the gold set, and reason-template drafts saved as inactive flow versions.
  • Learning: per-persona and per-user models, item neighbours, the learned linear ranker and the gradient-boosted ranker, written nightly.
  • Connectors: PostHog, GA4 via BigQuery, Shopify, quiz CSV and a customer database import.
  • Maintenance: deletions, retention, the daily LLM cost ledger.
  • Scheduler with /healthz, graceful shutdown, and a per-job lock so two workers never run the same job.

Contract#

  • contracts/openapi.yaml 0.1.0 fixed without semantic change: descriptions that broke parsing, and nullable: true replaced by OpenAPI 3.1 type: [T, 'null']. Regenerate anything built from the old file.
  • The raw_payloads table was dropped. Ingest stores no raw payloads; only the allowed fields of each event are kept.

Docs#

  • The Docs hub in the panel: concepts, guides, connector setup, API pages and reference, in English and Dutch.
  • One API host everywhere: https://api.patterna.nl/v1.
  • New pages: Flow config reference, Timeouts and the ledger, Identity across connectors.
  • Corrected: key prefixes (pk_s_, pk_i_, pk_a_), mode_conflict is 422, the full error-code list, the signing model and the secret, positions start at 0, reason overrides are keyed by reason key, deletion is synchronous, the user endpoints take an ingest key.

2026-10-11#

Connectors#

  • PostHog mapping: content_viewed, content_engaged, content_scroll_depth, community_joined and community_left are mapped; cycle, breathing, location and screen events and all person properties are not ingested. See Event types.
esc
  • Type to search every page, heading and code sample.