Patterna Docs

Go live in six steps

From an empty workspace to the first readable ledger row.

Everyone 7 min read View as Markdown

Six steps take a surface from nothing to live with a holdout. The Overview screen keeps this checklist for your workspace and shows the state of each step. Each step links to the screen that completes it. Want the API calls first? Integrate with your AI agent lists them in order: keys, surface, catalog, one recommend call, interactions, trace. The six steps are Patterna's side. Your side (mapping consent, ids, deletion, hydrating items into your UI, reporting from the app) is the larger part of the work; plan weeks, not an afternoon.

The result of step six is the first readable ledger row: the difference between the two arms, with an interval, on a sample large enough to read.

Connect a data source#

What. Give Patterna the events it learns from. Pick one or more sources: your backend through the API, an SDK, PostHog, Google Analytics 4, Shopify or a quiz CSV.

Where. Connectors. Each card opens a setup guide with your webhook URL filled in. Create the scoped key for the connector on the same screen: ingest keys start with pk_i_, serve keys with pk_s_, admin keys with pk_a_. A key is shown once.

Done when. The card shows a last run, rows in and rows mapped, and no errors. "Test connection" passes.

See Backend API, PostHog and the other connector guides.

Import and enrich the catalog#

What. Send every item you may recommend: documents, products, communities, events, tips. Then let the enrichment job tag them against your taxonomy.

Where. Catalog. Items arrive through PUT /catalog/items (500 per call), an importer, or for a Shopify shop the storefront sync below. Enrichment runs hourly on items whose text changed.

Done when. The Catalog summary shows your item counts by kind, enrichment status done for items with text, and no health-claim tags waiting for approval on items you plan to show. See Fix a tag and approve a health claim.

curl
curl -sS -X PUT "https://patterna-api.orven.am/v1/catalog/items" \
  -H "Authorization: Bearer $PATTERNA_INGEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      {
        "external_id": "doc_1042",
        "kind": "document",
        "collection": "recipes",
        "title": "Magnesium-rich breakfast bowl",
        "locale": "nl",
        "status": "published",
        "premium": false,
        "attributes": { "format": "recipe", "reading_time_min": 4 }
      }
    ]
  }'

Products from your shop#

If your shop runs on Shopify, you do not need an Admin API token to get its products in. Every Shopify store publishes its catalog at https://<your-shop>/products.json; the storefront sync reads that public feed and upserts each product as an item of kind product: title, description (for tagging), the shop's tags, product type, price, sale price, stock and the first image. Send us your shop address; on our side it is one command, and it goes on your connector schedule (shopify_storefront, every_minutes; hourly if you ask for it), so the feed is re-read on that interval and each run shows on the Connectors card. Between two runs a product that sold out stays available: true until the next run, your own PUT /catalog/items upsert, or the Admin webhook products/update if you set that up; pick the interval for the freshness you need:

Shell
python -m patterna_jobs shopify-storefront --tenant your-tenant --shop-url https://your-shop.example

It prints how many products it read and wrote. A product that left the feed is archived, never deleted, because old slates may still refer to it. The item's external_id is the Shopify product id, the same key the Admin connector uses, so you can switch to the Admin API later without duplicates. If your app shows product cards from your own CMS, map your CMS product to that Shopify id. The feed stops at 10,000 products; a larger shop needs the Admin connector.

Order history, as aggregates only. To give "bought together" a head start, export your orders from Shopify (Orders → Export, CSV) and load the file:

Shell
python -m patterna_jobs orders-csv --tenant your-tenant --csv orders_export.csv

The reader keeps three columns, the order name, the line-item name and the SKU, and nothing about the customer: names, emails and addresses in the export are never read. It writes pairs of products bought together, with their lift over all orders in the file, and only pairs seen in at least two orders. The file is not copied anywhere. The nightly jobs blend these pairs into each product's neighbours. Ongoing orders still arrive as order_placed events (webhook or your backend).

Attributes worth sending#

