Changelog
What changed in the engine, SDKs, jobs, contract and docs, newest first.
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 docshttps://patterna.nl; staging isapi-staging.patterna.nlandstaging.patterna.nl. The previous*.orven.amnames 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_versionon/health). New generatorvector_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, withsession_items' weights) and reads theparams.knearest 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. Reasonsimilar.model_versionsnames the embedding model asembeddings. - Real-time session state: with Redis configured, a session's reports (
POST /interactions) and signedrec_*/item_engagedevents 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_itemsstill 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 besttop_items(default 100) as the likely next ones, a shortlist that sits above every other item likeuser_bayes's. Every mode, reasonsimilar. 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_versionscarriessequence. - 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 unavailablewithRetry-After: 1instead of401 unauthorized, and a serve request whose safety net could not read answers the same instead of500. Read 503 as "retry or fall back", never as a bad key. The SDKs already fall back on any non-200 fromrecommend. - 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=trueat 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_linearnow applies the nightly ranker'sfeature_weights(the item outcome rates fitted beside the generator multipliers), logged per candidate aslearned_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_attributionrows flagged synthetic (engine migration 0020), andGET /proof/attribution?include_synthetic=truereturns them, never mixed with real rows. - Imports keep an editor's label: the jobs' item sink preserves
attributes.editor_labelacross runs, as the engine does onPUT /catalog/items. - Contract:
contracts/schema.sqlnow 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 itsattributes,tags,statusandenrichment, so a backend can hydrate product cards from the catalog without a raw HTTP call. New sentinelErrItemNotFound(item_not_found, 404, catalog) next toErrUnknownItem(unknown_item, 422 from/interactions); the SDK docs had the two swapped. No wire changes: 0.4.0 keeps working. Install: npm[email protected], Gogo get github.com/payam-h/patterna-sdk/[email protected](taggo/v0.4.1), Python from tagv0.4.1.
2026-10-14: engine 0.3.2 (round 7)#
- Engine and contract 0.3.2 (
engine_versionon/health). New generatoruser_to_user: what the person's nearest users (learned nightly from the same factorisation asuser_bayes) did last, personalized mode only, weight off by default; its paramsrecent_itemsandhalf_life_days. Reasonpeople_like_younow also names it. - Return visits: the engine logs the bucketed time since the person's previous visit (
gapin the impression context:visit,hours,days,weeks,unknown) and, when a flow carriessession_itemsnext 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_itemsdefaults tosequence: both: the directed next-item shares where a session item has them, its neighbours otherwise;nextandneighboursstay selectable.- Exclusions read the whole history:
exclude_consumed_daysgoes up to 3650 (0 keeps the rule off), fed by a nightlyuser_consumedset. Cold items reachitem_to_itemthrough 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_versionon/health). The contract now describes429 quota_exceededwith itsdetails(quota,used,resets_at), makesoccurred_aton interactions optional (the engine stores the receive time when absent), and says what the code does forweekday(UTC when absent),tenure_days(logged as a bucket, not ranked on) andmember_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), Gogo get github.com/payam-h/patterna-sdk/[email protected](taggo/v0.4.0), Python from tagv0.4.0(not on PyPI, by decision). They typeweekday,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 andbaseline_rate;llms-full.txthas 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_tierandtenure_daysnext tohour_bucket,platform,regionandsession_items. Bad values answer400. The context is logged with the impression for the learners and never stored on the user. - Catalog attributes the engine reads.
format,duration_minorreading_time_min,difficulty,author,availableorin_stock,expires_at,starts_at,repurchase_days, next toregionandphase_fit. - Hard rules, the same in both arms. An item past
expires_at, an event paststarts_atand a product withavailable: falseare never served (filters.hide_unavailable, on by default).min_slotscounts on them. - Personal exclusions, relaxed when nothing else is left. Dismissed or unpinned items stay out for
exclude_dismissed_days(30), completed items forexclude_consumed_days(30, personalized), an item that paywalled a free user forpaywall_cooldown_days(7), a bought product insiderepurchase_days(30; the item's own attribute wins). The fallback fill honours them too. - Generators
time_fitandformat_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 towardmin_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_withonly 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 saymore_about_tag.curatedis 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_reactionfeed a community's activity rate. The learner's weight for every event, includingpush_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.jsonfeed (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 incontextas 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_versionon/healthreports0.3.0. context.region: one of the region names you configure in Settings (coarse, such as a province; never coordinates;400 unknown_regionotherwise). Items carry aregiontoo, andfilters.region_boost(0.3 by default) lifts same-region items; other items are never hidden.context.session_itemsand thesession_itemsgenerator: neighbours of what the session just looked at, in every mode.- Trending counts a tap followed by an engaged view three times;
engaged_rateper 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_countandfill,shownon every result (ashown: falseslate is never abandoned),mode_requiredandnot_shownerrors, 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 tagv0.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'sfallback.by_anchorlist 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 incontrol_source(request,by_anchor,surface). not_shownslates count in their arm as served with zero success (intention to treat), in both arms; ashown: falseanswer carries no items, and abandon on it answers409 not_shown.min_slotsis 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_countis on the answer and the impression; each filled slot carriesfill: trueand propensity 1 in the trace. stratify_by: "persona"is applied: the arm is drawn within each persona; users without one form their own stratum. Optionalstrata_sharesper stratum.readablefollows the power calculation (readable_sampleper arm) instead of a fixed 200 units.- Per-block ledger rows (
by=blockorblock=) when interactions carryblock. model_versionson every impression, in trace and export.- Learning: the slate learners train on arms
treatmentandnone, never control, from the interactions you report (joined to the slate byrequest_id) and signeditem_completedevents 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}/modewithnon_profiled, orconsent_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 asnon_profiledand logs it without the id (audited once a day asserve.optout_downgrade) until the mode is set again. PUT /users/{id}/profilewithoutmode: an unknown user gets422 mode_required, an opted-out user422 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_shownandcoach_product_tappedwithproduct_id,conversation_idandposition. pairs_withstays 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_withis now said only per pairing.)phase_fitis sent only when the tenant turnsphase_tagson (off by default).- Unknown routes answer
404JSON withcode: not_found. engine quickstart: runs the migrations, creates ademotenant with 40 example items, a livehomesurface 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_countandcontrol_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-Msor bodydeadline_ms(10 to 10000); the engine's own budget is 120 ms by default. See Timeouts and the ledger. POST /recommendations/{request_id}/abandonanswers200and flags the slateclient_abandoned; it stays in its arm and counts in the fallback rate.- A UUID you send as
X-Request-Idbecomes the slate'srequest_id. assignment_unit(64 hex characters, non_profiled only): one holdout arm per person. See Identity across connectors.context.measured;context.anchor_item_idresolved across kinds with the reason keypairs_with;more_about_tagsplit frommatches_goals.- Reason templates keyed by reason key or generator kind; placeholders
{anchor_title},{tag},{segment}; bad templates answer400 invalid_reason_template. See Reason keys. - Exploration also runs on slates outside a holdout (arm
none); control is never explored. X-Patterna-Tester: 1puts a request on a surface's testers path (422 bad_tester_idotherwise).- Surface settings
min_slots(the slate comes back withshown: false) anddevice_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, optionalblock. - Events judged one by one with a
codeper rejection (pii_rejected,invalid_signature,no_signing_secret, …); the batch still answers202. - Catalog:
GET /catalog/items?changed_since=,DELETE /catalog/items/{external_id}(200with counts),localesper item. - Signing secrets are
pss_strings used as is;POST /settings/signing-secretcreates the first one. A secret from before this release has nopss_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) andengaged_after_tap; surface metrics gainfallback_rate,client_abandoned_rate,unmeasured_share.
SDKs (Go, TypeScript, Python 0.2.0)#
Abandon/AbandonSync;RecommendWithFallbackreports abandonment itself.- A 120 ms deadline sent inside the 150 ms timeout (
Config.RecommendDeadlinein Go). RecommendManyfor several surfaces on one screen;WithTester();MeasuredandAssignmentUniton the request.- Catalog:
ListItems,ItemsChangedSince,DeleteItem;CreateSigningSecret. - An
adminpackage and apatternaCLI 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 taggo/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 tagv0.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,
trendingper 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_linearandgbdt;lambdamartis 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, offlineEventQueue, 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 onClose.
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.yaml0.1.0 fixed without semantic change: descriptions that broke parsing, andnullable: truereplaced by OpenAPI 3.1type: [T, 'null']. Regenerate anything built from the old file.- The
raw_payloadstable 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_conflictis422, 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_joinedandcommunity_leftare mapped; cycle, breathing, location and screen events and all person properties are not ingested. See Event types.