For the complete documentation index, see llms.txt. This page is also available as Markdown.

What's actually in your data

How each source system maps into Kula Intelligence's canonical model — what we can see, what we can't, and how the systems link to each other.

Every studio's data arrives from a different system — Mindbody, Wix, GymMaster, Stripe, ClassPass — and no two of them expose the same things. One gives us door swipes, one gives us none. One gives us every recurring charge, one gives us only the original purchase. One tells us which member attended, one gives us only a name typed on a roster.

Kula Intelligence normalises all of them into a single canonical model so a question like "who's at risk this month?" works regardless of which system a studio runs on. But normalising the shape can't invent the substance. GymMaster has no API for financial data, so no amount of mapping produces revenue — and its door access arrives only by webhook, so nothing exists before the day we connected.

These pages are the honest map of that: one per source system, saying exactly which canonical tables it fills, at what grain, over what history — and, just as importantly, which tables it leaves empty.

Who these pages are for

AI assistants connected to a studio. Read the page for whichever sources the studio has connected before reasoning about coverage. It will stop you assuming a table has data it can't have, and stop you telling an operator their data is "missing" when the vendor never exposed it in the first place.

Humans are welcome too — engineers maintaining an ingestor, and operators who want to know why one number exists and another doesn't.

The pages

Source
Role in a studio
Map

GymMaster

Booking + membership system of record

Mindbody (MBO)

Booking + membership + POS system of record

Wix

Booking + pricing plans + payments (all-in-one)

Stripe

Payments + subscriptions

ClassPass

Aggregator revenue (CSV export only)

Xero

General ledger / accounting

QuickBooks

General ledger / accounting

One more map covers something that isn't a source at all:

Layer
Role in a studio
Map

The relationship graph

Derived: who is connected to whom, how strongly, and who is structurally exposed

It is built from the canonical tables the connectors above already fill, so it inherits every one of their gaps — and adds its own. Read it before answering anything about coaches, peer community, resilience, connection quadrants or the attention queue.

Meta, Google Analytics 4 and generic CSV are connected sources too; their ontology maps are the next to be written. Until then, treat their coverage as undocumented rather than assumed — check get_semantic_catalogue and the marketing.* tables directly.

Start here, every time

Two calls, in this order, before answering anything about coverage, revenue, attendance, door access or membership history:

  1. get_system_status — which sources this studio has connected, and whether each is current, stale or failing.

  2. get_doc('data/<source>') — the map for each connected source. data/gymmaster, data/mindbody, data/wix, data/stripe, data/classpass, data/xero, data/quickbooks. Add data/graph for any question about coaches, peer community, resilience, quadrants or the attention queue.

get_semantic_catalogue also carries a condensed version of each map's key facts under source_system_gotchas, with a pointer back to the full page. The maps are always the fuller answer.

Prefer the purpose-built tool

Every map ends with a Recipes section naming the fastest route to each common question for that source. The general rule holds everywhere: the tools encode rules that ad-hoc SQL gets wrong — pause-awareness, the guarded views, studio-local timezone bucketing, cancelled-session exclusions, identity de-duplication.

Question
Tool

Who's lapsing / at risk

list_at_risk_members (not list_quadrant_members — quadrants are graph enrichment, not recency)

A name or email → canonical id

entity_lookup

One member's relationships and risk

get_member_context

One member's money

get_member_payments

Schedule performance

get_class_utilisation, then get_time_slot_detail

One instructor

get_teacher_performance

Cohort survival

get_retention_curve

Acquisition cost

get_cac_by_cohort

Is the data current

get_system_status

Anything else

get_semantic_cataloguelist_tables / get_table_schemaexecute_query

When you do write SQL, prefer the *_guarded views — they mask restricted columns by the caller's scope and apply source-specific dedup filters that the base tables don't.

And count distinct humans with people.member_distinct, never people.member. The latter is one row per source system, so any studio with more than one connector over-counts.

How to read one of these pages

Each map has the same six sections, in the same order, plus Recipes at the end.

