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

Schema reference

The tables and columns behind the freeform query tools — schema families, the canonical column pattern, and how PII redaction works per permission level.

This is the data model the freeform query tools (execute_query, build_query) read. It documents the schema families and the shape of the core tables. For exact, live column lists, use list_tables and get_table_schema against your own studio — they are the source of truth, and this page mirrors them.

Every studio's database carries the same shape, populated from whichever connectors it has. A table is empty, not absent, if you haven't connected a source that feeds it.

How the model is organised

Tables live under a small set of schemas, all on the default search_path:

Schema
Holds

people

Members, staff, companies, locations, identity links

commerce

Sales, payments, refunds, plans, products

bookings

Class sessions, attendance, appointments, facility entries

accounting

Chart of accounts, journals, entries, invoice lines

marketing

Ad accounts, campaigns, daily spend, attribution links

comms

Messages and attachments

graph

Relationship edges/nodes, affinity, member & location connection strength (resilience), connection-strength quadrants, attention signals (derived)

compliance

Compliance records

ingest

raw_record — the verbatim landing zone before transform

mcp

The AI-collaboration layer (skills, memory, knowledge, artifacts, business context) and ingest bookkeeping

The canonical column pattern

Most fact tables are source-stamped and event-sourced, so they share a common envelope of columns:

Column
Meaning

id

Canonical UUID for the row.

source

The connector that produced it (e.g. com.stripe, com.wix).

source_external_id

The provider's own id for the record.

site_id

The location/sub-tenant within the studio, if any.

occurred_at

When the event actually happened (not when we received it).

source_extras

jsonb — provider fields we kept verbatim but didn't promote to columns.

source_modified_at

When the provider last changed it.

created_at / updated_at

When Kula first/last wrote the row.

ingest_event_id

The idempotency key from the ingest envelope.

This means you can always trace a canonical row back to the exact provider record it came from.

PII redaction and the _guarded companions

Many base tables have a _guarded companion (for example, commerce.sale and commerce.sale_guarded). The guarded view applies redaction based on your token's permission level — hiding member names, contact details, or payment fragments you aren't entitled to. Redaction happens server-side, so a lower level can't be tricked into revealing more.

Prefer the guarded companion (or the purpose-built read tools like get_member_payments and get_member_plan_status, which honour scoping automatically) when you want redaction handled for you.

Hide the books. A connector can also be set to withhold the whole accounting schema — chart of accounts, journals, entries, invoice lines — regardless of its permission level. A query against any accounting table (including its _guarded view) then returns a clear denial, while commerce (sales, payments, refunds) stays readable. This is the orthogonal "hide the books" restriction — see Permission levels.

Core tables

people.member

One row per member. Key columns:

id, first_name, last_name, display_name, email, mobile_phone, status, membership_type, plan_name, current_plan_id, is_booking_suspended, suspension_start_date, suspension_end_date, member_since, first_session_date, home_location_id, home_location_name — plus the canonical envelope columns.

Names, email, mobile_phone, and the suspension dates redact at the analytics level; operations and above see them. See permission levels.

people.member_status_event

Append-only log of member status and membership/plan transitions. No booking or payment system emits a status-change event — each exposes only a current-state field — so Kula synthesises the event by detecting the change itself. This is the only place that answers "when did this member churn" or "was that inactive flag the vendor's or ours". Key columns:

id, member_id, source, source_external_id, dimension (status | membership_type | plan | suspension), from_value, to_value, occurred_at, detected_at, is_baseline, reason, reason_source (rule | vendor | operator | baseline | unknown), actor, detail.

Two rules, or every number is wrong. (1) The log is live-forward only — it starts when capture began on the studio and cannot be backfilled — and each member has one is_baseline = true origin row per dimension recording what we first saw. Filter NOT is_baseline when counting changes. (2) It is one row per people.member row, not per human: group by COALESCE(canonical_member_id, id) or cross-source duplicates count several times. list_member_status_changes does both for you.

commerce.sale

One row per sale line (refunds are negative companions in commerce.refund). Key columns:

id, occurred_at, amount, tax, total, discount, currency, quantity, item_type, item_id, item_name, item_category, description, member_id, staff_id, location_id, payment_method, payment_status, payment_provider, provider_transaction_id, transaction_id, transaction_status, payment_last4, reason_code, billing_email, is_recurring, installments — plus the envelope columns.

Amounts, classification, and billing_email are visible at operations; payment_last4 and provider transaction ids only at full.

bookings.attendance

One row per booking/visit. Key columns:

id, class_session_id, member_id, occurred_at, booking_status, booked_at, checked_in_at, cancelled_at, cancellation_window_minutes, sale_source_external_id, plan_source_external_id — plus the envelope columns.

sale_source_external_id and plan_source_external_id are what the diagnostic uses to connect each attendance to the plan and sale behind it.

The graph schema (derived)

The graph.* tables are a derived layer, rebuilt from people, bookings and commerce — they hold no source data of their own. See Graph & relationship tools for what the model means; the tables below are what you query directly. Person/location keys are loose foreign keys to people.member.id / people.location.id.

graph.member_resilience

One row per (member, location). The connection-strength model.

person_id, location_id, w_ml, w_peers, max_w_ms, resilience, activity_affinity, contract_commitment, resilience_constrained, vuln_quadrant (resilient_enthusiast | constrained_captive | flexible_explorer | specialist_nomad), computed_at.

resilience_constrained is the headline connection-strength score; vuln_quadrant is the activity-affinity × contract-commitment taxonomy.

graph.member_affinity

One row per member — the routing rollup.

person_id, primary_staff_id, top3_staff_ids, staff_concentration_pct, primary_edge_weight, primary_edge_tier, computed_at.

graph.staff_concentration

One row per staff member. primary_member_count is routing value — how many members' connections are anchored on this staff member.

staff_id, primary_member_count, total_member_edges, avg_edge_weight, computed_at.

graph.location_resilience

One row per location — operational resilience.

location_id, ops_resilience, cover_depth, staff_count, single_points_of_failure, computed_at.

graph.risk_signal

The attention-signal queue — the sense layer, distinct from the connection- strength scores above. The open queue is WHERE resolved_at IS NULL.

id, person_id, node_id, signal_type (drift | freq_softening | missed_second_visit | milestone | pause_drift | at_risk_threshold), severity (low | medium | high), detected_at, context (jsonb — supporting facts and routing), resolved_at, resolution.

graph.node and graph.edge

The underlying entities and relationships. graph.node carries one row per entity (entity_type, entity_id, display_label); graph.edge carries the weighted relationships (from_node_id, to_node_id, kind, edge_class MS/ML/SL/MM/SS, weight, tier, last_observed_at, status). Prefer the rollups above and get_entity_edges for most questions.

Getting the full, live picture

This page documents the shape; your studio's database is authoritative.

Because the guarded views and the read tools enforce scoping, you can query freely with execute_query (SELECT/WITH only) without having to track the column-level PII map yourself.

Last updated

Was this helpful?