The engine reads a few editorial attributes per item. Send them in attributes on PUT /catalog/items, from your shop sync or your CMS. None of them is about a person.

AttributeSend it forWhat it does
formatevery item: text, video, audio, recipe, workout, tool, productformat_fit matches it to what each user engages with
duration_min or reading_time_minarticles, videos, workoutstime_fit places short items in the day and long ones in the evening until the item has its own time profile
available (or in_stock)productsfalse is never served, in both arms
expires_atoffers, seasonal contentnever served after this time
starts_ateventsa past event is never served
repurchase_daysproductsa buyer is not offered the product again inside this window; small for consumables, large for durable goods
price_cents, currency, image_url, urlproductsshown on the panel's catalog cards; not ranked on
difficulty, authorwhere you have themstored with the item; not ranked on yet

The storefront sync fills available, the price fields, image_url, url and format: "product" for you. Everything the engine reads, and what it refuses: What the engine uses, and what it refuses.

Create a surface#

What. A surface is one placement in your app, such as Home articles. It has a key, the item kinds it may show, a slot count, an editorial list (fallback) and an exploration share. It needs an active flow version that says how to rank.

Where. Surfaces → new surface. Then Flows to check the first flow version.

Done when. The surface card exists with status shadow and an active flow version.

See Surfaces and flows.

Go shadow#

What. Your backend calls Patterna for the surface on every render, exactly as it will in production, and discards the answer: the app still shows your current list. Patterna logs every slate as an impression with arm none, so you can check latency, the fallback rate and the reasons before anyone sees a slate. Your feature flag controls what is shown, not whether Patterna is called: a backend that skips the call while the flag is off produces no shadow data. Nothing changes for users.

Where. Surfaces → the surface → lifecycle control. Status shadow.

Done when. Impressions arrive in the live request stream, the fallback rate is low, and p99 latency sits inside the budget (60 ms cached, 250 ms live). The fallback rate includes slates your client gave up on, because the SDKs report the abandonment. Check a few slates in the phone preview: each slot has a reason.

Add testers#

What. Show Patterna's slates to a small tester group. Check on a device that reasons render and that seen and click events arrive.

Where. Surfaces → the surface → testers list (Patterna user ids, or session ids in non_profiled mode), then status testers. Or let your backend mark its own staff with the X-Patterna-Tester: 1 header.

Done when. A tester's seen and click events show up in Audiences → trace lookup under the right request id, and the fallback rate stays low.

Go live with a holdout#

What. Set the surface to live and start a holdout on it. Half the users get Patterna's slate, half get the surface's editorial list. From here the ledger counts. Treatment slates are explored, control slates never are. Slates outside any holdout (arm none) are explored too, so exploration may already run before this step.

Where. Surfaces → status live. Proof → Experiments → start a holdout. See Run a holdout.

Done when. The experiment shows users in both arms and a projected "readable on" date. A row turns readable when each arm reaches the experiment's readable_sample, from the power calculation. See Read a ledger row.

Worked example: a content app#

A content app takes an article list on Home and a "more to read" block under each article live this way:

  1. In the panel, it creates home_articles and more_to_read in shadow, backfills the catalog from its backend, and deploys the backend with its own recommendations feature flag off for everyone. With the flag off the backend still calls Patterna on every render and shows its own list, so the shadow impressions exist. Nothing changes for users.
  2. It turns the flag on for its tester roster and moves both surfaces to testers. On iOS and Android it checks that reasons render, that seen and click reach the trace view by request id, and the fallback rate.
  3. It moves the surfaces to live with a 50/50 holdout and makes the flag public. The ledger reads from here.
  4. It watches the fallback rate per surface, the p99 of its own recommendations endpoint, and the opt-in rate of its personalization consent.

Roll back at any step#

Your side can always stop Patterna without waiting for us. Keep two switches in your backend: a feature flag (instant, no deploy) and an empty Patterna URL in your config (redeploy). Both serve your own lists. Inside Patterna, set the surface to off (requests then get 409 surface_off) or roll back the flow. See Roll back a flow.

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