1. What this system is. Its role, and whether it is the system of record for a studio (bookings and memberships live there) or a satellite (it knows about money, or marketing, but not the studio's day-to-day).

2. Coverage at a glance. A table: vendor entity → canonical table → grain → how far back history goes → how fresh it stays. If a row isn't in that table, we don't have it.

3. What we cannot get. The blunt list. Each item says why — vendor has no API, endpoint is behind a plan we don't have, the data is destroyed at the source, or it's simply not built yet. "Not built yet" and "not possible" are very different answers to an operator, so the list keeps them apart.

4. Identity and join keys. How a member/class/payment in this system is identified, and how that id joins to the canonical tables. This is where most wrong answers come from — for example, GymMaster's live attendance feed carries no member id at all.

5. Counting traps. Places where the obvious query gives a wrong number: double counts, splits, backfill twins, revenue that looks like a single sale but is 59 charges.

6. Questions this source can and can't answer. Concrete phrasing. If you're about to tell an operator "you have no X", check here first — the honest answer is usually "your booking system doesn't expose X" rather than "your data is incomplete".

The canonical model everything lands in

Whatever the source, data ends up in the same per-studio Postgres schemas. The shared vocabulary:

Schema
Holds
Key tables

people

Humans and places

member, staff, location, identity_link

commerce

Money and what was sold

sale, payment, refund, plan, product

bookings

What happened in the studio

class_session, attendance, appointment, facility_entry

accounting

The general ledger

account, journal, entry

marketing

Spend, web, social

campaign, spend_daily, web_traffic_daily

graph

Derived relationships and risk signals

node, edge, risk_signal

ingest

The verbatim vendor payloads

raw_record

Two distinctions that matter constantly:

  • Sale vs payment. commerce.sale is what was sold (the invoice line). commerce.payment is money that moved (the charge). They are separate axes, joined through commerce.sale_payment. A subscription produces one sale per billing period and one payment per successful charge — and either can exist without the other (an unpaid invoice; a charge we can't attribute).

  • Refunds are their own rows. commerce.refund and commerce.payment_refund, never negative sales. Net revenue is SUM(sale.total) − SUM(refund.amount).

Every row carries its lineage

Four columns appear on essentially every ingested row, and they are how you reason about coverage at query time rather than trusting these docs blindly:

Column
Means

source

Reverse-DNS system id — com.gymmaster, com.mindbody, com.wix, com.stripe, com.classpass, com.xero, com.quickbooks

source_external_id

The vendor's own id for this thing

source_modified_at

The vendor's last-modified stamp — freshness, and the last-write-wins guard

source_is_backfill

True for rows loaded from a historical import rather than the live feed

Run something like that before asserting a source is missing. A studio may have connected a source last week and only have a week of it.

A studio running Mindbody for bookings and Stripe for payments has two people.member rows for the same human — one per source. That is deliberate: each ingestor owns its own rows and never guesses about another's.

The stitching happens in three places:

people.identity_link maps several source-side members onto one canonical_member_id, resolved by email match (high confidence), phone match (medium), or an operator confirming it manually. Ask "all activity for this human" through the canonical id, not through one source's member row.

commerce.payment_attribution is the read view that does this for money: it resolves the payer to the canonical member and stitches each payment to the sale it funded. Use it instead of grouping commerce.payment by payer_member_id — gateways mint a fresh customer id per checkout, so the raw grouping over-counts payers and under-counts per-person spend.

The graph layer (graph.node / graph.edge) is enrichment on top of that — affinity, staff concentration, risk signals. It is not a substitute for the canonical tables, and it is rebuilt from them.

One consequence worth internalising: a studio with a booking system and a separate payment system will not have perfect attribution. Some payments resolve to a member, some don't. commerce.payment_attribution.member_id being NULL is a normal state, not a bug — it means the payer wasn't matchable to a known member.

When a source has a gap

Say so plainly, and say whose gap it is. The three honest answers, in descending order of usefulness to an operator:

  1. "Your booking system doesn't expose that." Nothing to fix. Offer the nearest available proxy and be explicit that it's a proxy.

  2. "That's exposed, but we haven't built it yet." A real roadmap item.

  3. "That should be here and isn't." A genuine data issue — check get_system_status for connector health before concluding this.

What to avoid is the fourth answer, the one these pages exist to prevent: silently assuming the data should be there, computing a number from what little arrived, and presenting it as complete.

Last updated

Was this helpful?