Data model

Cohorts

Static and dynamic user segments — how cohorts are computed, when they refresh, and how Switch, Config, and Pulse target users by cohort.

A cohort is a named set of users that match a query you define. Cohorts power most of what makes Sankofa feel personalised — flag rollouts targeted to "Pro plan customers in the EU," configs overridden for "trial users in their second week," surveys triggered for "users who hit a 5xx on checkout in the last 24 hours."

Cohorts come in two flavours — static and dynamic — and they refresh on a schedule you can reason about.

Static vs dynamic cohorts

StaticDynamic
Membership ruleA list of distinct_ids you supplyA query against events / properties / people fields
Updated whenOnly when you replace the listOn the cohort's refresh schedule (default every 5 min)
Use forBeta lists, internal staff, manually-curated test cohorts"Power users", "free tier", "EU + Pro plan", anything rule-based
Maximum size100k membersUnlimited
Created viaDashboard upload (CSV) or Cohorts APIDashboard cohort builder or Cohorts API

The same cohort interface targets both — Switch, Config, and Pulse don't care whether a cohort is static or dynamic.

Building a dynamic cohort

The cohort builder in People → Cohorts → New cohort combines:

  • Property filterscountry = "GH", plan = "pro", signup_date > "2025-01-01". These match against the user's most recent People profile (set via setPerson / peopleSet).
  • Behavior filters — "performed checkout_completed at least 3 times in the last 30 days," "did NOT perform signup_completed in the last 7 days," etc.
  • Cohort filters — "is in cohort X but not in cohort Y" — for layering.

The builder generates a query that the engine runs on a refresh schedule. The result is a materialized list of distinct_ids stored against the cohort, ready for fast O(1) lookups during decision handshakes.

Refresh cadence

Dynamic cohorts refresh in the background, not in real time:

TierRefresh interval
Hobby30 minutes
Pro5 minutes
Growth1 minute
Enterprise30 seconds, with optional event-driven recompute

The engine doesn't refresh every cohort on every tick — it stagger-refreshes so the work is spread evenly across the project. A cohort scheduled at 5 minutes will refresh somewhere within a 5-minute window.

How decisions consume cohorts

When the engine evaluates a flag, config item, or survey trigger for a user, it does:

  1. Identity lookup

    Resolve the user's current distinct_id (post-alias, post-identify).

  2. Cohort membership lookup

    Look up which cohorts the distinct_id is currently in. This is an O(1) read against the materialized cohort table — no event-store scan involved.

  3. Decision evaluation

    For each rule (e.g. "Pro plan customers get rollout 100%"), check whether the user's cohorts match. The first matching rule wins.

This is why cohort-targeted decisions are fast — the heavy lifting (running the cohort query) happens out-of-band on the refresh schedule, not in the request path.

Identity stitching and cohorts

When a user goes from anonymous to identified, the engine forwards the anon_id on the next decision handshake. If the now-identified user matches a cohort that wasn't matched anonymously (because membership requires an email property, say), the next handshake routes the cohort-targeted decision to them.

This means: cohort-targeted experiments are continuous across the login boundary. A user in the "got the new checkout variant" arm before sign-in stays in that arm after sign-in.

Cohort lifecycle

StateWhat it means
buildingFirst materialization is running. Decisions can't query the cohort yet.
activeMaterialized and queryable. Refreshes on schedule.
pausedThe query is intact but won't refresh. Membership is frozen at last refresh.
archivedSoft-deleted. Not visible in the UI. Decisions referencing it fall back to the rule's default.

Archive cohorts you no longer use rather than deleting them — every decision that ever referenced an archived cohort still resolves cleanly to the default.

Practical patterns

What's next

Edit this page on GitHub