SaaS

SaaS

SaaS10 data marts99 fields8 calculated fieldsView on GitHubVlad FlaksRus Obolonsky

Account12 fields8 relationshipsInvoices17 fields2 relationshipsMarketing Spend7 fields1 relationshipPlan7 fields1 relationshipSubscription14 fields4 relationshipsSubscription Events10 fields2 relationshipsSupport Tickets9 fields2 relationshipsTrials8 fields1 relationshipUsage (daily)7 fields2 relationshipsUser8 fields3 relationships

Accounts move from marketing and trials through subscriptions and seat expansion, with usage, support and billing signals deciding who stays, and every plan change captured as an MRR movement.

Hand this to your AI

Finance, sales and customer success each calculate net revenue retention their own way, and the board gets three numbers.

Paste this into your AI assistant. It builds the SaaS model's Data Marts in your data warehouse from your billing, product and marketing data, and calculates net revenue retention once, split into expansion, contraction and churn.

The prompt

build the SaaS model with its Account, Subscription and Subscription Events Data Marts, then calculate net revenue retention for the last twelve months and show how much came from expansion versus what contraction and churn took back

Overview

A B2B subscription software business modeled end to end — from the marketing that brings accounts in, through trials, subscriptions and seat expansion, to the product usage, support experience and billing that decide whether they stay. Recurring revenue is the spine: every plan change flows through an MRR movement (new, expansion, contraction, churn) in Subscription Events, while engagement, support and payment health all feed the retention story. Account is the company behind every subscription, and the dimension the whole business is sliced by.

How this business is managed

  • Growth — brings in new accounts at an acquisition cost the business can sustain.
  • Revenue — grows recurring revenue faster than churn and downgrades take it back, and collects what it bills.
  • Customer Success — keeps customers satisfied and engaged, so they renew and grow.

Key metrics

Key Data Marts

  • Account — every customer company, with its plan tier, owner and health score.
  • Subscription Events — every MRR movement: new, expansion, contraction, reactivation, churn.
  • Marketing Spend — what each channel costs, and the leads and signups it brings.
  • Support Tickets — every support conversation, its satisfaction score and response time.

Example Questions

  • What is net revenue retention, and how much of it is expansion versus what contraction and churn take back?
  • Which acquisition channels pay back their cost fastest once you account for the revenue those accounts actually retain?
  • Do accounts with deeper product usage and better support experiences expand more and churn less?

Explore on canvas →

Relevant Data Sources

Account Data Mart

Account

Every customer account (a company), with the signals that frame the whole relationship: industry, size and region, the plan tier they are on, a revenue size band, the channel that acquired them, their success-manager owner, a product-health score, and where they sit in the lifecycle (trial, active, at-risk, churned). The dimension you slice the entire business by. What each acquisition channel costs is in Marketing Spend.

Fields

ColumnTypeAliasDescription
account_idSTRINGAccount IDPK. Unique account identifier.
nameSTRINGNameCompany name. Unique per account.
industrySTRINGIndustryIndustry vertical of the account.
employee_bandSTRINGEmployee BandCompany-size bucket by headcount.
plan_tierSTRINGPlan TierThe tier the account buys at. It sets the account's seat count and the plan its subscriptions sit on (a grandfathered few one tier below). Cut revenue by tier here or through Subscription → Plan.
mrr_bandSTRINGMRR BandMonthly-recurring-revenue size bucket.
regionSTRINGRegionSales/geographic region.
acquisition_channelSTRINGAcquisition ChannelMarketing channel that sourced the account — blended-CAC join key.
signup_dateDATESignup DateDate the account first signed up.
csm_ownerSTRINGCSM OwnerCustomer success manager who owns the account.
health_scoreINTEGERHealth Score0–100 product-health composite.
lifecycle_stageSTRINGLifecycle Stagetrial (no subscription yet) / active / at-risk (holds a live subscription) / churned (every subscription canceled).

Invoices Data Mart

Invoices

