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
One more map covers something that isn't a source at all:
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:
get_system_status— which sources this studio has connected, and whether each is current, stale or failing.get_doc('data/<source>')— the map for each connected source.data/gymmaster,data/mindbody,data/wix,data/stripe,data/classpass,data/xero,data/quickbooks. Adddata/graphfor 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.
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_catalogue → list_tables / get_table_schema → execute_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:
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.saleis what was sold (the invoice line).commerce.paymentis money that moved (the charge). They are separate axes, joined throughcommerce.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.refundandcommerce.payment_refund, never negative sales. Net revenue isSUM(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:
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.
How the systems link to each other
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:
"Your booking system doesn't expose that." Nothing to fix. Offer the nearest available proxy and be explicit that it's a proxy.
"That's exposed, but we haven't built it yet." A real roadmap item.
"That should be here and isn't." A genuine data issue — check
get_system_statusfor 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?