Every invoice and how it was paid — amount, tax, discounts and credits, payment status, and where it sits in the dunning cycle when a payment fails. This is where voluntary revenue meets involuntary churn: failed payments and collections stages that quietly erode the customer base.

This is the model's revenue mart, and it carries two figures on purpose: amount is what was billed on every invoice regardless of outcome, and net_amount is what was actually recognised — zero for a voided or written-off invoice, equal to amount otherwise. Any revenue total should sum net_amount, never amount. The four collection signals — status, dunning_stage, is_failed and paid_at — always agree with each other: a paid invoice always has a paid_at and never is_failed or a write-off, and is_write_off is exactly dunning_stage = 'write_off'.

Fields

ColumnTypeAliasDescription
invoice_idSTRINGInvoice IDPK. Unique invoice identifier.
account_idSTRINGAccount IDAccount billed. FK to Account
subscription_idSTRINGSubscription IDSubscription being billed. FK to Subscription
issued_atDATEIssued AtDate the invoice was issued — a billing anniversary of the subscription: monthly or yearly by its interval. No invoice is issued after cancellation or between a churn and its win-back.
period_startDATEPeriod StartStart of the billing period.
period_endDATEPeriod EndEnd of the billing period.
amountNUMERICAmountInvoice amount before tax, in USD: the subscription's MRR in force on the billing day, times twelve for an annual subscription. Booked on every status, including void and write-off — see net_amount for the recognised figure.
net_amountNUMERICNet AmountRecognised revenue: 0 when the invoice is void or written off, otherwise equal to amount. Sum this for revenue totals, not amount.
taxNUMERICTaxTax charged on the invoice, at the account region's rate.
statusSTRINGStatusPayment status of the invoice.
currencySTRINGCurrencyCurrency the customer is billed in, aligned to the account's region. The amounts are stated in USD.
discount_amountNUMERICDiscount AmountList price the subscription's negotiated discount took off this invoice, if any. amount is already net of it.
credit_appliedNUMERICCredit AppliedAccount credit applied to the invoice, if any.
dunning_stageSTRINGDunning StageCollections stage: none / retry_1 / retry_2 / final_notice / write_off.
paid_atDATEPaid AtDate the invoice was paid. Set if and only if status = 'paid'.
is_failedBOOLEANIs FailedFailed payment — involuntary-churn signal. Never true on a paid invoice.
is_write_offBOOLEANIs Write OffUncollectible invoice, exactly dunning_stage = 'write_off'. Use this to gate money rather than string-matching the dunning label.
failed_payment_rateFLOATFailed Payment Rate

Calculated Field. Share of invoices whose payment failed — the leading signal of involuntary churn. A conditional count over all invoices issued in the selected rows.

SAFE_DIVIDE(COUNTIF(is_failed), COUNT(invoice_id))

Relationships

Related data martOnCardinalityMeaning
Accountaccount_id = account_idN:1The account billed on this invoice.
Subscriptionsubscription_id = subscription_idN:1The subscription this invoice bills.

Marketing Spend Data Mart

Marketing Spend

Marketing investment by channel, campaign and day — cost, leads, and signups — attributed to the cohort of accounts each channel brings in rather than to individual accounts. The numerator and denominator of customer acquisition cost, and the starting point for payback analysis.

Fields

ColumnTypeAliasDescription
spend_idSTRINGSpend IDPK. Unique identifier for each spend record.
spend_dateDATESpend DateDay the spend was incurred.
channelSTRINGChannelAcquisition channel the spend targeted — attributed to the cohort of accounts acquired through it, not to individual accounts. FK to Account
campaignSTRINGCampaignCampaign the spend belongs to.
costNUMERICCostMoney spent — the CAC numerator.
leadsINTEGERLeadsLeads generated by the spend.
signupsINTEGERSignupsAccounts created — CAC denominator.
cacFLOATCAC (per signup)

Calculated Field. Marketing cost per signup: summed cost over summed signups, right at every cut — by channel, campaign or month. Averaging a per-row CAC would weight a quiet day like a busy one.

SAFE_DIVIDE(SUM(cost), SUM(signups))

Relationships

Related data martOnCardinalityMeaning
Accountchannel = acquisition_channelN:NAccounts acquired through this channel — a cohort, not one account per row.

Plan Data Mart

Plan

The price book: every sellable plan and price point — four tiers (Starter, Pro, Business, Enterprise), each offered monthly or annually, with its list price and whether it is currently sellable. The reference that gives every subscription its list pricing.

Fields

ColumnTypeAliasDescription
plan_idSTRINGPlan IDPK. Unique plan/price-point identifier.
plan_nameSTRINGPlan NameHuman-readable plan name (tier + billing interval).
tierSTRINGTierProduct tier: Starter / Pro / Business / Enterprise.
billing_intervalSTRINGBilling IntervalBilling cadence: monthly / annual.
list_priceNUMERICList PricePublished price per seat for this plan's interval — per seat-month on a monthly plan, per seat-year on an annual one (ten months' price). A subscription's MRR is its seats times this monthly equivalent, less its discount.
currencySTRINGCurrencyCurrency of the list price.
is_activeBOOLEANIs ActiveWhether the plan is currently sellable.

Subscription Data Mart

Subscription

The billing spine: one row per subscription showing which account is on which plan, at what monthly recurring revenue and seat count, on what cadence, and with any negotiated discount. It also reveals which subscriptions stay on a plan one tier below the account's under grandfathered pricing. The source of truth for what each customer pays today.

Fields

ColumnTypeAliasDescription
subscription_idSTRINGSubscription IDPK. Unique subscription identifier.
account_idSTRINGAccount IDAccount that owns the subscription. FK to Account
plan_idSTRINGPlan IDPlan the subscription is billed on, at the subscription's own billing interval. On the account's tier, except about one in ten grandfathered one tier below. FK to Plan
statusSTRINGStatusSubscription status: active / past_due / canceled.
seats_licensedINTEGERSeats LicensedNumber of seats licensed on the subscription. Follows the size of the account, so a grandfathered subscription keeps its account's seats on the cheaper plan.
mrrNUMERICMRRContracted monthly recurring revenue in USD: seats times the plan's per-seat monthly price, less the discount. The rate on the agreement, unchanged by cancellation.
active_mrrNUMERICActive MRRLive monthly recurring revenue: still contracted unless canceled, when it drops to 0. Equal to mrr for active and past_due subscriptions alike — a failed payment does not by itself stop the contract.
currencySTRINGCurrencyCurrency the customer is billed in, aligned to the account's region. Every amount in this model is stated in USD.
billing_intervalSTRINGBilling IntervalBilling cadence: monthly / annual.
started_atDATEStarted AtDate the subscription started — on or after its account signed up. Its billing anniversaries fall on this day.
current_period_startDATECurrent Period StartStart of the current billing period — the latest billing anniversary, or the last one before cancellation.
current_period_endDATECurrent Period EndEnd of the current billing period.
canceled_atDATECanceled AtDate the subscription was canceled, if it was. Every subscription of a churned account is canceled.
discount_pctNUMERICDiscount %Negotiated discount fraction applied to list price, if any.
arpaFLOATARPA

Calculated Field. Average live MRR per paying account. Divides by distinct accounts, not subscriptions — many accounts hold several — and leaves canceled subscriptions out of both sides.

SAFE_DIVIDE(SUM(active_mrr), COUNT(DISTINCT IF(active_mrr > 0, account_id, NULL)))
discount_bandSTRINGDiscount Band

Calculated Field. List price, Light, Moderate or Deep discount, from the negotiated discount. A row-level formula that behaves as a dimension: group and filter by it.

CASE WHEN discount_pct IS NULL OR discount_pct = 0 THEN 'List price' WHEN discount_pct < 0.12 THEN 'Light (5-11%)' WHEN discount_pct < 0.19 THEN 'Moderate (12-18%)' ELSE 'Deep (19%+)' END

Relationships

Related data martOnCardinalityMeaning
Accountaccount_id = account_idN:1The account paying for this subscription.
Planplan_id = plan_idN:1The plan this subscription is billed on.

Subscription Events Data Mart

Subscription Events

The movement history behind recurring revenue: one row per subscription change — new, expansion, contraction, reactivation, or churn — with the signed revenue and seat deltas and the running revenue after each change. This is what reconstructs the revenue waterfall and the retention rates the business lives or dies by.

Running revenue reconciles with each subscription's own contracted rate. A subscription still on the books finishes the series on that rate; a subscription that churned was carrying it in the period immediately before the churn and the series steps away from it — so a churn reads as revenue lost from a customer's full rate, not as a downgrade on the way up to it.

Fields

ColumnTypeAliasDescription
event_idSTRINGEvent IDPK. Unique subscription-event identifier.
account_idSTRINGAccount IDAccount the event belongs to. FK to Account
subscription_idSTRINGSubscription IDSubscription the event belongs to. FK to Subscription
event_tsTIMESTAMPEvent TimeWhen the subscription change occurred.
event_typeSTRINGEvent TypeMRR-movement type: new (an account's first subscription starting) / expansion (more seats, a higher tier, or an existing account's additional subscription starting) / contraction / reactivation (a win-back after a churn) / churn.
plan_fromSTRINGPlan FromPlan tier before the change. Empty when a subscription starts or is won back. Differs from plan_to only on a tier change; a seat change keeps the tier.
plan_toSTRINGPlan ToPlan tier after the change. Empty on a churn.
mrr_deltaNUMERICMRR DeltaSigned MRR change — the MRR-movement waterfall. Always the step between two consecutive levels, so the deltas of a subscription add up to the movement in its recurring revenue over the period.
seats_deltaINTEGERSeats DeltaSigned change in seat count: all of the subscription's seats on a start, win-back or churn, zero on a pure tier change.
mrr_afterNUMERICMRR AfterThe subscription's MRR after the change, reconciled with its contracted rate: a live subscription ends on that rate, and a canceled one was carrying it immediately before its churn, which takes it to 0.
mrr_gainedNUMERICMRR Gained

Calculated Field. Recurring revenue added: new, expansion and reactivation MRR summed at whatever grain the report asks for. Group by event_type to split it into its three sources.

SUM(IF(event_type IN ('new', 'expansion', 'reactivation'), mrr_delta, 0))
mrr_lostNUMERICMRR Lost

Calculated Field. Recurring revenue taken back: contraction and churned MRR, as a positive amount. Group by event_type to see how much is downgrades and how much is customers leaving.

-SUM(IF(event_type IN ('contraction', 'churn'), mrr_delta, 0))
net_new_mrrNUMERICNet New MRR

Calculated Field. Net change in recurring revenue: MRR Gained minus MRR Lost. Reads the two formulas, so all three always agree.

mrr_gained - mrr_lost
quick_ratioFLOATSaaS Quick Ratio

Calculated Field. Growth efficiency: MRR Gained per dollar of MRR Lost. Above 1 the base grows; the higher, the less growth is spent replacing what leaves. NULL where nothing was lost.

SAFE_DIVIDE(mrr_gained, mrr_lost)

Relationships

Related data martOnCardinalityMeaning
Accountaccount_id = account_idN:1The account whose recurring revenue moved.
Subscriptionsubscription_id = subscription_idN:1The subscription this change was made to.

Support Tickets Data Mart

Support Tickets

Every support interaction — priority, topic, satisfaction score, time to first response, and resolution time. Support experience is an early churn-risk signal: unhappy, slow-to-resolve accounts are the ones that quietly leave — read it beside how much they use the product, in Usage (daily).

Fields

ColumnTypeAliasDescription
ticket_idSTRINGTicket IDPK. Unique support-ticket identifier.
account_idSTRINGAccount IDAccount that opened the ticket. FK to Account
user_idSTRINGUser IDUser who opened the ticket, if attributable. FK to User
opened_atTIMESTAMPOpened AtWhen the ticket was opened.
closed_atTIMESTAMPClosed AtWhen the ticket was closed.
prioritySTRINGPriorityTicket priority level.
categorySTRINGCategoryTicket topic/category.
csat_scoreINTEGERCSAT ScoreCustomer satisfaction rating for the ticket.
first_response_minsINTEGERFirst Response (minutes)Minutes to first agent response.

Relationships

Related data martOnCardinalityMeaning
Accountaccount_id = account_idN:1The account that opened the ticket.
Useruser_id = user_idN:1The seat that opened the ticket, when known.

Trials Data Mart

Trials

Every trial and how it ended — converted to paid or not — with when it started and expired, where it came from (self-serve, sales-assisted, product-led upsell), and the plan it was evaluating. The top of the funnel for new recurring revenue.

Fields

ColumnTypeAliasDescription
trial_idSTRINGTrial IDPK. Unique trial identifier.
account_idSTRINGAccount IDAccount running the trial. FK to Account
started_atTIMESTAMPStarted AtWhen the trial began.
ends_atTIMESTAMPEnds AtScheduled trial expiry.
converted_atTIMESTAMPConverted AtWhen the trial converted to a paid plan, if it did.
is_convertedBOOLEANIs ConvertedTrial-to-paid outcome flag.
trial_sourceSTRINGTrial SourceWhere the trial came from (self-serve, sales-assisted, PLG upsell).
requested_planSTRINGRequested PlanPlan tier the trial is evaluating.

Relationships

Related data martOnCardinalityMeaning
Accountaccount_id = account_idN:1The account that ran the trial.

Usage (daily) Data Mart

Usage (daily)

Daily product engagement at the account-and-user level: active minutes, high-value actions taken, and how many distinct features were touched — the breadth signal for activation. The behavioral pulse that explains why accounts expand, stall, or churn.

Fields

ColumnTypeAliasDescription
usage_idSTRINGUsage IDPK. Unique daily-usage record identifier.
account_idSTRINGAccount IDAccount of the seat that generated the usage. No usage is recorded after an account has left, so a churned account's rows are from before it churned. FK to Account
user_idSTRINGUser IDUser that generated the usage. FK to User
usage_dateDATEUsage DateCalendar day of the usage.
active_minutesINTEGERActive MinutesMinutes the user was active in-product.
key_actionsINTEGERKey ActionsCount of high-value actions taken.
distinct_features_usedINTEGERDistinct Features UsedCount of distinct product features touched that day — activation breadth.

Relationships

Related data martOnCardinalityMeaning
Accountaccount_id = account_idN:1The account that generated the usage.
Useruser_id = user_idN:1The seat that generated the usage.

User Data Mart

User

The people inside each account: one row per user seat, with role, seat type, when they were invited, and how recently they were active. Seat-level activity is what turns a licensed account into an adopted one — and idle seats are early signs of shrinking value.

Fields

ColumnTypeAliasDescription
user_idSTRINGUser IDPK. Unique user identifier.
account_idSTRINGAccount IDOwning account. FK to Account
emailSTRINGEmailUser's email address, on the account's domain.
roleSTRINGRoleUser's role within the account.
seat_typeSTRINGSeat TypeType of seat assigned (e.g. full / viewer).
invited_atTIMESTAMPInvited AtWhen the user was invited — after the account signed up.
last_active_atTIMESTAMPLast Active AtMost recent activity timestamp.
is_activeBOOLEANIs ActiveWhether the seat is currently active.

Relationships

Related data martOnCardinalityMeaning
Accountaccount_id = account_idN:1The account a seat belongs to.

Apply to your project

  1. 1

    Install the Import Model plugin

    One plugin, installed once, in your own OWOX workspace.

    Get the plugin →

  2. 2

    Import this model

    Point it at this bundle and it creates every data mart above, joins and all.

    Open the model →

  3. 3

    Plug in your data and destinations

    Connect your own sources and send the results where your team already works.

    Browse connectors →

4. Optional — customize as you wish. Rename a column, drop a mart, add your own: once it is imported it is yours, and nothing here syncs back.

Customer stories